☰
Selenium与Requests爬取东方财富财务数据:接口与动态页面分工实践
2026/9/26 21:46:43 网站建设 项目流程

简介:面向金融数据分析与网络爬虫学习者,这份资源提供了一个基于东方财富网的上市公司财务数据采集项目。项目包含两套Python实现方案:方案A基于Selenium自动化框架,适合需模拟浏览器点击、处理动态加载页面的场景,但采集速率相对较低;方案B基于Requests库直接请求HTTP接口,数据处理效率提升约两个数量级,被定为主要实施方案。资源包共19个文件,核心为2个Python脚本和10个CSV结果文件,另含备份、许可证及说明文档等,压缩包整体约37.96MB。脚本支持灵活配置会计年度(2018—2023)、财季标识、报表分类(资产负债表/利润表/现金流量表)与页码区间,运行后自动输出CSV结构化数据并保存至预设目录,可直接用于量化分析、财务教学或企业财报研究。目前已有47人学习下载,适合有一定Python基础、希望快速获取上市公司结构化财报数据的开发者参考。

1. 用Selenium加Requests抓东方财富网财务数据:先想清楚接口在哪一层

老板丢过来一句话:"把沪深两市上市公司近三年的资产负债表、利润表、现金流量表全爬下来,月底前入库。" 打开东方财富网一看,每家公司几十张页面,光点鼠标就能点到手软。这时候基于Selenium与Requests的东方财富网上市公司财务报表数据爬取实践就成了大多数人的第一反应——浏览器能看的,Selenium就能自动化;接口能拿的,Requests一条请求就够。但真正动手你会发现,这个标题里的分工很微妙:东财的数据站点有七八成报表数据藏在JSON接口里,Requests几分钟就能拉完;剩下那些异步渲染的F10明细表和需要登录态的页面,才轮到Selenium出场。先拆清楚数据对象再决定用哪个工具,反爬问题和抓取效率就解决了一半。适合谁?做量化选股、行研复核、财务异常筛查的工程师和数据分析师,这篇把从选型到排错的完整路径讲完,照着能跑通。

2. 摸清数据源边界:东方财富网的三个数据层与两种拿法

2.1 三层数据结构:行情层、数据中心层、F10详情层

东方财富网面向普通用户的页面很多,但数据逻辑上可以切成三层。第一层是行情层,以push2系列接口为主,提供实时价格、市盈率、市净率这类交易衍生指标,数据粒度细、更新快,但对财务报表爬取来说只是辅助信息。第二层是数据中心层,对应的是 datacenter-web 这类接口,它是东财数据团队整理过的结构化输出,报表数据、业绩预告、主营构成、机构调研都在这一层,JSON格式,字段命名规范,是爬财务报表的主战场。第三层是F10详情层,也就是个股页面上"财务分析"那一整套页面,展示上最接近阅读习惯,但结构复杂,有页签切换、有异步加载,甚至有的区块包在iframe里,是Selenium发挥作用的地方。

选型理由很简单:能用Requests拿JSON,绝不开浏览器。JSON接口带宽消耗小、解析稳定、不需要等待渲染,单位时间能抓的股票数量比Selenium高一个数量级。我一般会先打开开发者工具、切到Network面板,在财务页面里翻几页,把返回JSON的关键接口找出来,能走接口的走接口。Selenium的价值在于兜底:那些数据不在接口里、必须靠浏览器执行脚本才能渲染出来的页面,以及需要维持登录态才能访问的受保护数据。这个判断顺序如果反了,后面所有努力都会变成在错误的地方做优化。

2.2 什么时候必须上Selenium:动态表格、页签切换与iframe

判断一个财务页面是否需要Selenium,我有个简单的测试方法:用Requests直接请求页面URL,看返回的HTML里有没有你要的表格数据。资产负债表如果出现在原始HTML中,直接解析就行;如果只在页面源代码里看到一个空的table容器、数据靠后面的JS填充,那就得上Selenium。典型场景是F10页面的财务分析模块,报告期切年、切季、切半年报,每一次切换都触发一次异步请求,表格区域整体重绘,Requests拿到的只是一层空壳。

另一个必须用Selenium的场景是带登录态的数据。有些深度财务指标或导出功能需要登录后可见,这时候Selenium的作用不是渲染,而是帮你走一遍登录流程、把Cookie取出来。取到之后的请求依然可以交回给Requests,用带着Cookie的Session去访问接口,速度和稳定性都更好。这种混合用法是爬东财数据最常见的架构——Selenium负责拿入场券,Requests负责大规模搬运。不需要整个项目都浸泡在浏览器自动化里,那是新手最容易犯的过度设计。

3. 用Requests拿数据中心JSON:从单表请求到三表串联

3.1 资产负债表接口的最小请求代码

东方财富网的数据中心接口遵循一套统一的查询风格,路径一般是 datacenter-web.eastmoney.com/api/data/v1/get,通过reportName指定报表类型,通过columns指定要返回的字段。以资产负债表为例,核心参数里filter用股票代码过滤,sortColumns指定报告期排序,pageNumber和pageSize控制分页。下面这段代码是一个可运行的最小闭环,抓取特定股票最近一份资产负债表,并解析出关键字段。

import requests import pandas as pd from datetime import datetime # 数据中心统一入口,用 reportName 区分报表 url = "https://datacenter-web.eastmoney.com/api/data/v1/get" params = { "reportName": "RPT_LICO_FN_CPZ", # 资产负债表 "columns": "ALL", # 拿全部字段,先探路 "filter": '(SECURITY_CODE="600519")', # 贵州茅台 "sortColumns": "REPORT_DATE", # 按报告期排序 "sortTypes": "-1", # 降序,最新报告期在最前 "pageNumber": "1", "pageSize": "1", # 先拿一条验证结构 } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Referer": "https://data.eastmoney.com/", } resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() rows = data["result"]["data"] # 时间戳字段转日期,方便核对报告期 for row in rows: row["REPORT_DATE"] = datetime.fromtimestamp(row["REPORT_DATE"] / 1000).strftime("%Y-%m-%d") df = pd.DataFrame(rows) print(df[["SECURITY_CODE", "SECURITY_NAME_ABBR", "REPORT_DATE", "TOTAL_ASSETS", "TOTAL_LIABILITIES", "TOTAL_EQUITY"]].to_string())

这段代码里值得注意的有三处。第一是filter的写法,字段名用引号包裹、值用双引号,整体作为一个字符串传进去,多条件用逗号连接。第二是REPORT_DATE是毫秒时间戳,直接丢给DataFrame会被当成数字,先转换再核对。第三是columns=ALL在探路阶段很有用,你可以把返回的JSON完整打印出来,看清楚字段有哪些、命名规律是什么,后面精确指定字段时就不会翻车。

提示:东方财富网的接口字段命名整体接近直译,TOTAL_ASSETS是总资产,TOTAL_LIABILITIES是总负债,TOTAL_EQUITY是所有者权益合计。实际字段以返回的JSON为准,不同报表的命名风格有差异,别靠着记忆写死。

3.2 利润表与现金流量表:三张报表按股票和报告期对齐

资产负债表只是第一张,利润表和现金流量表的请求套路完全一致,变的只是reportName和字段名。利润表是RPT_LICO_FN_INCOME,现金流量表是RPT_LICO_FN_CASHFLOW。三条请求打出去,拿回三个DataFrame,但这时的数据还不能直接入库——需要按"股票代码 + 报告期"对齐。同一家公司在同一份财报发布日期下,三张报表具有相同的报告期,对齐键就是这两个字段。

实际操作里我习惯把请求封装成一个通用函数,参数只要传reportName、filter、columns,返回DataFrame。然后循环调用三次,分别拼股票代码前缀,最后用merge或concat对齐。对齐不光是索引匹配,还要留意金额单位。东财接口大部分金额字段以元为单位,但有些衍生指标是万元甚至亿元,如果三张表混在一起入库,后面算毛利率、资产负债率时会出现量级差出四位数的血泪事故。判断方法很笨但可靠:把贵州茅台的总资产字段拉到网页上人工核对一遍,数字对上了再大规模跑。

def fetch_report(report_name: str, stock_code: str, page_size: int = 20) -> pd.DataFrame: """通用财务报表抓取函数""" url = "https://datacenter-web.eastmoney.com/api/data/v1/get" params = { "reportName": report_name, "columns": "ALL", "filter": f'(SECURITY_CODE="{stock_code}")', "sortColumns": "REPORT_DATE", "sortTypes": "-1", "pageNumber": "1", "pageSize": str(page_size), } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://data.eastmoney.com/", } resp = requests.get(url, params=params, headers=headers, timeout=15) payload = resp.json() if payload.get("result") is None: return pd.DataFrame() df = pd.DataFrame(payload["result"]["data"]) if df.empty: return df df["REPORT_DATE"] = pd.to_datetime(df["REPORT_DATE"], unit="ms") return df # 三张报表各取最近5期,按股票代码和报告期对齐 stock = "600519" balance = fetch_report("RPT_LICO_FN_CPZ", stock, page_size=5) income = fetch_report("RPT_LICO_FN_INCOME", stock, page_size=5) cashflow = fetch_report("RPT_LICO_FN_CASHFLOW", stock, page_size=5) merged = balance.merge( income, on=["SECURITY_CODE", "REPORT_DATE"], suffixes=("_BAL", "_INC"), ).merge( cashflow, on=["SECURITY_CODE", "REPORT_DATE"], suffixes=("_INC", "_CAS"), )

这个函数能跑通的前提是pageSize不要贪大。数据中心接口单页返回超过两百条时,接口响应时间会明显变长,偶发超时。我一般控制在50条以内,宁可多循环几次,也不要一次拉全量然后等超时重试。对齐之后,建议立刻做一次完整性校验:三张表行数是否一致、报告期集合是否相同、关键字段是否有空值。只有这三关都过了,数据才配进数据库。

4. 用Selenium补齐F10动态表格:渲染、页签与iframe的兜底方案

4.1 启动参数配置:关闭自动化特征与资源占用

数据中心接口覆盖了大部分报表数据,但总有几家公司的特殊字段只在F10页面里出现,比如审计意见类型、研发投入明细、分红送配详情。这些页面是典型的JS动态渲染,Requests拿到的是空壳。这时才轮到Selenium出场。许多人对Selenium又爱又恨,其实是没配置好启动参数,导致浏览器被识别为自动化控制、页面加载慢、内存占用高。下面是经过反复验证的最小配置。

from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service options = Options() # 去掉"Chrome正在受到自动软件控制"的提示条,降低被检测概率 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) # 关闭图片加载,F10页面图片多但对数据抓取无用 prefs = {"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs", prefs) # 必要配置,很多F10表格在这个窗口宽度下布局最稳定 options.add_argument("--window-size=1440,900") # 隐藏"自动软件"标记的CDP命令,不少站点会检测这个 options.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome(service=Service(), options=options) driver.set_page_load_timeout(30) driver.implicitly_wait(10)

这套参数解决的问题很具体。excludeSwitches去掉了浏览器顶部的提示条,useAutomationExtension配合CDP命令能减少webdriver特征被页面脚本读到的概率。关闭图片是个很实用的提速点,F10页面一张图表几百KB,爬几百家公司光是下载图片就多耗十几分钟,而数据本身都在DOM表格里,图片关掉不影响。窗口大小固定也很重要,有些前端框架在窄窗口下会改变表格渲染方式,把窗口固定成预设尺寸能减少这类不确定性。

注意:Selenium的防检测只是提高成功率,不是绝对保障。如果页面出现了验证码或滑块,别硬刚,先退出去检查请求频率。

4.2 从进入财务分析页到拿到整张表:显式等待与iframe切换

用Selenium爬F10财务分析页,第一步是定位到正确的URL。传统F10页面的URL格式比较稳定,股票代码拼进去就能访问。但真正进了页面才是考验的开始。财务分析模块里有资产负债表、利润表、现金流量表三个大页签,每个页签下面还有报告期选择下拉框。每一次切换页签或切换报告期,表格区域都会异步刷新。如果代码里没有等待逻辑,Selenium会在表格还没渲染出来时就去读DOM,返回空数据,这是最常见的翻车现场。

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 进入F10财务分析页 stock_code = "600519" url = f"https://emweb.securities.eastmoney.com/PC_HSF10/NewFinanceAnalysis/Index?type=web&code={stock_code}" driver.get(url) # 等待页签区域出现,确认页面主框架加载完成 WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.CSS_SELECTOR, ".mainTable")) ) # 切到"资产负债表"页签,按钮文本定位,不用XPath路径写死位置 tabs = driver.find_elements(By.CSS_SELECTOR, ".tab li") for tab in tabs: if "资产负债" in tab.text: tab.click() break # 页签切换后表格是异步加载的,必须等新表格出现 table = WebDriverWait(driver, 15).until( EC.visibility_of_element_located((By.CSS_SELECTOR, "#table1")) ) # 读取表头 thead_rows = table.find_elements(By.CSS_SELECTOR, "thead tr") headers = [th.text.strip() for th in thead_rows[-1].find_elements(By.TAG_NAME, "th")] # 读取表体,注意有些单元格带span标签,需要取text属性 data_rows = [] tbody = table.find_element(By.CSS_SELECTOR, "tbody") for tr in tbody.find_elements(By.CSS_SELECTOR, "tr"): row = [td.text.strip() for td in tr.find_elements(By.TAG_NAME, "td")] data_rows.append(row) # 组装成列表,每行是一个报表科目 report = [dict(zip(headers, row)) for row in data_rows] print(report[:5])

这里关键的坑有三个。第一是页签点击后必须用显式等待,visibility_of_element_located比implicitly_wait更可靠,因为它是轮询等待而后者是固定超时。第二是iframe问题,新版F10页面大部分表格不在iframe里,但老版本页面有嵌套iframe,如果driver.find_element找不到元素,优先检查是否用了driver.switch_to.frame()。第三是表头结构,东财F10表格的表头往往有两行,一行是分类、一行是具体科目,取最后一行作为列名才完整,否则会出现科目错位。

F10页面的表格一次最多展示最近五年或最近十几期,超过这个范围的数据还是得回到数据中心接口去取。这也再次印证了架构判断:Selenium在这个项目里是补缺口的,不是主力。数据量大、要覆盖全市场的任务,交给Requests;个别字段、个别页面的补漏,用Selenium精准打击。混用得当,整个项目的耗时和反爬风险都能控制在合理范围。

5. 避坑:东方财富网财务数据爬取的五个真实踩坑点

5.1 429 too many requests:限流与退避重试

现象:脚本连续抓了十几个股票后,突然报出类似"exceeded retry limit, last status: 429 too many requests"的错误,再过一会儿连正常访问首页都变慢。原因:数据中心接口对单个IP的QPS有限制,并发一高或循环太快就触发限流。解决:在每次请求之间加入随机延时,并对429状态码做指数退避重试,而不是报错就停。

import time import random def robust_get(url, params, headers, max_retries=5): for attempt in range(max_retries): resp = requests.get(url, params=params, headers=headers, timeout=15) if resp.status_code == 200: return resp if resp.status_code == 429: # 退避时间随尝试次数递增,抖动随机值避免所有线程同时重试 wait = (2 ** attempt) + random.uniform(0.5, 1.5) print(f"触发限流,等待 {wait:.2f}s 后重试") time.sleep(wait) else: resp.raise_for_status() raise RuntimeError("多次重试后仍然失败")

延时取值我常用0.5到1.5秒随机,整体抓取速度大约每秒一个请求,对全市场近五千家公司的资产负债表来说,单表耗时大约一个多小时。看起来不慢,但你要是用Selenium逐个页面点,这个时间会放大到十几小时。不要把延时写成固定值,固定间隔容易被识别成脚本特征。

5.2 Cookie与User-Agent不一致导致字段为空

现象:用Requests访问接口时,返回的JSON里result不为空,但部分字段的值是null。原因:接口校验了请求来源,Referer缺失或不合法时,服务端会选择性地不返回敏感字段。解决:请求头里带上Referer为https://data.eastmoney.com/,且保持User-Agent与浏览器一致。这是最简单但最容易被忽略的一步,有些字段就像挤牙膏,头没带对就藏着不给。

5.3 金额单位错位:元、万元、亿元混用

现象:同一张表里总资产显示为万亿级数字,但利息支出只有几百万,看起来比例失衡。原因:不同接口、不同字段使用了不同的金额单位,个别衍生指标按万元计算。解决:每个字段入库前都跟网页端核对一次,重点核对大额科目。不要相信字段名的后缀约定,要相信眼睛核过的数字。我在这个坑里翻过车,把某公司的净利润当成元入库,导致后续算出来的市盈率差了四位。

5.4 同一报告期多条记录:预告、快报与正式财报并存

现象:按"股票代码+报告期"去重后发现,某公司2023年年报有两条记录,一条是业绩快报,一条是正式财报。原因:数据中心会保留同一报告期的多个公告版本,字段值不同。解决:入库时增加公告类型字段,并按公告日期取最新一条。常见做法是优先取正式财报,其次才是快报和预告。如果不区分,后面做同比分析时会把快报数据和正式数据混在一起算,结论自然失真。

5.5 Selenium等待超时:懒加载与滚动加载

现象:F10页面的表格明明存在,但WebDriverWait等了二十秒还是找不到元素。原因:部分区块采用了懒加载策略,只有在页面滚动到底部时才触发数据加载。解决:在等待之前,先执行JavaScript把页面滚动到底部,再回到表格位置,让懒加载有触发机会。代码就一行:driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")。如果还不出来,检查是否被反爬识别,先手动打开页面看看能否正常显示。

6. 从爬下来的报表到可信数据库:校验技巧与入库习惯

数据抓下来只是第一步,它能不能用于决策,取决于有没有经过校验。我有一套固定的验证流程:先随机抽三家公司,覆盖大盘股、中盘股、ST股,每家公司手工打开东财网页,把总资产、净利润、经营现金流三个字段跟抓下来的数据逐条核对。三家公司全对,才认为这批数据的口径是统一的。如果有一家对不上,先别急着排查代码,而是去确认是否拿错了报告期版本——这比调试代码更常见。

全市场跑批的耗时要做一次预估算:沪深两市约五千家公司,每家三张表,按每秒一个请求算,大约四个小时能跑完一轮。如果需要季度更新,这个速度完全跟得上。建议把抓取脚本和校验脚本分开,抓取脚本只负责落库,校验脚本独立运行,输出不一致记录。这样每次跑批后能自动生成一份质量报告,而不是全靠人工抽查。入库时我有个仪式感很强的习惯:每张表写完后立刻执行SELECT COUNT(*)验证行数,再执行一次主键冲突检查。前者保证没丢数据,后者保证可重复抓取。这个习惯救过我很多次——某次调整参数后脚本少翻了一页,正是这个检查在第一时间发现了问题。

另外一个进阶用法是把请求频率控制做成可配置的,参数写在配置文件里而不是埋在代码中,根据当前是首次全量还是增量更新,切换不同的限速策略。增量更新可以稍微激进一点,全量更新则保守为妙。把Redis或SQLite当作简单的进度记录器,每抓完一只股票就写入状态,断点续跑时能跳过已完成的部分。这样爬取任务可以拆成多轮执行,不用担心中途挂了要全部重来。

用Selenium与Requests混搭爬东方财富网的财务数据,本质是给不同数据形态选最合适的工具:JSON接口能解决的绝不开浏览器,浏览器渲染才能拿到的再让Selenium上。我如今接任何数据爬取需求,都先问一句"这数据在接口里有没有",然后才考虑自动化方案。这套方法论比数据本身更值钱。希望帮到你。

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

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

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

立即咨询