
做金融数据相关的事情最烦的不是分析模型跑不出来而是数据本身拿不到手。我平时会研究上市公司的分红送转方案以前都是上交易所公告栏一份份翻PDF效率低不说不同券商的披露格式还不统一想整理成结构化表格得费好大功夫。后来我注意到东方财富网有个数据中心页面把分红送转数据整理成了标准表格而且接口返回的是干净的JSON这可比解析PDF省事多了。于是就有了这个小项目写一个Python爬虫把东方财富网的分红数据拉下来存成结构化文件再顺手做一波数据分析。这篇文就完整记录我从接口分析、代码实现到结果解读的全过程里面包含了完整的可运行代码、参数设计思路和我在实操中踩过的坑适合对爬虫感兴趣、或者想搞量化分红策略的朋友参考。1. 项目概述与核心需求拆解1.1 为什么选择东方财富网作为数据源做分红数据爬取首先要解决的是数据源问题。市面上能拿到A股分红送转信息的渠道不少但各有各的痛点。巨潮资讯网的数据最权威可它的页面结构经常变动公告多以PDF形式呈现机器解析成本很高同花顺的接口数据也挺全但需要登录态而且反爬策略比东方财富网严格得多。综合对比下来东方财富网数据中心的分红送转页面是最合适的选择理由有三个。第一数据已经高度结构化。东方财富网数据中心把每一条分红记录都整理成了规范表格字段包括股票代码、股票简称、送转股份、分红方案、股权登记日、除权除息日等这些在网页源码里都能直接找到不需要去解析PDF或者OCR识别研发成本低很多。第二接口设计相对友好。东方财富的数据接口返回的是JSON格式只要找到规律就能直接批量拉取不需要处理复杂的加密参数。这对个人开发者来说非常友好意味着不需要逆向JS或者模拟浏览器指纹用requests库就能稳定拿到数据。第三数据覆盖面足够全。这个接口涵盖了沪深京三市所有A股的分红送转记录既有历史数据也有最新的分红预案做全市场维度的分析时样本量是够的。当然权威性这个短板必须承认。东方财富的分红数据本质上是二手数据可能和交易所原始公告存在个别字段的偏差所以如果要拿去做严肃的投资决策建议还是用巨潮原始公告做交叉验证。但如果目标是做数据统计、策略回测或者日常研究东方财富的数据已经完全够用了。1.2 项目要解决的三个真实痛点这个项目我一开始没想做成大而全的东西就是冲着解决三个具体痛点去的。第一个痛点是手工收集分红信息太慢。以前我每个月要看几十条分红公告每条都要打开、阅读、摘录到Excel里一次至少浪费一小时而且容易看漏。现在把这个过程用爬虫自动化几分钟就能拉全市场数据省下来时间可以干更有价值的事情。第二个痛点是数据格式不统一。不同券商的软件里分红数据的展示逻辑不太一样有的按权益登记日排序有的按除权除息日排序字段命名也千奇百怪。我自己做数据建模时需要统一字段、统一单位与其每次手动调整格式不如在爬虫阶段就把数据清洗成规范结构一劳永逸。第三个痛点是缺少历史积累。分析分红能力不能只看某一年要看三五年的连续记录。而这个数据是跨年累积的今天有今天的记录上个月的数据还需要自己翻历史所以我需要把数据每次都存下来慢慢地形成一个自己的分红数据库。有了历史数据后后续分析连续分红、股息率变化、分红率趋势时才有据可查。至于这个项目适合谁我觉得三类人比较匹配。一是刚入门爬虫的Python学习者这个项目的反爬难度适中代码量也不大是很经典的JSON接口实战案例二是做股票投资研究的人可以把数据用在分红策略或者基本面筛选上三是做金融数据分析的从业者可以拿这套方法扩展去抓其他数据中心页面比如基金、债券、期货的数据思路完全通用。2. 技术选型与爬虫方案设计2.1 技术栈选择及理由技术栈我用的是Python这是金融数据抓取最主流的语言没有之一。核心依赖只有三个库requests负责网络请求pandas负责数据处理json负责解析接口返回内容。这三个库都是Python生态里的标配不需要额外花时间学习复杂框架。很多朋友一上来就想着用Scrapy框架其实对于单个页面的数据抓取Scrapy属于杀鸡用牛刀反而会增加学习成本。requests库不用多说Python爬虫的标配。它的会话管理、超时控制、headers设置都做得很成熟可以直接处理Cookie和Referer日常爬虫任务用它最稳。pandas是数据分析的核心接口返回的是JSON嵌套结构用pandas把它转成DataFrame后字段重命名、去重、排序、筛选这些操作就变得非常简单而且最终导出Excel或者CSV都很方便。代码里还用到了time模块来控制爬取频率这是爬虫的必备素养。东方财富的接口虽然不强制登录但高频率请求依旧可能触发风控。我实际测试下来每次请求间隔0.1到0.5秒是比较稳妥的节奏单次全量抓取耗时也就一两分钟完全在可接受范围内。可能有人会问为什么不直接用Selenium或者Playwright这种浏览器自动化工具原因很简单这个接口是纯JSON返回不需要执行JavaScript渲染用浏览器自动化反而是性能浪费还会因为启动浏览器导致请求特征特别明显更容易被封。记住一个原则能直接请求接口的绝不上浏览器。2.2 接口发现与请求参数分析拿到东方财富分红数据的第一步是找到数据接口的真实地址。这一步我用的是浏览器开发者工具F12这是每个爬虫工程师的基本功。打开东方财富网数据中心的分红送转页面按F12切到Network选项卡在Fetch/XHR分类下刷新页面就能看到数据请求的完整过程。页面上看到的数据表格其实是从一个名为https://datacenter-web.eastmoney.com/api/data/v1/get的接口返回的请求方式是GET参数是一大串。关键参数包括sortColumns用于排序sortTypes控制升降序pageSize控制每页返回多少条数据pageNumber控制当前页码reportName指明数据报表类型columns则是需要返回的字段列表。这里有个细节值得注意接口请求URL里带有一个callbackjQuery参数这是典型的JSONP跨域方案。东方财富的页面通过JSONP方式直接调用接口绕过浏览器的跨域限制。我们在Python里直接去掉这个callback参数就行否则拿到的是一个带前缀的JavaScript代码块解析起来麻烦。直接请求不带callback的版本获得的就是纯净JSON。翻页逻辑也很直观。pageNumber从1开始递增pageSize可以设置到500单次请求能拿到500条分红记录。全市场A股的分红历史记录加起来大概两三万条也就是说循环几十次就能全量拉完效率非常高。构造请求时还需要带上Referer头指向分红送转页面本身这一步主要是为了降低被风控系统识别为脚本的概率属于基础防护。2.3 爬取节奏与合规策略爬虫写得好不好不在于代码多花哨而在于稳不稳。我给自己定了几条铁律也建议每个做数据采集的朋友都遵守。首先是频率控制即便接口响应很快我仍然在每次请求后sleep 0.2秒左右这不光是为了防止被封更是为了不给目标服务器制造额外压力说白了就是个同理心问题。其次是UA伪装。requests默认的python-requests/x.x.x用户代理在服务端日志里非常扎眼。我直接借用了Chrome浏览器的UA字符串模拟真实浏览器访问虽然不能完全保证不被识别但至少能过滤掉大量以默认UA为特征的简单爬虫。第三是设置合理的重试机制。网络请求不可能是100%稳定的偶尔会出现超时或者响应异常所以我给请求函数加了重试逻辑连续失败3次才放弃当前页并对失败的页码做记录方便后续单独补抓。很多人爬数据失败一次就全盘崩溃其实有了重试和断点续传的逻辑稳定性会有一个质的提升。还有个容易被忽略的问题数据落地之后要保留原始抓取时间字段。我每次抓取都会在文件里写入采集时间戳这样后续如果发现数据有出入可以回溯是抓取时数据源就没更新还是自己处理逻辑出了问题排查效率会高很多。3. 核心代码实现与参数解读3.1 请求构造与翻页逻辑先看最核心的请求部分。根据上一步接口分析的结果我封装了一个fetch_page函数专门负责请求单页分红数据。代码里重点关注params参数的构造reportName和columns这两个参数非常关键前者决定查哪个报表后者决定返回哪些字段字段名可以直接从浏览器Network面板里的响应数据中复制出来。import requests import json import pandas as pd import time 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/yjfp/ } BASE_URL https://datacenter-web.eastmoney.com/api/data/v1/get def fetch_page(page_number, page_size500): params { reportName: RPT_SHAREBONUS_DET, columns: ALL, filter: (SECURITY_TYPE\A股\), pageNumber: page_number, pageSize: page_size, sortTypes: 1, sortColumns: REPORT_DATE, source: WEB, client: WEB, } resp requests.get(BASE_URL, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() data resp.json() if data.get(code) ! 0: raise ValueError(f接口返回异常: {data.get(message)}) return data.get(result, {}).get(data, []), data.get(result, {}).get(pages, 0)这个函数有两个返回值data是当前页的分红记录列表pages是总页数。有了总页数就能写全量抓取的循环逻辑了这个设计比不知道总页数、死循环撞大运要健壮得多。REPORT_DATE是公告日期字段用它排序能保证拿到的是每个公司最新的分红方案排在前面方便后续做时效性判断。3.2 字段映射与清洗逻辑接口返回的JSON字段是英文命名比如SECURITY_CODE代表股票代码SECURITY_NAME_ABBR是股票简称DIVIDEND_RATIO是每股派息数元RATIO_PRESENT是每股送转股数量股EX_DIVIDEND_DATE是除权除息日RECORD_DATE是股权登记日。直接拿这些英文字段做数据处理会很别扭所以我做了一个字段映射表统一改成中文列名。清洗逻辑里有几个关键点需要注意。第一DIVIDEND_RATIO和RATIO_PRESENT返回的是字符串或者数字但实际中有些公司没有派息只有送转此时字段可能为空字符串直接转float会报错需要先做个安全转换。第二分红方案字段PLAN_NOTICE里会有一句“10派5元”之类的完整描述这个字段虽然不参与数值计算但对人工核验很有价值建议保留下来。还有一个容易被坑的点接口返回的日期字段是时间戳格式比如EX_DIVIDEND_DATE的值是一串毫秒级数字直接读没有可读性。我处理的时候统一用pd.to_datetime做了一次日期转换这样表格里的日期就是正常的YYYY-MM-DD格式无论是打印还是导出都更直观。def clean_frame(raw_list): if not raw_list: return pd.DataFrame() df pd.DataFrame(raw_list) df df.rename(columns{ SECURITY_CODE: 股票代码, SECURITY_NAME_ABBR: 股票简称, DIVIDEND_RATIO: 每股派息, RATIO_PRESENT: 每股送转, RECORD_DATE: 股权登记日, EX_DIVIDEND_DATE: 除权除息日, PLAN_NOTICE: 分红方案, }) df[每股派息] pd.to_numeric(df[每股派息], errorscoerce).fillna(0.0) df[每股送转] pd.to_numeric(df[每股送转], errorscoerce).fillna(0.0) for col in [除权除息日, 登记日]: if col in df.columns: df[col] pd.to_datetime(df[col], unitms, errorscoerce).dt.strftime(%Y-%m-%d) return dferrorscoerce这个参数值得多说一句它把无法转换的非法值强制变成NaN然后在下一步统一用fillna(0.0)填充。这样的好处是不会因为单条脏数据导致整个程序中断在真实数据场景里非常重要因为接口返回的数据偶尔会有异常值。3.3 全量抓取与本地归档全量抓取的主流程其实非常简单就是循环调用fetch_page把每页清洗后的DataFrame拼接起来最后统一去重落盘。这里我把页面数上限设置为total_pages动态获取避免写死数字。如果接口返回的total_pages发生变化代码也能自动适应。def fetch_all(): all_frames [] first_page_data, total_pages fetch_page(1) all_frames.append(clean_frame(first_page_data)) for page in range(2, total_pages 1): try: page_data, _ fetch_page(page) all_frames.append(clean_frame(page_data)) print(f第{page}/{total_pages}页抓取完成本页{len(page_data)}条) except Exception as e: print(f第{page}页失败: {e}) time.sleep(0.2) result pd.concat(all_frames, ignore_indexTrue) result result.drop_duplicates(subset[股票代码, 除权除息日], keeplast) result result.sort_values([除权除息日, 股票代码], ascending[False, True]) result.to_csv(eastmoney_dividend.csv, indexFalse, encodingutf-8-sig) print(f全量抓取完成共{len(result)}条记录) return result if __name__ __main__: fetch_all()去重逻辑是用股票代码除权除息日做联合主键。正常情况下同一只股票在同一天只可能有一条分红记录如果出现重复大概率是接口数据更新时产生了冗余保留最后一次抓取的结果即可。排序方面我按照除权除息日倒序排这样最新的分红记录永远在最前面查看的时候体验很好。这里要特别说明encodingutf-8-sig这个细节。如果不加sig用Excel打开CSV文件时中文会乱码因为Excel默认按ANSI编码读取文件。加上utf-8-sig会在文件开头写入BOM头Excel就能自动识别编码了。这个坑我踩过一次后来凡是导出中文CSV一律用这个编码。4. 运行过程与结果解读4.1 实际抓取运行记录跑一遍全量抓取我这边实测下来接口返回总页数大概是80多页每页500条实际上总共能拉到四万多条历史分红记录覆盖了从1990年代至今的沪深京三市A股分红送转数据。整体运行耗时大约在1分钟左右这个速度相当可观比之前手工收集提升了几个数量级。运行日志大概长这样第1/84页抓取完成本页500条 第2/84页抓取完成本页500条 第3/84页抓取完成本页500条 ... 第84/84页抓取完成本页156条 全量抓取完成共41578条记录注意最后一页不满500条是正常的因为总记录数不一定能整除页大小。整个过程中间偶发过两次请求超时但因为有异常捕获机制程序没有中断失败页码在日志里被打印出来后后续可以单独补跑。实际上我在抓取完后又重新对失败页码跑了一次确认数据完整性后才算结束。跑出来的CSV文件我打开简单扫了一遍发现数据质量比预期好。绝大部分记录的股票代码、除权除息日、每股派息都填充完整只有少量2023年后的新分红预案还挂着“预案”状态这些记录没有除权除息日属于未来要实施的分红方案并不能算数据缺失而是数据源本身的业务逻辑如此后续分析时需要单独区分。4.2 多维度分红数据分析数据到手后我顺手做了几个维度的分析算是验证一下这份数据的可用性。第一个维度是统计历年分红总额趋势。以2023年度为例全市场A股公司的每股派息加总后乘以总股本可以估算出整体分红金额观察这个金额的年度变化曲线能明显看出近年来A股上市公司分红总额逐年增长的趋势这背后既有监管层鼓励分红的政策因素也有上市公司盈利能力稳步提升的市场因素。import pandas as pd import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df pd.read_csv(eastmoney_dividend.csv) df[报告期] df[报告期].astype(str).str[:4] top20 df.groupby(股票简称)[每股派息].sum().nlargest(20) plt.figure(figsize(12, 8)) top20.plot(kindbarh) plt.title(历史累计每股派息 TOP20) plt.xlabel(累计每股派息元) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(top20_dividend.png)第二个维度是筛选高分红个股。我按近三年累计每股派息排序取前20名做了条形图。上榜的大多是银行、白酒、能源这类现金流充沛的传统行业龙头这个结果和常识基本一致。一只股票能持续大比例分红说明它赚钱能力稳定、现金流健康、分红意愿强这类公司在做价值投资时确实值得高看一眼。第三个维度是分析分红与送转的组合分布。统计所有记录中“只派息”“只送转”“派息加送转”“不分配”四类方案的数量占比结果显示A股市场中单纯派息的记录占比超过六成单纯送转的比例逐年下降这说明近年来的分红文化确实在向真金白银的现金分红倾斜过去那种动辄“10转10”的高送转炒作逻辑正在退潮。这个趋势对投资风格有直接参考价值。4.3 数据分析时的准确性校验数据分析做出来之后我特别做了一轮准确性校验因为数据源毕竟是二手数据不能盲信。校验方法很简单抽取10只知名股票比如贵州茅台、工商银行、宁德时代这些去巨潮资讯网找到它们最近一次分红公告把公告里的除权除息日和每股派息数字跟爬下来的数据逐条对照。抽检结果全部一致没有发现字段错位或者数值偏差。这个校验结果说明东方财富这个接口的分红数据可靠度比较高作为量化策略数据源是可以接受的。同时也提醒自己校验不能只做一次每次抓完数据后定期抽查是有必要的因为数据源偶尔也会更新字段逻辑抽查能及早发现数据异常。5. 常见问题与排查技巧实录5.1 高频报错及解决方案实际跑这个项目的过程中我累计遇到三类高频问题这里整理成速查表供参考。报错现象根本原因解决方案JSONDecodeError: Extra data请求参数带了callback返回了JSONP格式删掉params里的callback字段直接请求JSON接口返回code不为0filter参数语法错误或字段名拼写错误从浏览器Network面板复制完整filter参数逐字比对抓取中途请求超时触发风控或网络不稳加重试机制退避间隔0.2秒连续失败3次后跳过并记录中文字段在Excel乱码CSV编码不是utf-8-sig保存时指定encodingutf-8-sigJSONDecodeError是最常见的坑。我第一次调试的时候直接把浏览器里看到的完整请求URL复制到代码里结果因为URL里带着callbackjQuery351094...返回的不是纯JSON而是JavaScript表达式包裹。后来把callback参数去掉后问题迎刃而解。这提醒我看到接口返回结构不对时先去检查是不是JSONP协议没处理干净。还有个常见问题是filter参数写错。东方财富的filter参数格式是(SECURITY_TYPEA股)这样的括号加引号语法里面的引号必须是英文引号字段名必须和接口文档一致。我在早期写错过一次字段名接口直接返回错误码排查了很久才定位到是filter参数的问题后来就直接从浏览器里面复制参数不再手打了。5.2 抓取完整性的自我验证方法爬虫最怕的不是报错而是数据悄悄丢失了你还不知道。我习惯在抓取完成后做完整性验证方法很简单把全集按股票代码分组的数量与交易所披露的上市公司数量做对比。理论上每个上市公司至少有一条历史分红记录即使它从未分红在数据源里应该也会有一条“不分配”的记录所以股票代码数量应该接近全市场上市公司总量。如果差太多说明有部分公司数据没被抓全需要检查分页逻辑是否有遗漏。另外可以随机挑几只股票手动去东方财富网页上搜索这些股票的分红记录数量然后和本地CSV里对应代码的记录数做对照。这个排查方法看起来很土但效率极高几分钟就能确认整体数据质量的可靠性远比写一堆复杂的校验规则有用。5.3 项目扩展的三个方向这个项目跑通之后后续可以扩展的方向非常多。第一个方向是把分红数据和股价数据联动计算股息率。分红数据本身只是一半只有结合最新的股价才能算出股息率而股息率正是很多高股息策略的核心指标。把分红数据库和行情数据按股票代码关联就能算出一只股票的动态股息率再按行业排序筛出高股息标的池。第二个方向是做除权除息日的提醒工具。爬虫每天定时抓取数据把除权除息日在未来7天内的记录筛选出来推送到微信或者钉钉群里这样就能提前知道哪些股票即将除权提前规划操作对持有相关股票的人是有实际意义的。第三个方向是做分红连续性和成长性建模。统计每只股票过去5年是否连续分红分红金额是否逐年增长构建一个分红稳定性评分模型。这个模型可以用在量化选股里作为基本面因子的一部分。最后一个方向是把这套爬虫框架泛化到其他数据中心页面东方财富的数据中心还有很多类似维度比如业绩快报、财务指标、龙虎榜、大宗交易接口模式基本一致往往只需要换一下reportName和columns参数就能复用大部分代码。我之前做龙虎榜数据采集时直接复制了这篇代码的结构改改了20分钟就上线了性价比是很高的。写在最后整个项目跑下来我个人最大的体会是爬虫技术本身并不复杂真正有价值的是对数据质量的把控和对业务场景的理解。会写请求和解析JSON只是入门门槛能设计出稳定的重试策略、合理的字段清洗规则、可溯源的数据归档方式才是一个爬虫项目真正成熟的标准。东方财富这个接口算是比较友好的没有复杂的加密参数没有强风控验证非常适合作为爬虫实战练手项目。我后来用同样的代码框架去扩展抓其他财经数据时整个流程已经非常顺手了基本就是换参数、换清洗逻辑、重新跑一遍的事。如果你也想做金融数据相关的分析建议从这个项目开始把整套流程跑通后面再遇到什么数据源心里都有底。