1. 项目缘起:为什么是东方财富网?
作为一名在数据分析和量化策略领域摸爬滚打了十多年的从业者,我深知一手、及时、结构化的金融数据有多重要。无论是做简单的市场情绪分析,还是构建复杂的多因子模型,数据都是地基。市面上虽然有Wind、Choice这样的专业金融终端,但对于个人开发者、学生或者小型团队来说,成本是个不小的门槛。这时候,公开的财经网站就成了我们获取数据的“富矿”。
东方财富网,作为国内流量最大的财经门户之一,几乎涵盖了A股市场所有我们关心的信息:实时行情、公司公告、财务报表、龙虎榜、资金流向、股吧讨论……数据维度非常丰富。更重要的是,它的数据相对规整,页面结构稳定,对于希望通过技术手段获取数据的我们来说,是一个绝佳的练习场和实战数据源。我这次整理笔记,就是想系统地梳理一下从东方财富网抓取各类数据的思路、方法以及那些官方文档里不会写的“坑”,希望能给同样需要金融数据的朋友们一条清晰的路径。
2. 环境准备与核心工具选型
工欲善其事,必先利其器。爬虫项目的环境搭建看似简单,但工具选型直接决定了后续开发的效率和代码的健壮性。下面是我基于多年实战总结出的一套稳定组合。
2.1 Python生态:为什么是Requests + BeautifulSoup + Pandas?
对于东方财富网这类主要以静态HTML页面呈现数据的网站,我的首选工具链是Requests + BeautifulSoup + Pandas。这个组合轻量、灵活,学习曲线平缓,足以应对90%以上的数据抓取需求。
- Requests库:负责网络请求,是爬虫的“手”。它的API设计非常人性化,会话保持(Session)、请求头(Headers)模拟、代理设置等功能一应俱全。相比于更底层的
urllib,用Requests写出的代码可读性要高得多。 - BeautifulSoup库:负责解析HTML,是爬虫的“眼睛”。东方财富网的页面虽然结构复杂,但标签相对规范。BeautifulSoup支持多种解析器(如
lxml,速度更快),能让我们用类似find()、select()这样直观的方法,像使用CSS选择器一样精准定位到需要的数据标签。 - Pandas库:负责数据清洗与存储,是爬虫的“大脑”。爬下来的数据往往是文本、列表或字典格式,用Pandas的
DataFrame可以非常方便地进行格式化、去重、缺失值处理,并一键导出为CSV或Excel文件,无缝对接后续的数据分析流程。
为什么不直接用Scrapy?对于定向、目标明确的单站爬取(如只爬东方财富),Scrapy框架稍显“重”了。它的优势在于强大的异步处理、项目管理和中间件扩展,适合构建大型、分布式的爬虫系统。而我们当前的需求,用上述“三板斧”组合,开发速度更快,代码也更直观易懂。
2.2 关键配置:让你的请求更像“真人”
直接发送请求很容易被网站识别为爬虫并拒绝访问。因此,模拟浏览器行为是关键。这主要靠精心设置HTTP请求头(Headers)。
import requests from bs4 import BeautifulSoup # 定义一个常用的请求头,尽可能模拟Chrome浏览器 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': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1' } # 使用Session对象,可以自动管理Cookies,保持登录状态(如果需要) session = requests.Session() session.headers.update(headers) # 示例:访问东方财富网首页 url = 'https://www.eastmoney.com/' try: response = session.get(url, timeout=10) response.raise_for_status() # 检查请求是否成功 response.encoding = response.apparent_encoding # 自动识别编码 print("页面请求成功") except requests.exceptions.RequestException as e: print(f"请求失败: {e}")实操心得一:User-Agent轮换与超时设置不要永远使用同一个User-Agent。可以准备一个列表,随机选取,降低被屏蔽的风险。同时,务必为requests.get()设置timeout参数(如10秒),避免因网络问题导致程序长时间挂起。对于大规模爬取,还应该加入随机延时(如time.sleep(random.uniform(1, 3))),体现友好爬虫的伦理。
3. 实战解析:攻克东方财富网典型数据页面
东方财富网的数据分布在不同的子域名和页面路径下。我们需要针对不同页面的结构,定制化的编写解析逻辑。下面以几个最常用的数据场景为例,拆解其中的技术细节。
3.1 抓取个股实时行情与日K线数据
个股详情页(如https://quote.eastmoney.com/sz000001.html)包含了丰富的实时数据。但要注意,很多实时数据是通过JavaScript动态加载的,直接解析HTML可能拿不到。
策略:寻找数据接口(API)现代网站普遍采用前后端分离架构,数据通常通过Ajax请求特定的JSON接口获取。我们可以通过浏览器的“开发者工具”(F12),切换到“网络”(Network)选项卡,刷新页面,观察XHR或Fetch请求,找到返回数据的真实接口。
例如,东方财富的个股五档行情、分笔交易等数据,往往有独立的接口。找到接口后,直接用Requests模拟该接口的请求(可能需要携带特定的参数或Cookies),就能拿到结构清晰的JSON数据,比解析HTML方便得多。
# 示例:假设通过分析找到了获取某股票实时数据的API(此处为示例URL,非真实接口) stock_code = 'sz000001' api_url = f'https://push2.eastmoney.com/api/qt/stock/get?invt=2&fltt=2&fields=f43,f57,f58,f169,f170&secid={stock_code}' try: resp = session.get(api_url, headers=headers) data_json = resp.json() # 解析JSON数据 stock_data = data_json['data'] current_price = stock_data['f43'] # 当前价 change = stock_data['f170'] # 涨跌额 print(f"股票{stock_code}: 当前价 {current_price}, 涨跌额 {change}") except Exception as e: print(f"获取接口数据失败: {e}")对于历史日K线数据,东方财富有标准的数据中心页面,其数据也是通过接口加载,通常参数包含股票代码、开始日期、结束日期、K线周期(日K=101)等。分析并模拟这个接口请求,就能批量获取历史数据。
3.2 解析财务报表(年报/季报)
公司的财务数据是基本面分析的核心。东方财富的财务数据页面(如https://data.eastmoney.com/bbsj/000001.html)表格众多,结构规整,非常适合用BeautifulSoup的select方法结合Pandas处理。
步骤:
- 定位表格:使用浏览器检查元素,找到包含利润表、资产负债表等数据的
<table>标签,观察其id或class属性。 - 使用Pandas直接读取:Pandas的
read_html()函数能直接将HTML中的表格解析为DataFrame列表。这是处理这类结构化数据的“神器”。
import pandas as pd # 假设我们已经获取了财务报表页面的HTML内容 `financial_html` financial_soup = BeautifulSoup(financial_html, 'lxml') # 方法一:通过CSS选择器找到所有表格 tables = financial_soup.select('table') # 通常第一个或第二个大表是核心的利润表或资产负债表,需要具体分析 for i, table in enumerate(tables[:3]): # 先看前三个表格 df_list = pd.read_html(str(table)) if df_list: print(f"表格 {i} 解析成功,形状:{df_list[0].shape}") # 这里可以进一步判断哪个df是我们需要的 # 方法二:如果知道表格的特定class或id,直接定位 # 例如,发现利润表有一个id为`dt_1` try: income_statement_df = pd.read_html(str(financial_soup.find('table', id='dt_1')))[0] print("利润表数据:") print(income_statement_df.head()) except: print("未找到指定的利润表")实操心得二:表格数据的清洗pd.read_html()解析出来的DataFrame往往带有合并的表头、多余的空行或索引列。后续清洗工作必不可少:
df.dropna(how='all')删除全为空的行。df.dropna(axis=1, how='all')删除全为空的列。- 使用
df.iloc或df.columns重新设置正确的列名。 - 注意货币单位(如“亿元”)和百分比符号,可能需要将其转换为纯数字。
3.3 抓取公司公告列表与详情
公告数据是事件驱动策略的重要输入。公告列表页(如https://data.eastmoney.com/notices/stock/000001.html)通常是分页的,并且有筛选条件。
策略:处理分页与详情页链接
- 分析分页规律:观察点击“下一页”时URL或请求参数的变化。通常是
pageNum或page参数递增。构造一个循环,即可遍历所有页面。 - 提取公告链接:在列表页解析出每条公告的标题和详情页链接(
<a>标签的href属性)。 - 串行访问详情页:遍历链接列表,逐个请求详情页,提取公告正文、发布时间等。这里必须加入延时,避免对服务器造成过大压力。
import time base_url = 'https://data.eastmoney.com/notices/stock/000001.html' notice_list = [] for page in range(1, 6): # 假设爬取前5页 # 构造带页码的URL,具体参数需根据实际网站分析 list_url = f'{base_url}?pageNum={page}' list_resp = session.get(list_url) list_soup = BeautifulSoup(list_resp.text, 'lxml') # 假设公告链接在 class 为 ‘notice-item’ 的div里的a标签中 items = list_soup.select('.notice-item a') for item in items: notice_title = item.text.strip() notice_link = item['href'] # 可能需要补全为绝对URL if notice_link.startswith('/'): notice_link = 'https://data.eastmoney.com' + notice_link # 访问详情页 time.sleep(1) # 重要!每次请求详情页前暂停1秒 detail_resp = session.get(notice_link) detail_soup = BeautifulSoup(detail_resp.text, 'lxml') # 解析详情页正文,假设在 id 为 ‘content’ 的div里 content_div = detail_soup.find('div', id='content') notice_content = content_div.get_text(strip=True) if content_div else 'N/A' notice_list.append({ 'title': notice_title, 'link': notice_link, 'content': notice_content[:200] # 只存储前200字符示例 }) print(f"第{page}页公告列表抓取完成") time.sleep(2) # 每爬完一页列表,再暂停2秒 # 转换为DataFrame df_notices = pd.DataFrame(notice_list)4. 数据清洗、存储与反爬应对策略
爬取只是第一步,让数据变得可用、可管理,并确保爬虫能长期稳定运行,才是更考验功夫的地方。
4.1 数据清洗:从杂乱到规整
爬取的原始数据往往包含大量噪音。清洗工作通常在Pandas DataFrame中进行:
- 处理缺失值:用
df.isnull().sum()检查缺失情况。对于数值列,可能用均值、中位数填充;对于文本列,可能填充为“未知”或直接删除。 - 格式统一:日期字符串可能有“2023-01-01”、“2023/01/01”等多种格式,使用
pd.to_datetime()统一转换。数字字符串可能包含逗号(如“1,234.56”)或百分号,需要先使用str.replace()清理,再用pd.to_numeric()转换。 - 去重:使用
df.drop_duplicates()根据关键字段(如公告ID、股票代码+日期)去除重复数据。 - 异常值处理:对于股价、交易量等数据,可以通过分位数(如
df[‘price’].quantile(0.99))或标准差方法,识别并处理极端异常值。
4.2 数据存储:选择适合的介质
根据数据量和用途,选择存储方式:
- CSV/Excel文件:适合小规模、一次性分析的数据。使用
df.to_csv(‘stock_data.csv’, index=False)保存。优点是简单直观,无需额外环境。缺点是不便于增量更新和复杂查询。 - SQLite数据库:轻量级单文件数据库,适合个人项目。使用Python内置的
sqlite3库,可以将清洗后的DataFrame通过df.to_sql(‘table_name’, conn)写入。它支持SQL查询,管理比多个CSV文件方便。 - MySQL/PostgreSQL数据库:如果数据量很大,或者需要多用户、多程序并发访问,就需要专业的数据库服务器。这是生产级项目的标配。
import sqlite3 # 存储到SQLite conn = sqlite3.connect('eastmoney_data.db') df_cleaned.to_sql('daily_quotes', conn, if_exists='replace', index=False) # 替换模式 # if_exists='append' 可用于增量添加数据 conn.close()4.3 反爬虫策略与应对之道
东方财富网作为大型网站,肯定有反爬虫机制。除了之前提到的设置Headers和添加延时,还可能遇到以下问题及应对策略:
| 遇到的现象 | 可能的原因 | 应对策略 |
|---|---|---|
| 返回状态码403/404 | IP地址被识别为爬虫并封禁 | 1.使用代理IP池:购买或搭建代理IP服务,在请求时随机切换。这是应对IP封锁最有效的方法。 2.降低请求频率:大幅增加 time.sleep()的间隔,模拟真人浏览。 |
| 返回的HTML中包含“请验证”、“访问过于频繁”等提示 | 触发了基于行为(如请求速度、会话)的风控 | 1.模拟完整会话:使用requests.Session(),并在首次访问时获取必要的Cookies。2.随机化操作:不仅延时随机化,访问顺序也可以在一定范围内随机。 |
| 数据通过JavaScript加密或混淆 | 网站为了增加爬取难度,对关键数据进行了前端加密 | 1.分析JS代码:在开发者工具的“源代码”中搜索关键数据字段,找到解密函数,用Python的execjs库执行。2.使用Selenium:直接控制浏览器渲染页面,等数据加载完成后再从DOM中提取。这是“终极”但最慢的方法。 |
实操心得三:关于Selenium的使用只有当Requests+BeautifulSoup完全无法获取数据(即数据由JS动态生成且没有公开接口)时,才考虑Selenium。它通过WebDriver控制真实浏览器(如Chrome),能执行所有JS,但代价是速度极慢、资源占用高。使用时务必配合WebDriverWait和expected_conditions来等待元素加载,避免因页面加载慢而报错。对于大规模爬取,Selenium通常不是首选。
5. 项目架构与代码组织建议
当要爬取的数据类型增多(如行情、财务、公告、研报),代码很容易变得混乱。一个好的项目结构能极大提升可维护性。
eastmoney_crawler/ ├── config.py # 配置文件,存放请求头、代理列表、数据库连接字符串等 ├── utils.py # 工具函数,如网络请求封装、日志设置、延时函数 ├── parsers/ # 解析器模块 │ ├── quote_parser.py # 行情数据解析 │ ├── financial_parser.py # 财务报表解析 │ └── notice_parser.py # 公告数据解析 ├── storages/ # 存储模块 │ ├── csv_storage.py │ └── db_storage.py ├── spiders/ # 爬虫主逻辑 │ └── stock_spider.py # 统筹调度各解析器和存储器 ├── data/ # 存放爬取的原始或清洗后的数据文件 └── main.py # 程序主入口核心思想是“高内聚、低耦合”:每个解析器只负责解析一种页面的逻辑;存储模块负责所有与IO相关的操作;主爬虫负责组装流程(获取URL -> 调用解析器 -> 调用存储器)。这样,当东方财富网的页面结构发生变化时,你只需要修改对应的解析器,而不用动其他部分的代码。
6. 法律、伦理与数据使用边界
这是所有数据爬取者必须严肃对待的一课。爬取公开数据本身可能不违法,但行为方式和使用目的可能触碰红线。
- 遵守Robots协议:访问
https://www.eastmoney.com/robots.txt,查看网站允许或禁止爬虫访问的路径。尊重这些规则是基本的网络礼仪。 - 控制访问频率:这是最重要的伦理准则。你的爬虫不应该对目标网站的正常运营造成任何可感知的影响(如服务器负载显著升高)。这既是技术上的自我保护(避免被封),也是道德上的要求。
- 明确数据用途:爬取的数据应用于个人学习、研究或非商业用途。绝对禁止将数据用于非法活动、商业倒卖或任何可能损害东方财富网及其用户利益的行为。
- 关注用户协议:仔细阅读网站的用户协议,其中可能对数据抓取和使用有明确条款。违反协议可能导致法律风险。
- 保护个人隐私:如果爬取过程中意外接触到非公开的个人信息(这在财经网站不常见,但在股吧等UGC区域需留意),应立即停止并删除相关数据。
我的个人原则是:只取所需,慢速渐进,心存敬畏。技术是中立的,但使用技术的人需要为自己的行为负责。通过这个项目,我们锻炼的是技术能力,但时刻不能忘记法律和伦理的框架。
7. 从数据到价值:简单的分析示例
爬取数据不是终点,让数据产生洞察才是目的。这里举一个最简单的例子:计算一批股票最近一段时间的平均日收益率和波动率。
假设我们已经爬取并清洗好了多只股票的历史日K线数据,并存储在一个Pandas DataFramedf_history中,包含code(股票代码)、date(日期)、close(收盘价)等字段。
import numpy as np # 计算每日收益率 df_history[‘return’] = df_history.groupby(‘code’)[‘close’].pct_change() # 按股票代码分组,计算平均收益率和收益率的标准差(波动率) summary = df_history.groupby(‘code’)[‘return’].agg([‘mean’, ‘std’]) summary.columns = [‘avg_daily_return’, ‘daily_volatility’] # 年化处理(假设一年有242个交易日) summary[‘annualized_return’] = summary[‘avg_daily_return’] * 242 summary[‘annualized_volatility’] = summary[‘daily_volatility’] * np.sqrt(242) print(summary.sort_values(by=‘annualized_return’, ascending=False).head(10))这个简单的分析可以帮你快速筛选出在一段时间内表现强势且波动特征各异的股票,作为进一步深度研究的基础。数据的价值,正是在于通过这样的加工和分析,从杂乱的信息中提炼出有意义的信号。
整个爬取东方财富网数据的项目,从环境搭建到数据应用,是一个完整的闭环。它考验的不仅是Python编程和HTML解析能力,更是对网络协议、数据清洗、系统架构甚至法律伦理的综合理解。希望这份详细的笔记,能为你打开金融数据获取的大门,并在实践中少走一些我曾经走过的弯路。记住,稳健的代码和审慎的态度,是让项目长期运行下去的关键。