☰
Selenium爬取东方财富财报实战:从动态页面解析到数据落地
2026/10/2 15:18:24 网站建设 项目流程

简介:压缩包提供了一套完整的东方财富网上市公司财务报表数据爬取方案,适合Python爬虫学习者、金融数据分析师及量化投资爱好者使用。项目中包含两个爬虫脚本:eastmoney_crawler.py基于Selenium,适合处理动态加载页面但速度较慢;eastmoney_crawler2.py基于Requests,速度提升上百倍,推荐日常使用。配套的CSV示例数据涵盖资产负债表、利润表、现金流量表、业绩报表、业绩预告表、业绩快报表和预约披露时间表等常用财务表格,可直接用于后续分析或建模。资源共12个文件,以Python脚本和CSV数据文件为主,压缩包大小约19.88MB,目前已有356人学习过。通过该资源可以掌握财报类网站爬虫的基本思路、数据解析与存储方法,并能在命令行中自定义年份、季度、报表类型及页码范围,快速生成指定格式的本地数据文件,为量化研究或财务分析提供数据基础。

1. 这份爬东方财富财报的 selenium 项目:能落地多少,值不值得装环境

网络爬虫最怕的不是抓不到数据,而是抓下来的数据没法直接用。这份「爬取东方财富网上市公司财务报表数据」的人工智能项目实践,目标非常明确:用 selenium 把东方财富 F10 页面的资产负债表、利润表、现金流量表自动抓下来,按股票代码和报告期组织成结构化数据,喂给后续的分析和建模。它解决的是手动逐家复制财务数据的重复劳动——一家公司五分钟,一个几百只股票的组合就是几十个小时。

适合三类人:正在做人工智能项目实践或大作业的同学,需要批量财报做因子分析的量化入门者,以及想系统学 selenium 面对动态页面怎么处理 iframe、懒加载和翻页的爬虫新手。zip 解压之后是一个完整的 Python 工程,依赖少、改改股票列表就能跑。下面我从页面结构开始拆,尽量把每一步的选型理由和坑都说清楚。

2. 先摸清 F10 财报页面的结构:iframe、动态渲染与抓取边界

2.1 财报数据在 F10 里怎么组织:iframe、懒加载与动态拼接

拿到这类项目的第一件事,不是写爬虫,而是把目标页面看明白。东方财富网 F10 的财务分析模块,入口是 emweb.securities.eastmoney.com 下面的 PC_HSF10/NewFinanceAnalysis,地址里的 code 参数区分市场和股票:沪市是 SH600519 这种带前缀的写法,深市是 SZ000001。页面本身是一个典型的单页应用——外层框架、内层内容放在 iframe 里,报表表格是 JS 通过 ajax 拉数据后动态拼出来的。

这意味着两件事。第一,直接查看页面源代码是拿不到数据的,源代码里只有一堆 script 标签,真正的表格要等浏览器执行完脚本才会出现在 DOM 里;第二,用 requests 裸请求这个 HTML 地址,返回的也是空壳。所以这类场景反而 selenium 最直接——它驱动一个真实浏览器,让页面完整渲染,再对最终 DOM 做解析,天然绕开了"数据藏在 JS 里"这个坎。

从网络爬虫原理的角度看,爬虫的三段式——下载、解析、存储——在这个项目里对应得非常清晰:selenium 负责下载,拿到渲染完成后的页面;pandas 的 read_html 负责解析,把 table 标签转成结构化数据;CSV 或 Excel 负责存储。理解这个映射关系,后面调参和改代码就有了坐标系,不会出现"报错不知道去哪查"的茫然。

顺便说一句数据源选型。同样能拿到上市公司财报的还有巨潮资讯和交易所官网,巨潮的数据最权威但要下载 PDF 再解析,交易所官网的接口结构相对复杂。东方财富的 F10 页面把三大报表整理成了现成的 HTML 表格,不需要登录就能看,对爬虫项目来说是性价比最高的选择,这也是这份资源选它做数据源的原因。

2.2 selenium 与直接调接口的取舍:速度和可靠性的权衡

实际动手前还有一个选型问题,也是很多人会问的:财报数据明明有 JSON 接口,为什么不直接用 requests 调?我整理了一张对比表,基本覆盖了两种方案的差异:

对比项selenium 方案直接调接口方案
JS 渲染依赖不需要自己处理,浏览器自动执行需要抓包分析 ajax 请求并模拟
单次抓取速度慢,单页 3-8 秒快,毫秒级
页面改版抗性弱,DOM 变了要改定位更弱,接口返回结构变了要重写
反爬暴露面接近真实用户操作容易被识别为脚本流量
调试体验能看到页面渲染过程,出错直观出错只有状态码和空 JSON

我的态度很明确:小批量抓取、以练手和学习人工智能项目实战为目的,selenium 是更划算的选择。它慢,但是每一步都能看见,翻车了好查。接口方案虽然快,但需要花时间抓包分析每一个参数,而且东方财富的接口带有版本更新机制,哪天接口升级,脚本说废就废,维护成本并不低。

关于反爬,这里要讲清楚底线。东方财富的 F10 页面没有强登录墙,但短时间内高频访问会触发验证码或者临时限制。常见做法是每抓一页停 3-5 秒,单批股票控制在几十只的量级,跑完一批歇几分钟再跑下一批。这份资源里的代码默认参数也是按这个逻辑设计的,后面扩规模时不要贪快。

2.3 页面改版怎么快速定位:文本 XPath 优先于 class 定位

F10 页面改版不算频繁,但每次改版,class 名几乎都会变。这里有一个我反复用的定位策略:凡是页签、按钮这种带固定文字的控件,一律用文本 XPath 定位,而不是复制页面上的 class。文本是给人看的,改版时大概率保留;class 是给前端工程师看的,随手就改了。

from selenium.webdriver.common.by import By # 优先用文本定位,抓不到再退到 CSS,data 属性以实际页面为准 try: tab = driver.find_element(By.XPATH, "//*[text()='资产负债表']") except Exception: tab = driver.find_element(By.CSS_SELECTOR, "li[data-type='1']")

逻辑说明:try 分支先按文本找,找不到说明页面结构变了,再走 CSS 兜底。CSS 选择器里的>python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate pip install -r requirements.txt

参数说明:venv 把依赖隔离在项目目录内;requirements.txt 里核心是 selenium、pandas、openpyxl 三个包,如果资源里没带清单,手工装也是一样的命令:pip install selenium pandas openpyxl。pandas 负责表格解析和落盘,openpyxl 是 pandas 写 Excel 文件的后端引擎,只存 CSV 可以不装,但项目里一般会同时输出 xlsx,建议装全。

然后是 ChromeDriver。selenium 4.6 之后的版本自带 Selenium Manager,只要 Chrome 版本不太旧,driver 会自动下载匹配,不用手动维护 chromedriver 路径,这是很多人第一次跑通的关键改进。如果仍然报 session not created 之类的错,先看浏览器版本:chrome://version 里看主版本号,再下载对应大版本的 driver 放进 PATH。

注意:driver 和 Chrome 大版本号不一致,是报错频率最高的入口,表现为浏览器闪退或者卡在创建 session 阶段。

3.2 最小代码:打开 F10、切入资产负债表页签

环境就绪后,先跑最小链路,目标是拿到一家公司的资产负债表。以下代码是这份资源核心逻辑的简化版,去掉日志和重试,只留主干:

import time import pandas as pd from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def create_driver(): options = Options() options.add_argument("--window-size=1920,1080") options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) return webdriver.Chrome(options=options) driver = create_driver() driver.get("https://emweb.securities.eastmoney.com/PC_HSF10/NewFinanceAnalysis/Index?type=web&code=SH600519") # 等待主框架的 iframe 出现,再切进去 wait = WebDriverWait(driver, 20) wait.until(EC.presence_of_element_located((By.TAG_NAME, "iframe"))) driver.switch_to.frame(driver.find_element(By.TAG_NAME, "iframe"))

代码逻辑:先启动一个真实 Chrome,窗口固定成 1920x1080,避免小窗口下页面布局变化导致元素被折叠;get 打开指定股票的财务分析页;等 iframe 出现后切进去,因为财报表格在这个 iframe 内部,不切 frame,后面怎么找都会扑空。

接着切到资产负债表页签并抓表:

# 页签用文本定位,比 class 抗改动 wait.until(EC.element_to_be_clickable((By.XPATH, "//*[text()='资产负债表']"))) driver.find_element(By.XPATH, "//*[text()='资产负债表']").click() time.sleep(2) # 等表格重新渲染 # 表格渲染完成后抓 outerHTML,交给 pandas 解析 table = driver.find_element(By.XPATH, "//table") html = table.get_attribute("outerHTML") df = pd.read_html(html, header=0)[0] print(df.head())

参数说明:time.sleep(2) 是简单粗暴的等待方式,工程里更推荐 WebDriverWait 配合元素可见条件,但财报表格重新渲染的时机不固定,sleep 反而更容易一次打通;XPATH 按文本定位是因为页签文字「资产负债表」基本不变,而 class 每次改版都可能变;read_html 是 pandas 自带的 HTML 表格解析器,直接把 table 转成 DataFrame,比逐行找 tr/td 省事一个数量级。

如果不想每次调试都弹出浏览器窗口,可以在 create_driver 里加一行 options.add_argument("--headless=new")。但这里有一个经验:headless 模式下某些元素的渲染时机和定位行为与有头模式有细微差别,第一次跑通之前建议保持有头模式,能看见页面就不慌,跑通了再切无头去做批处理。

3.3 字段清洗与第一张报表落盘

抓下来的 DataFrame 通常是多级表头:第一列是科目名,其余列是各个报告期的数值。要变成干净的分析表,需要做一次标准化清洗:

df = df.dropna(how="all") df = df.drop_duplicates() df.columns = [str(c).replace("报告期", "").strip() for c in df.columns] df = df.rename(columns={df.columns[0]: "科目"}) # utf-8-sig 编码让 Excel 直接打开不乱码 df.to_csv("SH600519_资产负债表.csv", index=False, encoding="utf-8-sig")

逻辑说明:先把整行全空的行丢掉,这类行是页面里的分隔线或者空行;再清掉列名里的「报告期」字样,让列名只保留日期,方便后面按时间维度合并;第一列统一命名成「科目」。这里有个细节容易踩:to_csv 要用 utf-8-sig 而不是 utf-8,否则 Excel 打开 CSV 时中文科目会乱码。

如果同时要 xlsx 格式,一行代码就够了:

df.to_excel("SH600519_资产负债表.xlsx", index=False)

跑完这一步,当前目录下就有一张贵州茅台的资产负债表。先别急着继续抓,打开东方财富网页同一家公司的报表,用资产总计和负债合计算一下资产负债率,跟 CSV 里的数字对一下,一致再扩大规模。这是所有爬虫项目都应该养成的验证习惯——第一个数对了,后面才有意义。

4. 扩大抓取规模:多股票循环、报告期口径与落盘结构设计

4.1 用股票列表驱动循环:抓全市场的节奏控制

单只股票打通后,下一步是让脚本批量跑。资源里一般会带一个股票列表文件,至少包含 code 和 name 两列,自己维护也可以。核心改动是把单只逻辑包成函数,循环调用,并加上失败记录和随机延时:

import random import time from pathlib import Path def fetch_report(driver, code, report_type="资产负债表"): """抓单只股票的指定报表,返回 DataFrame""" url = f"https://emweb.securities.eastmoney.com/PC_HSF10/NewFinanceAnalysis/Index?type=web&code={code}" driver.get(url) wait.until(EC.presence_of_element_located((By.TAG_NAME, "iframe"))) driver.switch_to.frame(driver.find_element(By.TAG_NAME, "iframe")) wait.until(EC.element_to_be_clickable((By.XPATH, f"//*[text()='{report_type}']"))) driver.find_element(By.XPATH, f"//*[text()='{report_type}']").click() time.sleep(2) table = driver.find_element(By.XPATH, "//table") return pd.read_html(table.get_attribute("outerHTML"), header=0)[0] stocks = ["SH600519", "SZ000001", "SH601318", "SZ300750"] for code in stocks: try: df = fetch_report(driver, code) df = df.dropna(how="all") df.to_csv(f"data/{code}_资产负债表.csv", index=False, encoding="utf-8-sig") print(f"[OK] {code}") except Exception as e: print(f"[FAIL] {code}: {e}") time.sleep(random.uniform(3, 6))

逻辑说明:fetch_report 把第 3 章的链路封装成函数,report_type 用参数传入,三大报表共用一段逻辑;循环里 try/except 是必须的,几百家公司里总会有几家页面加载超时或者元素缺失,一个异常中断整个批次是最亏的;每跑完一家随机停 3-6 秒,模拟人工节奏,减少被限流的概率。

参数怎么调:延时区间建议从 3-6 秒起步,如果频繁触发验证码就加到 8-10 秒。窗口大小 1920x1080 不要轻易缩小,某些报表列数多,窄窗口下表格可能横向滚动,定位虽然仍有效,但截图调试时容易误导判断。

4.2 三大报表的报告期口径:年度、中期与单季数据的差别

东方财富 F10 的财务分析页面上,报告期有「按报告期」和「按年度」两种口径。按报告期会给出每个季末节点的累计数据,按年度只给年末数据。做基本面分析,绝大多数场景用按报告期,因为要的是时间序列上的连续快照。

报表类型F10 页签名关键科目举例分析注重点
资产负债表资产负债表货币资金、资产总计、负债合计偿债能力与杠杆水平
利润表利润表营业总收入、净利润、扣非净利润盈利能力与盈利质量
现金流量表现金流量表三类活动的现金流净额现金流的真实性与持续性

三类报表的字段完全不同,但抓取流程一致,所以 fetch_report 里的 report_type 直接替换页签文本就行。需要特别注意「按报告期/按年度」的切换:有的页面默认展示年度数据,要拿季度序列就得先点「按报告期」。这个切换按钮在不同版本的 F10 页面位置有差异,稳妥做法是判断元素存在再点击:

# 如果页面上存在"按报告期"按钮就点一下,不存在则跳过 if driver.find_elements(By.XPATH, "//*[text()='按报告期']"): driver.find_element(By.XPATH, "//*[text()='按报告期']").click()

这个写法的好处是安全的:元素存在才点,不存在直接跳过,不会因为页面版本差异直接报 NoSuchElementException,批量跑的时候少了很多无谓的中断。

4.3 落盘结构:按公司分文件还是按报表分文件

批量跑起来之后,文件怎么组织会直接影响后续使用体验。两种常见方案各有适用场景:按股票分目录,适合做单公司的时间序列追踪;按报表类型分目录,适合做全市场的截面分析。我一般推荐混合式:data/报表类型/股票代码.csv,兼顾两种分析路径。

from pathlib import Path report_type = "资产负债表" out_dir = Path("data") / report_type out_dir.mkdir(parents=True, exist_ok=True) for code in stocks: df = fetch_report(driver, code, report_type) df.to_csv(out_dir / f"{code}.csv", index=False, encoding="utf-8-sig")

最终目录结构类似 data/资产负债表/SH600519.csv、data/利润表/SZ000001.csv。如果之后要喂给模型,可以写一个 merge 脚本,把同一报表类型的所有文件纵向拼接,并加一列股票代码:

import glob import pandas as pd frames = [] for fp in glob.glob("data/资产负债表/*.csv"): t = pd.read_csv(fp) t.insert(0, "code", fp.split("/")[-1].replace(".csv", "")) frames.append(t) merged = pd.concat(frames, ignore_index=True) merged.to_csv("all_balance_sheet.csv", index=False, encoding="utf-8-sig")

逻辑说明:insert 把股票代码放到第一列,concat 按行拼接,最后得到一张「公司 + 科目 + 报告期」的长表,这是量化分析里最常见的面板数据雏形。到这一步,抓取规模的问题基本解决,接下来进入排错环节。

5. 避坑与常见问题排查:爬东方财富财报的五个翻车现场

这章是血泪经验汇总。下面五条问题,我在跑类似项目时基本都碰到过,每一条按「现象 → 原因 → 解决」展开,你可以直接对照排查。

5.1 现象:浏览器闪退,报 session not created

原因:Chrome 自动升级了,ChromeDriver 还停在旧版本,版本号不匹配。selenium 4.6 以下的版本这个坑特别深,因为需要手动维护 driver 路径。

解决:先看 chrome://version 里的主版本号,去下载同大版本的 driver 覆盖。如果用的 selenium 4.6 以上,把 Chrome 和 selenium 都升到较新版本,让 Selenium Manager 自动匹配。另外提醒一句:如果资源包里带了 chromedriver.exe,先确认它对应哪个 Chrome 版本,再决定是否替换,不要盲目用旧驱动。报错信息里出现 "This version of ChromeDriver only supports Chrome version" 时,基本就锁定是这个问题,不用怀疑别的地方。

5.2 现象:switch_to.frame 之后,find_element 一直抛 NoSuchElementException

原因:iframe 还没渲染完成就切进去了,或者页面有多个 iframe,切错了。F10 页面外层框架渲染很快,内层财报 iframe 是异步加载的,两者之间有明显时间差,过早切入自然什么都找不到。

解决:切 frame 之前先等 iframe 出现在 DOM 里,并且用 frame_to_be_available_and_switch_to_it 这个专用等待条件,一步到位:

wait = WebDriverWait(driver, 20) wait.until(EC.frame_to_be_available_and_switch_to_it( driver.find_element(By.TAG_NAME, "iframe") ))

另外,如果页面里 iframe 不止一个,先打印数量确认特征:

frames = driver.find_elements(By.TAG_NAME, "iframe") print(len(frames), [f.get_attribute("src") for f in frames])

用 src 特征区分哪个才是财报内容的 iframe,避免切到顶栏导航或者广告位。

5.3 现象:read_html 解析出来只有表头,没有数据行

原因:表格里的数据行是懒加载渲染的,页面操作太快,JS 还没来得及把行数据拼出来,outerHTML 就被抓走了。这是动态页面最常见的竞态问题。

解决:点击页签后强制等一下,或者更稳健地设置等待条件——等到表格里出现已知科目文本再抓。比如「货币资金」几乎每家公司的资产负债表都有,就可以作为渲染完成的信号:

wait.until(EC.presence_of_element_located((By.XPATH, "//*[text()='货币资金']")))

调试时如果不确定是渲染慢还是定位错,抓完之后立刻执行 driver.save_screenshot("debug.png"),看截图里表格区域有没有数据。有数据就是时序问题,没数据就是定位问题,一张截图能省半小时瞎猜。

5.4 现象:抓了五十家之后开始弹验证码,后续全部失败

原因:请求频率过高,触发了站点的频率限制。单进程连续跑,IP 维度很容易被标记。东方财富的治理策略不算激进,但连续高频访问一定会触发。

解决:把随机延时从 3-6 秒拉到 8-10 秒;批次之间停 5 分钟再继续;股票列表拆成多个小批次错峰执行。还有一个技巧:触发验证之后,把 driver 整个关掉重启,清理旧会话的 cookie,再重新打开页面,很多时候就恢复了。原则是「慢就是快」,宁可多花半小时把时间拉长,也不要前功尽弃重跑一遍。

5.5 现象:CSV 打开中文乱码,或者科目列和数值列错位

原因:中文乱码是编码问题,CSV 用 utf-8 写入,Excel 默认按 GBK 打开,两边对不上;错位则多半是表头有多级结构,read_html 的 header 参数没有对齐,pandas 把合并单元格拆开了。

解决:写入统一用 utf-8-sig,这是 Windows 生态下最省心的方案。解析前先打印 df.shape 和 df.head(),看清楚表头结构再决定 header 传 0 还是 1。另外,数值列有时会被 pandas 读成 object 类型,需要统一做一次类型转换:

# 从第二列开始尝试转数值,转不了的置为 NaN for col in df.columns[1:]: df[col] = pd.to_numeric(df[col], errors="coerce")

转换后留意出现的 NaN,那些位置往往是页面里显示为「--」的空值,属于正常情况,不要当成抓取失败。

6. 进阶:把脚本改造成定时更新的财报数据管线

批量抓完只是第一步,真正的使用场景是定期更新——季报、中报、年报发布后增量补抓,而不是每次全量重跑。常见做法是用系统的定时任务调用主脚本,或者在自己项目里用 APScheduler 做调度。工程实践里,我更推荐先把抓取脚本改成支持命令行参数:

python crawl.py --type 资产负债表 --batch 1 --sleep-min 3 --sleep-max 6

参数解析用 argparse,--batch 支持分片,这样定时任务可以把全市场拆成多个批次错峰执行。增量逻辑很直接:每次跑之前扫描一遍目标目录里已有的 CSV 文件名,只抓缺失的股票代码;报告期维度上,固定抓最近几期即可,不需要每次都拉全历史,省时间也降低对数据源的打扰。

验证环节我养成了一个习惯:每次跑完,随机抽三家公司,打开东方财富网页,把最新一期的资产总计和净利润跟 CSV 里的值做人工对比。这个动作看起来笨,但能一次性暴露三类问题——页签点错导致抓成别的报表、报告期口径不对导致数值对不上、列错位导致科目张冠李戴。从那次抓出整列错位数据、还直接拿去算了指标之后,我再没跳过这个抽检。

定时任务本身不复杂:Windows 上用任务计划程序调用 venv 里的 python.exe 跑 crawl.py,Linux 上写 cron 表达式指定在季报发布窗口后的日期执行。数据落地之后,再接到下一步分析脚本里,这套爬虫才真正完成了从「页面数据」到「可分析数据集」的闭环。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询