ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Selenium抓取同花顺财报:从动态渲染到data2text训练样本的完整链路

2026/10/7 18:32:53 拓冰建站 浏览量
Selenium抓取同花顺财报:从动态渲染到data2text训练样本的完整链路 简介面向金融数据分析与data2text任务的数据采集需求这份zip压缩包整合了基于Selenium的同花顺上市公司财务报表爬虫方案适用于需要批量获取财报结构化数据的金融研究、量化分析与自然语言生成场景。包内共9个文件主要包括Python核心脚本、说明文档、md笔记、txt文本及示例图片main.py负责自动抓取README与说明文件覆盖安装配置与运行指引stocks.txt等维护抓取列表和失败记录整体约375KB轻量易部署。方案特别设计了cookie自动更新与浏览器行为伪装能有效应对常见反爬机制降低账号受限或访问拦截风险保障长周期采集的稳定性抓取结果可直接用于data2text任务中表格数据的清洗与预处理为金融分析报告自动撰写提供数据支撑附赠资源还包含额外的分析和解读材料。已有57人学习下载适合具备Python基础、希望快速搭建财报数据采集管道的开发者参考。1. 金融数据分析里的数据缺口Selenium 与同花顺财报自动抓取解决什么做金融数据分析的人早晚会碰上一件烦心事同花顺上把上市公司的营收、净利润、现金流堆得明明白白可要拿这些数据去训练 data2text 模型时手工复制粘贴几百张报表能把人累到怀疑人生。这个标题里的方案就是用 Selenium 控制浏览器去同花顺自动翻页、拉取财务报表再把这些表格清洗成模型能用的“数据-文本”训练对。它解决的是从网页到训练样本的最后一段体力活适合做财经 NLP、大模型数据采集、量化数据底座的人。看似只是爬虫真正磨人的反而是 cookie 生命周期和反爬虫伪装这两件事没做好脚本跑不过三天。2. 为什么是 Selenium动态财报页背后的抓取逻辑与选型对比2.1 同花顺财报页不是静态 HTMLrequests 拿到的只是空壳早期抓网页的习惯是 requests 加上请求头直接模拟但这套打法在同花顺财报页面会碰壁浏览器里表格内容一清二楚查看网页源代码却发现 table 区域几乎是空的。原因是这类页面采用了前端动态渲染报表数据由 JavaScript 异步请求接口获得再填入 DOM。requests 拿到的 HTML 是框架不是数据。那能不能直接逆向接口能但成本不稳定。接口里常见带签名参数和时间戳前端每次改版字段名一变解析逻辑就要跟着调整。而 Selenium 控制真实浏览器相当于让代码代替人手点页面服务器视角看到的就是普通用户访问。代价是慢和重但胜在稳定适合数据量在万级以下、按天更新的采集任务。方案稳定性速度反爬对抗成本适用场景requests 直接请求差页面改版即失效快高需逆向签名接口稳定的站点Selenium 控制浏览器好模拟真实操作慢中需伪装浏览器指纹动态渲染、登录态复杂的站点Playwright 等现代工具好API 更现代中中同样需要伪装需要截图、多标签协作的场景Selenium 在动态站点上的核心价值不是省事而是把“执行页面 JS”这件事从你身上拿走服务器看到的是完整渲染后的页面表格、分页、筛选器都在抓起来像静态页面一样。2.2 从启动浏览器到 DataFrame抓取链路的五个环节实际抓取一条财报数据链路是固定的五个环节启动浏览器并配置伪装参数访问目标财报列表页按报告期做筛选翻页并等待表格渲染最后从 page_source 中提取表格交给 pandas 解析。顺序不能乱尤其“等待”必须在“提取”之前。同花顺的表格是异步加载的没等行数出现就抓拿到的常常只有表头和空行。这里有个容易忽略的定位整个链路里Selenium 只负责前四步第五步交给 pandas 的 read_html。read_html 会从 HTML 字符串里把 table 标签解析成 DataFrame省掉手写 XPath 提取单元格的苦活。这也是选择 Selenium 的一个隐藏好处——渲染后的 HTML 结构规整read_html 能直接接手。项目里每个模块的分工应该从第一天就定清楚cookie 管理是独立模块页面操作是独立模块数据清洗是独立模块。混在一起写后期排查问题时你会被日志淹没。2.3 data2text 任务需要什么样的数据结构化表格到文本的映射data2text 任务是给定结构化数据生成自然语言描述。比如给一行财务数字让模型输出“报告期内公司实现营业收入 215.3 亿元同比增长 8.7%”。所以对这个爬虫项目来说抓取只是前半场后半场是构造训练样本。抓下来的表格不能直接当训练集用必须先定三件事字段名统一、单位统一、报告期标注。很多翻车现场是抓取层只存了数值没存报告期到了构造句子阶段才发现没法区分 2023 年年报和 2023 年三季报。模型学到的是“一串数字对应一句话”如果数字和上下文对不上生成出来的文本看着通顺数字都是错的这类错误在评测时极难发现。所以抓取之前先设计好目标格式公司代码、公司简称、报告期、指标名、指标值五列锁定后面所有环节都围绕这个结构展开。3. 用 Selenium 抓同花顺财报最小脚本与身份伪装参数3.1 驱动与浏览器参数三个必调的伪装选项Selenium 控制 Chrome 的第一步是配置 Options这一步决定了你的脚本更像是真实用户还是更像一个裸露的自动化测试工具。以下是我每次写抓取脚本都会先贴上的基础配置from selenium import webdriver from selenium.webdriver.chrome.options import Options opts Options() # 无头模式在某些反爬策略下容易被标记先开着有头调试稳定后再换 opts.add_argument(--headlessnew) # 窗口尺寸是反爬指纹的一部分1920x1080 是大多数用户的分辨率 opts.add_argument(--window-size1920,1080) # 去掉 Chrome 的自动化控制提示也能去掉一部分 webdriver 特征 opts.add_argument(--disable-blink-featuresAutomationControlled) # 设置桌面版 UA避免被识别为自动化环境 opts.add_argument(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36) # 这两个实验选项是常见的去自动化标记组合 opts.add_experimental_option(excludeSwitches, [enable-automation]) opts.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsopts)固定窗口尺寸是很多人忽略的细节。默认的无头窗口只有 800x600这在服务端指纹库里是明显的自动化特征。设置 1920x1080 后配合 UA 和禁用 AutomationControlled脚本的“人味”会明显提升。提示如果你发现 --headlessnew 在某些版本下不稳定可以退回旧的无头方式但要注意旧无头模式对 WebGL 等特性的支持更弱部分反爬库能通过 canvas 指纹识别无头环境。最好的办法是调试阶段用有头模式部署阶段跑在无人的服务器上用虚拟显示器方案这个后面避坑章节再细说。3.2 进页面后先等渲染显式等待与抓取时机Selenium 最坑的一点是driver.get() 返回并不代表页面加载完成财务报表这类异步渲染页面尤其明显。表格区域可能还在转圈你的代码已经往下走了。解决办法是显式等待明确告诉浏览器“等表格行出现再继续”。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver.get(目标站点财报列表页) wait WebDriverWait(driver, 20) # 等到页面里出现表格行说明主体数据已经渲染完成 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, tbody tr))) # 此时再取页面源码交给 pandas 解析就是完整表格 html driver.page_source这里用tbody tr而不是某个具体的 class 名称是有意为之。财报页面经常改版CSS class 说变就变但表格的行结构tbody tr是 HTML 语义层的东西改版概率低得多。另外presence_of_element_located 只检查元素是否存在如果要保证表格里的数据行数是稳定的还可以配合EC.text_to_be_present_in_element去检查“共 xx 条”这类分页提示文本。等待超时设置 20 秒是比较合理的。太短弱网环境会误报太长页面资源加载阻塞时会拖慢整体抓取。超时后别直接抛异常退出先把 driver.page_source 存成快照文件排查时你能看清页面到底卡在什么状态。3.3 翻页和表格提取read_html 把渲染结果变成 DataFrame财报列表页一般都有分页遍历分页时页面 URL 往往带 page 或 pageNum 参数。翻页逻辑不复杂关键是每翻一页都要重新等待渲染再把 page_source 交给 pandas 解析不能复用上一次的 HTML。import pandas as pd import time all_frames [] base_url 目标站点财报列表页report_date2023-12-31 for page in range(1, total_pages 1): url f{base_url}page{page} driver.get(url) # 每页都要等防止翻页后数据还没渲染就抓取 WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.CSS_SELECTOR, tbody tr)) ) # read_html 返回当前页面所有 table 的 DataFrame 列表 tables pd.read_html(driver.page_source) if not tables: continue df tables[0] # 记录当前页数和报告期后续清洗时用来对齐数据 df[page] page df[report_date] 2023-12-31 all_frames.append(df) # 翻页间隔两个连续请求之间至少留 1.5 到 4.5 秒 time.sleep(random.uniform(1.5, 4.5)) raw_df pd.concat(all_frames, ignore_indexTrue)pd.read_html 是好工具但它依赖 lxml 解析库首次使用需要确认环境里装好了 lxml否则会在运行时抛 ImportError。另一个常见的坑是页面里如果包含图表组件read_html 可能把图表里的 SVG 展示也用 table 包裹返回多个 DataFrame。取 tables[0] 前最好检查一下列名是不是你预期的那几个财务指标否则后续清洗时会拿到一堆无关数据。提示page_source 是在当前渲染状态下拿到的如果表格使用了懒加载滚动到底部才加载更多行你需要先让 Selenium 模拟滚动到底部再取 HTML。标准做法是执行driver.execute_script(window.scrollTo(0, document.body.scrollHeight))并配合短暂等待。3.4 cookie 的获取、注入与更新登录态的保存与刷新同花顺的财务数据部分模块要求登录后才能查看完整字段。每次手工登录再跑脚本显然不现实所以要做 cookie 持久化第一次手工登录后把 cookie 导出之后脚本启动时自动注入。import json import time # 首次登录手工在浏览器里完成登录然后执行 get_cookies() 导出 cookies driver.get_cookies() with open(cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse) # 后续启动重新加载 cookie 并注入到新会话 with open(cookies.json, encodingutf-8) as f: cookies json.load(f) # add_cookie 之前必须先访问一次目标域名否则会报 InvalidCookieDomainException driver.get(目标站点首页) for c in cookies: # 把过期时间字段删掉避免注入一个已经过期的 cookie c.pop(expiry, None) driver.add_cookie(c) driver.refresh()get_cookies 导出的字典里包含 name、value、domain、path、expiry 等字段add_cookie 时只需要 name 和 value 就能生效但 domain 要和当前访问的域名匹配。expiry 字段是 Unix 时间戳如果原 cookie 即将过期注入后也会立刻失效所以注入前把它删掉让浏览器在会话期重新协商是一个更稳妥的策略。cookie 更新的核心是“什么时候换”我的习惯是每天早上跑任务前先做一次登录检测访问一个只有登录用户才能看到的页面检查是否出现了用户名或退出按钮。如果没出现就认为登录态已失效脚本会弹出一个窗口提醒人工重新登录并更新 cookies.json而不是傻傻地带着过期 cookie 去抓数据抓回一堆登录跳转页面的 HTML。4. 把财报表格做成 data2text 样本清洗、单位换算与文本模板4.1 多级表头与合并单元格第一个绕不开的脏数据坑同花顺财报表格的表头不只是一行。“指标”下面还有“报告期”、“同比增长”、“环比增长”分组read_html 解析出来会得到 MultiIndex 列名。你要是不处理后续按列名取值时会一直报 KeyError。import pandas as pd df raw_df.copy() # 把 MultiIndex 列名展平成字符串空列用上一级列名填充 df.columns [_.join([str(level) for level in col if Unnamed not in str(level)]) for col in df.columns] # 去掉列名里多余的空格防止 read_html 把空格带进来 df.columns [col.strip() for col in df.columns]展平后列名可能长成“营业收入_2023-12-31”或“归母净利润_2022-12-31”这样的形态。我一般会再手动维护一张字段映射表把不同报告期的列归一到统一的“指标名 报告期”结构这样才能让后续的文本模板构造逻辑保持简单。如果你发现某些列展平后是空的那是 read_html 对合并单元格的处理缺陷需要回退到 XPath 手工提取这些列。4.2 把“亿”和“%”变回数值统一单位换算财报页面上的数据默认显示成“215.30亿”这种字符串同比增长显示成“8.72%”。这些东西人看没问题但 data2text 模型需要的结构化字段是数值类型否则生成句子时没法做格式化拼接。import re def to_number(x): 把 215.30亿、1.2万 转成 float空值返回 None if x is None or (isinstance(x, float) and pd.isna(x)): return None x str(x).replace(,, ).strip() if x in (--, -, ): return None unit 1.0 if 亿 in x: unit 1e8 x x.replace(亿, ) elif 万 in x: unit 1e4 x x.replace(万, ) try: return float(x) * unit except ValueError: return None def to_percent(x): 把 8.72% 转成 0.0872百分比数值统一成小数 if x is None or (isinstance(x, float) and pd.isna(x)): return None x str(x).strip().replace(%, ) try: return float(x) / 100.0 except ValueError: return None df[revenue] df[营业收入_2023-12-31].apply(to_number) df[rev_yoy] df[营业收入同比增长_2023-12-31].apply(to_percent)注意 to_number 里对空值的处理。财务数据里“--”表示该期间无数据或未披露这类值要转成 None 而不是 0因为模型学到“0”会误认为业务归零。to_percent 统一转成小数也是给文本模板铺路——格式化时可以直接乘 100 加百分号避免字符串拼接时重复出现百分号。4.3 构造训练样本模板轮换、实体对齐与报告期去重数据清洗到位后就可以构造 data2text 样本了。这一步的核心不是写 Python而是确定“模型要从哪些字段学到什么”。我的模板设计思路是每个样本固定包含公司简称、报告期、核心财务指标和同比变化但句式和字段顺序不能千篇一律否则模型会产生位置偏见。import random import json def fmt_number(v): if v is None: return 未披露 # 超过一亿的用亿作单位保持文本可读性 if abs(v) 1e8: return f{v / 1e8:.2f}亿元 return f{v:.2f}万元 def fmt_yoy(v): if v is None: return 未披露 return f{v * 100:.2f}% templates [ {company}在{report_date}实现营业收入{revenue}同比增长{yoy}。, 报告期内{company}的营业收入为{revenue}较上年同期变化{yoy}。, {report_date}{company}营收达到{revenue}同比增速为{yoy}。, ] samples [] for _, row in df.iterrows(): # 同一份数据用随机句式生成避免模型死记模板 template random.choice(templates) text template.format( companyrow[company], report_daterow[report_date], revenuefmt_number(row[revenue]), yoyfmt_yoy(row[rev_yoy]), ) samples.append({ table: { company: row[company], report_date: row[report_date], revenue: row[revenue], rev_yoy: row[rev_yoy], }, text: text, })实体对齐是这块最容易翻车的地方。同花顺页面上的公司简称、报告期写法和模型训练语料里的常见表达不一定一致。比如页面里写“报告期:2023-12-31”你希望文本里是“2023年年报”就需要维护一张报告期别名映射表把 12-31 结尾转成年报、03-31 转成一季报。公司简称同理建议在做样本构造前先跑一遍公司名去重人工核对是否有同一家公司多种写法的情况。4.4 输出格式与文件组织JSONL 与断点续跑样本构造完成后输出格式建议用 JSONL每行一个完整样本。相比 CSVJSONL 的优势是每条记录自包含字段新增不用改表结构也方便后续接入大模型训练框架时直接流式读取。with open(data2text_train.jsonl, w, encodingutf-8) as f: for s in samples: f.write(json.dumps(s, ensure_asciiFalse) \n) # 同时输出一条不参与模板化的原始数据用于校验和回溯 raw_df.to_csv(financial_statements_raw.csv, indexFalse, encodingutf-8-sig)CSV 这边用 utf-8-sig 编码是因为 Excel 打开无 BOM 的 UTF-8 文件会乱码这也是一个常见的协作摩擦点。JSONL 文件不需要表头训练脚本读取时逐行 json.loads 即可中间某一行损坏不影响其他行解析——这个特性在采集任务中断时尤其有用。5. 踩坑与排查清单cookie 失效、反爬识别和浏览器退出异常5.1 cookie 第二天就过期登录态失效的检测与自动恢复现象前一天脚本跑得好好的第二天启动后抓到的全是登录跳转页DataFrame 解析出来是空的。原因登录 cookie 通常有有效期短的话一两天就过期。脚本加载的是磁盘上的旧 cookie 文件没有检测登录态带着过期 cookie 去请求服务端重定向到登录页而 read_html 只解析到登录表单。解决打一个登录检查前置步骤在每次任务开始前访问受保护页面断言出现登录成功的标志元素。下面是一个最小实现def ensure_login(driver, cookies_file, check_url, success_selector): driver.get(check_url) try: WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, success_selector)) ) return True except Exception: # 登录态失效重新注入前先清空旧的失效 cookie driver.delete_all_cookies() with open(cookies_file, encodingutf-8) as f: cookies json.load(f) driver.get(目标站点首页) for c in cookies: c.pop(expiry, None) driver.add_cookie(c) driver.get(check_url) WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, success_selector)) ) return True这里的逻辑是失效就清空全部 cookie 再重新注入磁盘里上次导出的 cookie。如果磁盘上的 cookie 也已经过期会跳回登录页触发异常这时候让脚本停下来提醒人工干预比让它继续空跑一天强得多。5.2 无头模式被识别拒绝反爬虫伪装失败的典型症状现象本机有头模式一切正常部署到 Linux 服务器开启 headless 后请求频繁返回 403 或验证码页面。原因无头模式与正常浏览器的差异是多维度的UA 里的 HeadlessChrome 标记虽然可以手动改、canvas 指纹、WebGL 渲染信息、navigator.webdriver 属性。反爬系统综合判断后能识别“这不是一个真人浏览器”。解决最简单有效的做法是放弃无头在服务器上装虚拟显示器 Xvfb然后继续跑有头模式。配合之前的伪装参数成功率会大幅提升。另一个技巧是在启动后执行一段 CDP 脚本把 navigator.webdriver 属性改成 undefineddriver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) })这个 CDP 注入必须在每次导航前执行。它不解决全部问题但能挡住一部分只检测 webdriver 属性的反爬实现。如果服务端升级了检测策略出现新的验证码弹窗最直接的应对是切换代理 IP 和降低请求频率同时检查是否有其他浏览器指纹被标记。5.3 抓到一半只剩表头渲染不全导致的缺行现象批量抓取的 200 个页面里有十几页只抓到列名一行数据都没有。单独打开这些页面数据是正常的。原因这些页面恰好数据量较大表格需要二次加载或滚动触发。Selenium 的 presence_of_element_located 只检查了第一个 tr 是否存在而第一行可能只是表头或首行占位后续数据行还在异步加载。解决把等待条件升级为“明确知道总行数”。如果页面有分页器显示“共 156 条”可以等待包含行数的文本节点出现再用text_to_be_present_in_element判断。另一个更稳妥的方案是抓完 HTML 后用tbody tr的数量做一次 sanity check如果行数小于预期sleep 两秒重抓当前页。这个“重试一次”的兜底逻辑能覆盖大部分偶发缺行的问题。5.4 浏览器进程堆积一场把内存吃满的“僵尸浏览器”现象脚本连续跑了几天服务器内存持续上涨docker 容器频繁 OOM。ps 一看几十个 chrome 进程没退出。原因脚本中途异常退出finally 里的 driver.quit() 没有执行chromedriver 和它拉起的 Chrome 子进程变成孤儿进程一直驻留在系统里。每次运行残留一批内存逐渐被吃光。解决除了用 try/finally 确保退出更保险的做法是在程序启动时先清理之前残留的浏览器进程。我一般会在入口处加一段清理逻辑# Linux 上清理残留的 chrome 和 chromedriver 进程 pkill -f chromedriver || true pkill -f chrome.*headless || trueWindows 环境对应的是 taskkill /F /IM chromedriver.exe。这段清理放在任务调度的最前面可以避免“上一次崩溃拖垮本次运行”的问题。代码里再配合 with 语句或 contextlib 封装 driver 生命周期双重保险。5.5 报告期数据串行按行号合并表格的代价现象构造 data2text 样本时发现 2023 年年报的营业收入行和 2022 年年报的数值错位生成出来的句子数字对不上。原因抓取时按 page 顺序拼接 DataFrame但不同报告期的表格行数不完全一致某些公司某年数据缺失页面会自动补空行或删除一行导致行号错位。按行号直接合并多个报告期几乎一定踩坑。解决合并时不要依赖行号用“公司代码 指标名 报告期”做唯一键再 merge。具体做法是把每个报告期的表格都转成“公司代码、指标名、报告期、数值”的长表结构再用 pivot 转宽表。整理完以后抽查几个公司的数据和页面手工核对这一步能拦下大部分对齐错误。6. 把一次性抓取升级成采集小管线调度节奏与数据校验技巧当抓取脚本稳定运行一周后你会发现真正的问题不再是 Selenium 本身而是整条采集流程的工程化。我现在把这类任务拆成三层调度层、抓取层、校验层。调度层用系统自带的 cron 或 Windows 任务计划程序每天固定时间跑一次。关键不是“启动”而是“止损”给整个任务设置一个最大运行时长超时直接杀掉浏览器进程并记录日志防止页面阻塞导致任务堆叠。抓取层最重要的心法是请求节奏。同花顺虽然不会像小网站那样动辄封 IP但短时间高频访问依然会触发风控。我会在每次翻页之间随机加 1.5 到 4.5 秒的延迟每个公司页面之间加 5 到 10 秒。抓完 50 家公司休息一分钟让服务端日志里看到的是一个“人类浏览速度”的访问序列。校验层是 data2text 数据质量的地基。我习惯每次抓取完自动做一次交叉校验用利润表里的营收同比增长率和上一年营收实际值做复核如果两者对不上说明要么抓错了行要么公司发生了报表重述。具体校验公式不复杂# 用 2022 和 2023 的营收绝对值反算同比与页面标注的同比增长对比 implied_yoy (rev_2023 - rev_2022) / rev_2022 actual_yoy row[rev_yoy] if abs(implied_yoy - actual_yoy) 0.02: print(f数据不一致: {company} {report_date})误差阈值设在 0.02即两个百分点。超过这个值需要人工确认是抓取错误还是公司发布过更正数据。浏览器退出这件事我以前吃过不小的亏。早期图省事直接 headless 跑完就扔那后来服务器内存被 Chrome 进程堆满才发现问题。现在的习惯是在脚本入口先清理残留进程脚本结尾用 finally 保证 driver.quit()再额外加一道超时看门狗。这套组合下来连续跑三个月没再出现僵尸进程。整个方案值不值得做我的判断是如果你只有几十家公司、几个报告期的数据量手工复制最多半天就结束了不值得上 Selenium但如果你要覆盖全市场五千多家公司还要按季度增量更新那这套“浏览器自动化 cookie 管理 数据校验”的管线就是必需品。踩过这么多坑之后我最大的体会是爬虫脚本写出来不难难的是让它明天、下周、下个月还能稳定跑出干净数据。希望帮到你。本文还有配套的精品资源点击获取