Python爬虫实战:从东方财富网稳定获取股票数据 1. 项目缘起为什么是东方财富网做量化分析、策略回测或者只是想看看市场动态数据是第一步。市面上免费的股票数据接口不少但要么数据不全要么更新不及时要么就是突然哪天就不好使了。对于个人开发者或者数据分析爱好者来说一个稳定、免费、数据维度相对丰富的来源就显得格外重要。东方财富网作为国内主流的财经门户其网页上公开的股票数据就成了一个绕不开的“数据富矿”。我最近在做一个简单的市场情绪分析工具需要批量获取一批股票的历史行情和基本面数据。像Tushare、AKShare这类封装好的库固然方便但有时会遇到接口调用频率限制或者需要的数据字段它们没有提供。更重要的是直接通过爬虫从源头获取数据能让你对整个数据获取链路有更深的掌控感遇到数据异常时你也知道该去哪里核对该怎么调整。这不仅仅是“拿到数据”更是“理解数据从哪里来、如何来”的过程。所以这次我们就以东方财富网为目标手把手构建一个简单但健壮的股票数据爬虫。这个爬虫的核心目标很明确稳定、准确、可复用。我们不追求一次性爬取海量数据导致IP被封而是设计一个能长期、温和、有效地获取所需数据的方案。本文将涵盖从环境准备、页面分析、请求构造、数据解析到异常处理和本地存储的完整流程并分享我在这个过程中踩过的坑和总结的经验。2. 环境准备与核心工具选型工欲善其事必先利其器。一个高效的爬虫项目离不开合适的工具链。我们的选择基于几个原则轻量、主流、社区支持好、能应对常见的反爬策略。2.1 Python与必备库核心语言自然是Python版本建议3.8及以上。我们需要安装以下几个库Requests: 用于发送HTTP请求。这是最基础、最核心的库比Python内置的urllib更友好、功能更强大。Pandas: 用于数据处理和存储。爬取到的表格数据用Pandas的DataFrame来处理和保存为CSV或Excel文件是标准操作。BeautifulSoup4 (bs4): 用于解析HTML文档。当我们需要从复杂的HTML标签中提取数据时它比正则表达式更直观、更稳定。lxml: 作为BeautifulSoup的解析器后端。lxml的解析速度通常比Python标准库的html.parser更快对不规则HTML的容错性也更好。安装命令非常简单在命令行中执行pip install requests pandas beautifulsoup4 lxml2.2 为什么选择RequestsBeautifulSoup而不是Scrapy这是一个常见的选型问题。Scrapy是一个强大的、异步的爬虫框架适合构建大型、复杂的爬虫项目它内置了请求调度、去重、管道处理等高级功能。但对于我们“简单爬虫”的定位——目标明确东方财富网、页面结构相对固定、数据量不大——使用Scrapy就显得有些“杀鸡用牛刀”了。它的学习曲线更陡峭项目结构也更复杂。Requests BeautifulSoup的组合更加轻量、灵活、直观。你可以清晰地看到每一个请求是如何发出的每一个响应是如何被解析的非常适合初学者理解和快速上手也足以应对东方财富网这类静态页面的数据抓取。我们的爬虫逻辑将非常线性构造URL - 发送请求 - 解析HTML - 提取数据 - 保存。这种模式用Requests和BeautifulSoup来实现是最自然不过的。2.3 开发工具与调试准备一个好的代码编辑器如VS Code、PyCharm和浏览器开发者工具是必备的。后者是我们分析目标网页结构的“眼睛”。打开浏览器以Chrome为例打开东方财富网任意一个股票页面例如http://quote.eastmoney.com/sz000001.html平安银行。打开开发者工具按F12或右键选择“检查”。使用“元素Elements”面板这里可以看到网页完整的HTML DOM树。我们的任务是找到目标数据如股价、涨跌幅、市盈率等在HTML中对应的标签和属性。使用“网络Network”面板这是爬虫分析中更关键的一步。刷新页面在Network面板中查看浏览器发送了哪些请求特别是XHR/Fetch类型的请求。很多时候网页上动态加载的数据如K线图、分时成交明细并不是直接写在初始HTML里的而是通过额外的Ajax请求获取的JSON数据。直接找到这个数据接口比解析HTML要高效和稳定得多。注意东方财富网对爬虫有一定的反爬措施虽然不算特别严格但我们仍需保持“礼貌”。主要注意两点一是设置合理的请求头User-Agent模拟真实浏览器二是在请求间添加随机延时避免短时间内高频访问。我们会在代码中具体体现。3. 目标页面分析与数据接口定位这是爬虫成功与否最关键的一步。盲目地开始写代码很容易陷入与复杂HTML结构斗争的泥潭。正确的做法是先花时间仔细分析找到最高效的数据获取路径。3.1 分析静态页面元素以个股详情页为例我们首先打开平安银行sz000001的页面。在页面上我们可以看到股票名称、当前价、涨跌幅、市盈率PE、总市值等数据。右键“检查”其中一个数据比如当前价。你会发现这些关键数据很可能被包裹在具有特定id或class的span或div标签里。例如股价可能在一个idprice9的span里。用BeautifulSoup可以通过这些属性轻松定位。# 假设我们已经获取了页面的HTML内容 soup price_element soup.find(span, idprice9) if price_element: current_price price_element.text print(f当前价: {current_price})这种方法适用于数据直接嵌入在初始HTML中的情况。但它的缺点是脆弱一旦网站前端改版修改了HTML结构或标签的id/class我们的爬虫就可能失效。因此这通常作为备用方案或者用于获取那些确实没有独立接口的数据。3.2 寻找动态数据接口推荐方法更可靠的方法是寻找背后的数据API。我们打开开发者工具的“网络Network”面板然后刷新股票页面。在请求列表中过滤XHR或Fetch类型的请求。你会看到很多请求其中很可能包含类似http://push2.eastmoney.com/api/qt/stock/get?ut...fltt2fieldsf43,f57,f58,f169,f170...secid0.000001这样的请求。点击这个请求查看它的“预览Preview”或“响应Response”标签页你会惊喜地发现这里是一个结构清晰的JSON对象里面包含了股价、涨跌、成交量、市盈率、市值等几乎所有我们关心的数据这就是我们要找的“宝藏接口”。通过分析这个请求的URL参数和请求头我们可以用Python的Requests库完美地复现这个请求直接拿到结构化的JSON数据完全绕开HTML解析的麻烦。这种方式不仅高效而且更加稳定因为数据接口的变动频率通常远低于前端页面。3.3 解析关键请求参数以刚才发现的接口为例我们来拆解一下URL中的关键参数secid: 这是股票的唯一标识。0.000001表示深交所0的股票000001。对于上交所股票可能是1.600000。这个参数是核心。fields: 这个参数指定了需要返回哪些字段。比如f43可能代表最新价f57代表股票代码f58代表股票名称等。我们需要通过尝试或查找资料来明确每个字段的含义。fltt,ut,invt等: 这些可能是固定的校验参数或版本参数直接从浏览器捕获的请求中复制过来即可。我们的策略就是模拟浏览器发送一个完全相同的GET请求到这个接口URL然后解析返回的JSON。4. 爬虫核心代码实现经过前面的分析我们已经有了清晰的路线图。现在让我们开始编写代码。我将代码分为几个函数以提高可读性和可复用性。4.1 构建请求头与基础函数首先我们需要构建一个看起来像来自真实浏览器的请求头。import requests import pandas as pd import time import random def get_headers(): 构造请求头模拟浏览器访问 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, Referer: http://quote.eastmoney.com/, # 添加Referer更模拟真实行为 } return headersUser-Agent是必须的它告诉服务器我们是什么类型的客户端。Accept表明我们接受JSON格式的返回。Referer表示我们是从哪个页面跳转过来的加上它会使请求看起来更自然。4.2 获取单只股票数据的函数接下来我们实现获取单只股票信息的核心函数。这里我们使用分析得到的数据接口。def fetch_single_stock_data(secid): 根据secid获取单只股票的实时数据 :param secid: 股票代码格式如 0.000001 (深交所) 或 1.600000 (上交所) :return: 包含股票数据的字典如果失败返回None # 这是从Network面板分析得到的基础URLfields参数定义了需要的数据字段 # 你可以根据需要调整fields这里是一个示例包含了代码、名称、最新价、涨跌额、涨跌幅、成交量、成交额、市盈率等 url fhttp://push2.eastmoney.com/api/qt/stock/get params { ut: fa5fd1943c7b386f172d6893dbfba10b, fltt: 2, invt: 2, fields: f57,f58,f43,f169,f170,f46,f44,f45,f168,f47,f48,f49,f50,f51,f52,f53,f54,f55,f56,f92,f116,f117, secid: secid, } headers get_headers() try: # 发送GET请求设置超时时间 response requests.get(url, paramsparams, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 data_json response.json() # 接口返回的数据通常在 data 字段下的 data 键里有点绕看实际JSON结构 stock_data data_json.get(data, {}) if not stock_data: print(f股票 {secid} 未获取到有效数据。) return None # 将数据整理成更易读的字典 # 字段映射需要根据实际接口返回的字段名来调整这里是一个示例 field_map { f57: code, # 股票代码 f58: name, # 股票名称 f43: latest, # 最新价 f169: change, # 涨跌额 f170: change_pct, # 涨跌幅 f46: open, # 今开 f44: high, # 最高 f45: low, # 最低 f168: turnover_ratio, # 换手率 f47: volume, # 成交量手 f48: turnover, # 成交额 f49: pe_ttm, # 市盈率TTM f50: pb, # 市净率 f116: total_market_cap, # 总市值 f117: circulating_market_cap, # 流通市值 } result {} for field_key, field_name in field_map.items(): result[field_name] stock_data.get(field_key, N/A) return result except requests.exceptions.RequestException as e: print(f请求股票 {secid} 数据时发生错误: {e}) return None except ValueError as e: print(f解析股票 {secid} 的JSON数据时发生错误: {e}) return None代码解读与注意事项参数ut,fltt等这些值是我从浏览器请求中捕获的示例它们可能在一段时间内有效但并非永久不变。如果未来接口失效你需要重新用开发者工具捕获最新的请求参数。fields参数这是核心定义了你要哪些数据。字段代码如f57, f58的含义需要你自己去摸索或查找资料。一个笨办法是修改fields只留一个f57看返回什么再只留f58看返回什么以此类推。或者在网上搜索“东方财富接口字段说明”。异常处理我们捕获了网络请求异常和JSON解析异常。在实际爬虫中异常处理至关重要它能让你的程序在遇到个别错误时不会整体崩溃。数据清洗返回的JSON中数字可能是字符串形式且可能包含逗号如100,000。在后续处理中你可能需要将这些字符串转换为数值类型。4.3 批量获取与数据存储单只股票的数据获取后我们需要一个函数来批量处理股票列表并将结果保存下来。def batch_fetch_stocks_data(secid_list, delay1): 批量获取股票数据 :param secid_list: 股票secid列表如 [0.000001, 1.600000, 0.000002] :param delay: 每次请求之间的延迟秒数基础值用于避免请求过快 :return: 包含所有股票数据的DataFrame all_stocks_data [] for idx, secid in enumerate(secid_list): print(f正在获取第 {idx1}/{len(secid_list)} 只股票: {secid}) stock_info fetch_single_stock_data(secid) if stock_info: all_stocks_data.append(stock_info) # 礼貌性延迟避免给服务器造成压力 # 增加随机性使请求间隔不完全固定 time.sleep(delay random.uniform(0, 0.5)) # 将列表转换为Pandas DataFrame if all_stocks_data: df pd.DataFrame(all_stocks_data) # 可以在这里对DataFrame进行排序、重命名列等操作 # 例如按涨跌幅排序 # if change_pct in df.columns: # df[change_pct_num] df[change_pct].str.rstrip(%).astype(float) # df df.sort_values(bychange_pct_num, ascendingFalse) return df else: print(未获取到任何股票数据。) return pd.DataFrame() def save_to_csv(df, filenameeastmoney_stock_data.csv): 将DataFrame保存为CSV文件 if not df.empty: df.to_csv(filename, indexFalse, encodingutf-8-sig) # 使用utf-8-sig编码避免中文乱码 print(f数据已保存至 {filename}) else: print(DataFrame为空未保存文件。)关键点延迟delay这是爬虫的“道德”和“生存”关键。time.sleep(delay random.uniform(0, 0.5))这行代码在每次请求后暂停一段时间。delay是基础延迟random.uniform增加了一个随机扰动使得请求模式不那么规律更接近人工操作。对于东方财富网1-2秒的延迟通常是安全的。千万不要去掉延迟否则你的IP很快就会被限制访问。错误处理在循环中即使某只股票数据获取失败返回None程序也会继续处理下一只不会中断。数据存储我们使用Pandas的DataFrame来组织数据并最终保存为CSV文件。utf-8-sig编码可以确保在Excel中打开时中文不会乱码。4.4 主程序与示例运行最后我们编写一个主程序来串联整个流程。def main(): # 示例获取几只股票的实时数据 # secid格式深交所股票前加0.上交所股票前加1. stock_secid_list [ 0.000001, # 平安银行 1.600000, # 上证指数 (实际上是指数但接口可能也支持) 0.000002, # 万科A 1.600036, # 招商银行 0.300750, # 宁德时代 ] print(开始批量获取股票数据...) stock_df batch_fetch_stocks_data(stock_secid_list, delay1.5) if not stock_df.empty: print(\n获取到的股票数据预览) print(stock_df.head()) # 保存数据 save_to_csv(stock_df, feastmoney_stock_data_{time.strftime(%Y%m%d_%H%M%S)}.csv) else: print(未能获取到任何数据请检查网络或股票代码。) if __name__ __main__: main()运行这个程序你将在控制台看到获取进度并最终在项目目录下生成一个类似eastmoney_stock_data_20231230_143022.csv的文件里面就是你爬取到的结构化股票数据。5. 进阶获取历史K线数据实时行情数据固然有用但做分析往往更需要历史数据。东方财富网同样提供了历史K线数据的接口其分析思路与实时数据类似。5.1 历史K线接口分析在个股的K线图页面如http://quote.eastmoney.com/concept/sz000001.html打开开发者工具切换到“日K”等周期观察Network面板。你会找到一个返回K线数据的请求URL可能类似http://push2his.eastmoney.com/api/qt/stock/kline/get?secid0.000001klt101fqt1beg0end20500101分析其参数secid: 同上。klt: K线周期。101日线102周线103月线601分钟线等。fqt: 复权类型。1前复权2后复权0不复权。beg/end: 开始和结束日期格式YYYYMMDD。0和20500101可能代表获取所有可用数据。这个接口返回的JSON中data字段下的klines很可能是一个列表列表中的每一项是一个用逗号分隔的字符串包含了时间、开盘、收盘、最高、最低、成交量等信息。5.2 历史数据爬取函数实现我们可以仿照实时数据的函数编写获取历史K线的函数。def fetch_kline_data(secid, klt101, fqt1): 获取股票历史K线数据 :param secid: 股票代码 :param klt: K线周期101日线 :param fqt: 复权类型1前复权 :return: 包含K线数据的DataFrame url http://push2his.eastmoney.com/api/qt/stock/kline/get params { secid: secid, klt: klt, fqt: fqt, beg: 0, end: 20500101, fields1: f1,f2,f3,f4,f5, # 这些fields控制返回的元信息 fields2: f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61, # 这些fields控制K线数据字段 # fields2中f51时间f52开盘f53收盘f54最高f55最低f56成交量f57成交额f58振幅f59涨跌幅f60涨跌额f61换手率 } headers get_headers() try: response requests.get(url, paramsparams, headersheaders, timeout15) response.raise_for_status() data_json response.json() klines data_json.get(data, {}).get(klines, []) if not klines: print(f股票 {secid} 未获取到K线数据。) return pd.DataFrame() # 解析每一条K线字符串 data_list [] for kline_str in klines: # 例如: 2023-12-29,14.38,14.30,14.40,14.22,1063650,152345678.00,... parts kline_str.split(,) if len(parts) 11: # 确保有足够字段 data_list.append({ date: parts[0], open: float(parts[1]), close: float(parts[2]), high: float(parts[3]), low: float(parts[4]), volume: int(parts[5]), # 成交量股 amount: float(parts[6]), # 成交额元 amplitude: float(parts[7].rstrip(%)) if parts[7] ! else 0.0, # 振幅 change_pct: float(parts[8].rstrip(%)) if parts[8] ! else 0.0, # 涨跌幅 change: float(parts[9]), # 涨跌额 turnover: float(parts[10].rstrip(%)) if len(parts) 10 and parts[10] ! else 0.0, # 换手率 }) df_kline pd.DataFrame(data_list) # 将日期列转换为datetime类型便于后续时间序列分析 df_kline[date] pd.to_datetime(df_kline[date]) return df_kline except requests.exceptions.RequestException as e: print(f请求股票 {secid} K线数据时发生错误: {e}) return pd.DataFrame() except (ValueError, IndexError) as e: print(f解析股票 {secid} K线数据时发生错误: {e}) return pd.DataFrame()重要提示历史K线数据量可能很大频繁、大量爬取会给服务器带来巨大压力极易触发反爬机制导致IP被封。务必在每次请求间添加更长的延迟例如3-5秒或更长并且仅爬取你真正需要的数据。最好在非交易时段如深夜、周末进行小批量爬取。6. 反爬应对策略与实战经验即使我们如此“礼貌”在实际操作中仍可能遇到问题。以下是我总结的几个关键点和应对策略。6.1 请求头Headers的完善我们之前构造的请求头是最基础的。有些网站会检查更多的Header字段。你可以从浏览器中复制一个完整请求的Headers选取其中关键的几个添加到我们的get_headers()函数中例如Host,Origin,Sec-Fetch-*系列头等。但注意不是所有头都是必需的User-Agent和Referer通常是最关键的。6.2 IP限制与代理使用如果你在短时间内发送了太多请求可能会收到HTTP 403 Forbidden错误或者发现返回的数据是空或错误信息。这说明你的IP地址可能被暂时限制了。解决方案首要方案降低请求频率。这是最有效、最根本的方法。将batch_fetch_stocks_data函数中的delay参数调大比如从1.5秒增加到3秒或5秒。对于历史K线数据间隔应更长。使用代理IP池如果数据量实在巨大单IP无法满足就需要考虑使用代理。你可以购买付费代理服务或者使用一些免费的代理IP但免费代理通常不稳定、速度慢。使用代理时需要在requests请求中设置proxies参数。proxies { http: http://your_proxy_ip:port, https: https://your_proxy_ip:port, } response requests.get(url, headersheaders, proxiesproxies, timeout10)注意使用代理涉及额外的复杂性和成本且需要处理代理IP的失效问题。对于东方财富网这类数据通过合理控制频率通常不需要走到这一步。6.3 处理数据字段变更东方财富网的接口字段代码如f57,f58或其含义可能会发生变化。如果你的爬虫某天突然获取不到某个数据或者数据错位了首先应该去浏览器开发者工具里重新抓取一次请求对比一下现在的fields参数和返回的数据结构看看是否发生了变化。这是维护爬虫的日常工作。6.4 数据验证与清洗爬取到的数据不能直接信任必须进行基本的验证和清洗。检查数据完整性查看获取到的DataFrame是否有大量N/A或空值。检查数据合理性股价会不会是0或负数涨跌幅是不是一个离谱的数字成交量是否异常巨大可以设置一些简单的逻辑规则进行过滤。数据类型转换从JSON中取出的数字可能是字符串如14.50需要转换成float或int。包含百分比的字符串如2.50%需要去掉%并转换为float。去重与排序如果是历史数据检查是否有重复的日期并按日期排序。6.5 代码健壮性提升重试机制对于偶尔的网络超时可以加入简单的重试逻辑。def fetch_with_retry(url, params, headers, max_retries3): for attempt in range(max_retries): try: response requests.get(url, paramsparams, headersheaders, timeout10) response.raise_for_status() return response except requests.exceptions.RequestException as e: if attempt max_retries - 1: wait_time 2 ** attempt # 指数退避 print(f请求失败{wait_time}秒后重试... (尝试 {attempt 1}/{max_retries})) time.sleep(wait_time) else: raise e # 重试多次后仍失败抛出异常日志记录不要只使用print可以考虑使用Python的logging模块将程序运行状态、错误信息记录到文件便于后期排查问题。配置文件将股票代码列表、请求URL、字段映射、延迟时间等配置信息写入一个单独的配置文件如config.py或config.ini这样修改起来更方便不需要动主代码。7. 项目总结与扩展方向通过这个项目我们完成了一个从分析到实现的完整爬虫流程。它虽然“简单”但涵盖了爬虫最核心的步骤目标分析、请求模拟、数据解析、异常处理和持久化。这个爬虫可以作为你金融数据分析项目的一个可靠数据源模块。可以扩展的方向定时任务使用schedule库或操作系统的crontabLinux/任务计划程序Windows让爬虫在每天收盘后自动运行更新数据到数据库如SQLite、MySQL或直接追加到CSV文件。更多数据维度除了实时行情和历史K线还可以研究如何爬取财务数据利润表、资产负债表、资金流向、龙虎榜、新闻公告等。每个数据都可能对应不同的接口分析方法大同小异。构建股票代码列表如何获取全市场股票的secid列表你可以从东方财富的其他页面如板块成分股页面爬取或者利用其他数据源如AKShare库提供的股票列表进行转换。封装成类将上述函数封装成一个EastMoneySpider类使代码结构更清晰配置和管理更方便。可视化结合matplotlib或pyecharts等库将爬取到的数据绘制成K线图、走势对比图等让数据更直观。最后也是最关键的一点请务必尊重网站的服务条款和robots.txt文件将爬虫用于个人学习和研究目的控制请求频率避免对目标网站的正常运行造成干扰。技术是一把双刃剑合理使用才能创造价值。