年报季,谁翻财报谁知道。打开一份几百页的PDF,先在目录里找“合并利润表”,翻到对应页码,对着表格一格一格地抄数据:营业收入、营业成本、净利润……一套十来项指标,复制粘贴加手工校验,一家公司少说也要十几分钟,遇到扫描版PDF还得自己敲数字。要是碰上几十家公司,一天基本就耗在这上面了,眼睛看花不说,稍不留神还能把“归母净利润”和“净利润”填串行。
我后来搭了一套自动化抽取方案,核心组合是文本解析平台TextIn和AI应用编排平台Coze。简单说,用TextIn把财报PDF解析成结构化文本,再让Coze编排一个工作流,自动抽取指标、计算派生数据、回填到表格里。配置好之后,单份财报从上传到生成结果只需要几分钟,批量处理时体验更明显。这个方案不需要太深的编程功底,财务分析师、投研助理、代码基础薄弱但想提效的人都能上手,下面把整个搭建过程和踩过的坑从头到尾讲清楚。
1. 核心思路拆解:为什么是“解析 + 编排”这套组合
1.1 问题本质:财报里的数据,难点从来不是“读”,而是“结构化”
很多人在接到“财报数据抽取”这个需求时,第一反应是写Python脚本、用开源解析库。但真正跑过一轮就会明白,难点不在“识别文字”,而在“把散落的文字还原成有业务含义的字段”。一份财报PDF里有几百个表格、上千个单元格,还有合并单元格、跨页断行、科目名称换行、货币单位不统一等各种情况。
我最初也试过纯代码路线,用解析库把PDF转成文本,再靠正则表达式去匹配“营业收入”后面的数字。结果并不理想:有些财报的“营业收入”后面紧跟的是“营业总收入”,有些表格里“营业收入”下方还有“其中:营业收入”一行,正则表达式写来写去越来越复杂,换个公司、换个报告期就可能匹配失败。这属于典型的“用工程方法解决文本理解问题”,维护成本极高。
后来我才把思路调整为:先用专业的文档解析服务把财报转成结构化数据,再用AI大模型去理解和抽取字段,最后用低代码工作流把整条链路串起来。这套组合的好处是各干各擅长的事:TextIn负责解决“非结构化数据怎么变规整”的问题,Coze负责解决“流程怎么自动编排、AI怎么调用”的问题,我不需要维护复杂的解析逻辑,也不用关心服务器的稳定性。
1.2 技术选型:给PDF装一只眼睛,再给流程配一个调度员
TextIn在这套方案里的角色,相当于给PDF装了一双“看得懂表格”的眼睛。它能把财报里的表格结构识别出来,输出成有序的文本块,甚至支持按坐标定位某个单元格。这对于财报这种“位置即语义”的文档特别关键——财报里的数据,光看文字本身没法确定含义,必须结合它所在的表名、行名、列名来判断是哪个指标。
Coze的角色,则更像一个总调度员。它负责把“文档解析”“指标抽取”“数值校验”“结果回填”这些步骤串成一条自动流程,每一步之间可以传递数据。尤其方便的是它内置了交互式的AI应用节点,不需要我写一堆调用大模型的繁琐代码,就能自定义提示词让AI从解析结果里提取指定的财务指标,并且规定输出为JSON格式。
这套组合对非程序员尤其友好。之前一些同事想让我搭个工具帮他们减轻底稿工作量,但一提Python环境、依赖安装,很多人就直接放弃了。而TextIn和Coze都是可视化配置,全程不需要部署任何本地环境,浏览器打开就能操作。即使完全不懂代码,按照流程照猫画虎也能搭出基础版本。
1.3 整体数据流:五步完成从PDF到数据表
我最终跑通的方案,整体链路是固定的五步:
- 上传财报PDF到解析平台的对应解析任务中,触发文档解析;
- 解析平台返回带有页码、坐标、内容的文本块结构;
- Coze工作流接收到结构化文本,调用大模型节点,按照预设提示词抽取营收、净利润、总资产等核心科目;
- 用代码或公式节点计算毛利率、ROE、资产负债率等派生指标;
- 将结果写入目标表格(Excel、飞书多维表格、数据库均可),同时输出人工复核标记。
这五步听起来简单,实际执行中每一步都有不少细节。尤其是字段设计、提示词写法、异常值判断这三块,直接决定抽取结果靠不靠谱。我后面几张表里会详细拆开说。
| 处理环节 | 传统人工方式 | 自动化方案 | 节省时间 |
|---|---|---|---|
| 定位报表页码 | 看目录、手翻页,一份5-10分钟 | 解析服务自动定位 | 约80% |
| 提取指标数据 | 肉眼核对、手输Excel | 大模型按提示词抽取 | 约90% |
| 计算派生指标 | 手工拉公式 | 工作流内自动计算 | 约100% |
| 多份财报汇总 | 逐份复制粘贴 | 批量任务并行处理 | 约90% |
说“5分钟”,其实包含两层意思:一层是配置好完整流程大概只需要5分钟左右;另一层是单份财报从上传到拿到结构化结果,实际运行也就几分钟。两个维度都实打实。
2. TextIn端配置:把财报PDF变成机器能读懂的文本
2.1 创建解析任务与报表类型选择
TextIn的使用入口很直接,注册后进入控制台,先创建一个解析应用。创建时最关键的一步是选择处理类型,因为平台内置了针对不同版式的优化模型,选错类型会直接影响识别效果。财报PDF属于“表格+文字混合”的文档,我建议优先选支持阅读顺序输出和表格结构还原的处理模式,这样后面的AI节点拿到的是有序文本而不是乱序字符流。
创建完成后,解析页面上传一份财报PDF做测试。测试目的只有一个——确认平台输出的文本结构是否符合预期。打开返回结果,重点看三件事:
- 表格中的科目名称和数据有没有被拆到不同文本块;
- “合并利润表”“资产负债表”等表头是否被正确识别;
- 跨页表格的上下文有没有粘在一起。
如果表格结构还原得好,你会看到每一行几乎都保持“科目名称 + 数字”的顺序,这就为后面大模型抽取打下了好基础。相反,如果结果里全是戛然而止的碎片文本,那就要考虑换一种处理类型,或者开启额外的版面分析选项。
我在这个环节踩过最大的坑是:有些财报PDF本身是电子版,有些是扫描版。扫描版的识别速度和处理精度都不一样,第一遍测试时最好用一份扫描版财报,最差情况能跑通,其他情况就都不是问题了。
2.2 字段设计:先想清楚要抽哪些指标,再配置模板
很多人拿到解析结果后急着让AI去“读报表”,这其实本末倒置了。正确的做法是先想清楚业务上到底需要哪些字段,然后让AI只针对这些字段做抽取。字段设计得越明确,大模型的输出越可控,后期交叉复核也越轻松。
以我处理A股财报的经验,三个核心报表里必抽的字段大概是这些:
| 报表 | 必抽字段 | 备注 |
|---|---|---|
| 利润表 | 营业总收入、营业收入、营业成本、销售费用、管理费用、财务费用、归母净利润、扣非净利润 | 注意区分“净利润”和“归母净利润” |
| 资产负债表 | 总资产、总负债、所有者权益合计、货币资金、应收账款、存货 | 关注期末数还是期初数,建议两个都抽 |
| 现金流量表 | 经营活动现金流净额、投资活动现金流净额、筹资活动现金流净额 | 注意正负号,很多初学者会漏掉负数 |
| 每股数据与回报 | 基本每股收益、加权净资产收益率(ROE) | 有些在利润表附注里,需要提示模型到补充资料里找 |
字段不是越多越好。字段多,一方面容易让大模型产生幻觉,把“营业收入”跟“营业总收入”混淆;另一方面也增加校验难度。我的习惯是第一版只保留上面这19个核心指标,跑通后再按需增加。
提示:设计字段时一定要明确“取合并报表数据,还是母公司报表数据”。A股财报里往往同时存在合并利润表和母公司利润表,两者科目相同但数字完全不同。我通常统一取“合并”这一列,并在提示词里用一句话锁定。
2.3 配置解析结果字段:用“同目录填写”的思路处理表格定位
TextIn支持基于模板配置结果字段,让解析结果直接输出成业务字段名。这一步相当于提前把“大模型抽取”的一部分工作固定下来,减少后续AI环节的负担。
具体操作逻辑可以理解成一个“同目录填写”的思路:先在解析结果里框选“营业收入”这个单元格所在位置,告诉平台“我要提取这一行的数据”;平台记住这个位置特征后,后续文件只要版式相近,它就会自动匹配并取出对应数值。当然,由于各家财报的科目排序不完全一致,做法上不能死板地只靠坐标,要结合表头字段去做动态定位。
我在模板配置阶段的实用经验有三条:
第一,字段别名要写全。比如“货币资金”在部分银行财报里叫“现金及存放中央银行款项”,只设一个名称容易漏,建议在同名字段底下配置多个别名。
第二,灵活使用以“表名+科目名”定位的方式。不要只选一个单元格,而是把单元格所在的行首科目名也一起包含进来,相当于让平台按“某某表里的营业收入那一行”来找,跨版本兼容性明显更强。
第三,对不存在的科目要设置默认值。比如有些公司没有“其他收益”这行,解析时就会跳过,如果不做处理,后续大模型可能把上一个数值错误地带过来。我在模板里给这类字段设置了“空值填充为0”的后处理规则。
2.4 处理扫描件、跨页和特殊排版
财报PDF有个特点,很多重要数据正好在表格的延续位置,比如利润表最后一行“净利润”经常被挤到下一页顶部或底部。如果解析平台没有正确处理跨页表格,原本应该属于同一张表的上下文会被硬生生切断。
针对跨页问题,我常用的补救方法是:在配置界面把“阅读顺序”调整为“按表格块输出”,尽量让一个逻辑表格的连续文本块紧挨在一起;同时把大模型的上下文范围放宽,让它看到的是整个利润表相关的文本块,而不是零散的段落。
扫描件的问题更普遍,很多早期年报全是扫描图,字体模糊、带水印、有折痕。这种文档不能指望一次解析到位,我建议开启“图像增强”类处理参数,让平台先做倾斜矫正和对比度优化。另外,扫描件中“0”和“O”、“1”和“I”经常混淆,如果抽取结果里出现明显异常,比如总资产突然多一个零,一定要回到原始PDF位置做人工确认,不能盲目相信解析结果。
3. Coze工作流搭建:把解析结果变成最终可用的数据
3.1 创建工作流:节点怎么排,数据怎么传
TextIn端把文本结构准备好之后,真正“自动化”的核心就到了Coze这边。我一般先新建一个“开源插件”或“自定义应用”类型的流程,然后在画布上把节点按顺序拖出来。
节点布局上,我推荐如下顺序:
- 开始节点:接收文件URL或上传路径;
- 文档解析节点:调用TextIn能力,传入文件地址,拿到解析后的JSON文本;
- 大模型节点:把解析结果整段喂给大模型,同时附上提示词和输出约束;
- 代码节点:对大模型返回的JSON做二次处理,计算派生指标、补齐空缺字段;
- 数据回填节点:把最终结果写入飞书多维表格或Excel在线文档。
这里最需要注意的是节点之间的字段传递。Coze工作流里,前一个节点的输出会成为后一个节点的输入变量。为了让下游节点不迷路,我在大模型节点的输出约束里会明确要求返回固定的JSON字段名,比如revenue、net_profit、total_assets,这样代码节点里直接用变量名取值即可。
一开始我犯过一个错误:在大模型节点里把所有抽取结果塞进一个长字符串,然后在回填节点里用正则去拆,结果经常拆错。后来改成让大模型直接输出严格JSON格式,回填节点按Key取值,稳定多了。这也提醒大家:流程设计上,越早让数据变成结构化格式,后面的麻烦越少。
3.2 大模型节点提示词:把“岗位说明书”写得清清楚楚
大模型节点是整套流程里最灵活也最容易翻车的环节。它的作用是从解析出的长文本中提炼指定财务指标字段,本质上相当于给一个临时分析师布置工作。只不过这个分析师的理解能力很强,但耐心有限——你不在提示词里说清楚需求,它就会按自己对公司财报的理解“自由发挥”。
我目前在用的提示词模板,经过多轮迭代后基本稳定,核心结构是四段式:
你是财报数据抽取专家。请从下面给定的财报解析文本中,按字段清单提取数据。 字段清单及说明: - revenue:营业总收入(合并利润表,本报告期金额) - net_profit:归属于母公司股东的净利润 - total_assets:资产总计(期末余额) - total_liabilities:负债合计(期末余额) - equity:所有者权益合计(期末余额) - ocf:经营活动产生的现金流量净额 提取要求: 1. 只输出JSON,不要输出任何解释。 2. 缺失的字段填null,不要猜测。 3. 金额单位统一为元,如果原文是“万元”“亿元”,先换算成元再输出。 4. 只取合并报表数据。 解析文本内容如下:这里有几个关键细节值得多说两句。
第一,金额单位问题。“单位统一为元”这条约束极其重要,否则今天输出的是百万元、明天输出的是万元,最终汇总时对不上账。我在代码节点里还加了一道兜底逻辑:如果大模型返回的数值明显异常,比如总资产小于净利润,就自动标记该条记录为“需复核”。
第二,“只取合并报表数据”必须明确。很多财务新手分不清合并和母公司报表,如果提示词不写,大模型通常会按文本中出现位置先抽取,结果就是一会儿是合并的、一会儿是母公司的,数据完全不可比。我在提示词里用一行字锁定后,这个问题基本绝迹。
第三,关于“缺失填null”。以前我不加这条,大模型经常自作聪明地拿上一期数据或相近科目来补,看起来“填满了”,实际上全是错的。后来在提示词里强制“缺失就写null”,并且在后续节点里把null统一标记为待人工补录。
注意:大模型的输出温度建议设置为0,也就是最保守的采样模式。抽取任务不是创作任务,需要的是稳定性和确定性,不要让它“发挥”。
3.3 代码节点:计算派生指标并处理异常值
大模型直接输出的字段能满足基础需求,但业务上通常还需要毛利率、ROE这类派生指标。这些指标完全可以在Coze的代码节点里用JavaScript脚本来计算,不需要让大模型去“心算”,一方面快,另一方面可追溯。
我的一段代码逻辑大致是:
const data = input.data; // 大模型节点输出的JSON对象 let grossMargin = null; if (data.revenue && data.operating_cost) { grossMargin = (data.revenue - data.operating_cost) / data.revenue * 100; } let roe = null; if (data.net_profit && data.equity) { roe = data.net_profit / data.equity * 100; } let debtRatio = null; if (data.total_liabilities && data.total_assets) { debtRatio = data.total_liabilities / data.total_assets * 100; } // 标记异常值:净利润为负但蚀收为正,仍保留真实负值 let flag = ''; if (data.net_profit && data.net_profit < 0) flag = '亏损'; output = { ...data, gross_margin_pct: grossMargin ? Number(grossMargin.toFixed(2)) : null, roe_pct: roe ? Number(roe.toFixed(2)) : null, debt_ratio_pct: debtRatio ? Number(debtRatio.toFixed(2)) : null, check_flag: flag };这段代码的核心是防止除零和空值计算。实际跑的过程中,经常遇到equity为0或total_assets为0的极端情况,直接除会返回Infinity,落库就成了脏数据。所以每个计算步骤前都要做存在性判断,算完还要保留两位小数,避免浮点误差。
计算完之后,我会在输出对象里增加一个check_flag字段。这个字段的作用是给后续人工复核做提示,比如净利润为负、总资产比负债还少等异常情况,系统不会“自作主张”地修正,但会优先标记出来。
3.4 数据落库与批量触发:多份财报一起跑
数据最终要落到一个可查询的地方。我第一版方案是输出CSV,后来改成了写飞书多维表格。原因很简单,多维表格里可以直接加“复核状态”字段,人工确认后打钩,整个处理流程就成了半自动闭环。如果团队习惯用Excel,Coze里也有对应的在线表格操作节点,原理一样,只是目标源不同。
批量处理时,触发方式有两种选择。一种是手动逐份上传,适合零星补充;另一种是设定为定时启动或批量文件导入,适合处理几十份财报的合集。我建议从第二种开始,因为自动化方案的价值在大批量时才会完全释放。把几十份财报PDF压缩包或文件列表传到指定位置,工作流自动遍历并逐份解析、抽取、落库,中途失败的任务会抛异常并记录日志,不会影响其他文件。
批量跑起来后还有个“限速”问题需要留意。解析服务的并发调用有限额,Coze工作流并行执行时如果触发频率过高,容易报429这一类限流错误。我的处理方法是把批量任务切分成小批次,比如每次并发3份、间隔几秒再继续下一批,稳定性和速度都能接受。
4. 常见问题与排错经验:实测中踩过的坑
4.1 高频问题速查表
整个方案我从搭建到稳定运行,前前后后折腾了好几周,下面这个表格是把最常遇到的问题、原因和解决办法汇总起来的结果,可以说是一份“避坑地图”。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 利润表数据抽成母公司数据 | 提示词没限定“合并报表” | 在提示词里强制加“只取合并口径” |
| 金额单位串味 | 原文含万元、亿元混排 | 提示词统一换算并附单位示例 |
| 科目缺失时被“瞎补” | 提示词未要求缺失填null | 明确“缺失写null,禁止猜测” |
| 跨页表格字段断连 | PDF自身表格被强制分页 | 解析时勾选跨页合并或按表格块输出 |
| 解析结果包含大量乱码 | 扫描件质量太差 | 开启图像增强,并做二次OCR校对 |
| 大模型偶尔返回非JSON格式 | 模型没被明确约束 | 输出说明里给一个严格的JSON样例 |
| 批量任务中途报错 | 并发触发限量 | 调低并发数,分批执行 |
| 净利润算出来和年报差很多 | 取了母公司而非合并数 | 人工核对一次后锁定提示词口径 |
最值得重视的问题是第一行。我在测试阶段有几次抽取结果“看起来特别合理”,营收、净利润都能对上,但最后和年报核对时发现全是母公司报表的数据,合并报表里的数据明明在下一页。自从在提示词里加上“只取合并口径”后,准确率明显提高。
4.2 稳定性策略:从“能跑通”到“跑得稳”
方案跑通后,真正决定它能不能长期用下去的是稳定性。财报格式虽然大体统一,但每年都有个别公司做创新排版,时不时出现“科目名跨两行”“金额前有空格”“负数用括号表示”等变体。这些不会导致流程崩掉,但会悄悄把错误数据带进来。
我给自己定了几条铁律:第一,每次解析流程里必须带人工抽查步骤,每批至少抽3份与原始PDF人工比对关键指标;第二,大模型输出的每个数值都做范围校验,比如营收增长率超过100%或ROE大于100%就自动标记;第三,每周复盘一次新增的异常样本,把新的“说法”补充到提示词或解析配置里。
另外值得一提的是负数处理。旧准则下的财报里,部分科目用括号表示负数,比如财务费用是“(5000)”,如果不加处理,容易被解析成负号。我在代码节点里加了一步预处理:如果字段值是字符串且首尾带括号,统一转换为负数。这个细节看着小,但直接影响现金流量表净额的正负,差之毫厘谬以千里。
4.3 后续可以扩展的方向
这套方案稳定之后,我陆续给它加了几个附加能力,这里简单提一下,供有同样需求的人参考。
一是在Coze里加一个“异常波动作警”节点,对抽取回填的数据做同比环比计算,当某项指标变动幅度超过预设阈值时,自动在工作通知群里发一条消息。这等于把一个“只会抄数据”的工具,升级成了“会盯着财报异常”的助手。
二是把抽取结果接入可视化看板。文章开头提到的传统流程里,最容易被反复问的问题是“这个季度营收到底是多少”,数据分散在一堆Excel文件里。现在回填结果落到统一数据源后,我直接用图表组件做了一张总览看板,营收、利润、现金流趋势一图看全。
三是支持更多财报类型。目前跑通的是A股定期财报,但同样思路可以复用到港股、美股或基金定期报告的抽取。区别主要在于解析模板和提示词里的报表科目名称,底层链路完全不用动。
根据我个人的实操经验,最想给后来者的一句建议是:不要一上来就追求“全自动零人工”,那在财报这种复杂文档上不现实。先把流程跑起来,保留一个灵活的人工复核节点,抽样校验几批数据后,再逐步扩大自动化范围和批量规模。这样既不用天天提心吊胆担心数据出错,又能实打实地把年报季的工作时间从几天压缩到几小时。
如果哪天你也被几百页PDF折磨到怀疑人生,不妨按这套思路试一试。先把一份财报跑通,再慢慢增加字段、增加批量。5分钟搭出一个能用的流程,这句话不是夸张,是这套方案的实际体验。而你省下来的时间,值得花在更重要的分析工作上,而不是和PDF斗智斗勇。