年报财务数据提取
概览
从年报 PDF 中自动化提取结构化财务数据,历经 文本提取 → 清洗 → 解析 → 核查 → 换算 → 输出 六阶段流程。核心输出为带同比标注的 HTML 报告(亿元为单位,红涨绿跌)。
架构(v2 起采用"通用引擎 + 公司模板"分离设计):
skills/年报财务数据提取/
├── SKILL.md ← 本文档(含异常情况库 + 接入指南)
├── scripts/
│ ├── generate_report.py ← v1 单公司脚本(Pop Mart 版,参考实现)
│ └── generate_report_v2.py ← v2 通用引擎(从模板加载配置)
└── templates/
├── _schema.json ← 模板字段规范(JSON Schema)
├── _default_hk.json ← 港股默认模板(新公司接入起点)
├── nongfu_spring.json ← 农夫山泉 09633.HK 模板
└── popmart.json ← Pop Mart 09992.HK 模板
核心设计原则:
1. 公司特定逻辑(页眉过滤词、报表标题、BS结构、文本顺序)全部下沉到 JSON 模板
2. 解析引擎对所有公司通用,不包含任何 if company == 'xxx' 分支
3. 新公司接入 = 新增一个 JSON 模板文件,无需修改引擎代码
六阶段流程
阶段一:PDF 文本定位与提取
目标:精准定位三大报表所在页面,提取文本层数据。
# 1. 搜索报表标题关键词定位页码(用英文标题最稳定)
pdfkit.py search_text --input YEAR年报.pdf --find "PROFIT OR LOSS" --engine pymupdf
pdfkit.py search_text --input YEAR年报.pdf --find "FINANCIAL POSITION" --engine pymupdf
pdfkit.py search_text --input YEAR年报.pdf --find "CASH FLOWS" --engine pymupdf
# 2. 排除目录/章节引言页,取最前面的"报表标题页"
# 验证:单页提取后内容首行应为报表中文标题(如"綜合損益表")
# 3. 提取范围:[损益表起始-1, 损益表起始+8](10页,含缓冲,覆盖三大表+权益变动表)
pdfkit.py extract_text --input YEAR年报.pdf --pages "[107,108,109,110,111,112,113,114,115,116]" \
--format text --output YEAR_financial.txt
注意:pdfkit search_text 返回的 page 字段与 extract_text --pages 接受的索引基准一致
(两者都用同一编号系统,可直接复用)。--pages 接受 0-based 索引数组,需显式列出所有页号。
- 只提取文字层,非扫描件不需要 OCR
- 如遇加密 PDF,先
pdfkit.py decrypt解密 - 港股年报关键词:
PROFIT OR LOSS/FINANCIAL POSITION/CASH FLOWS(用英文最稳) - A股年报关键词可能为:
利润表/资产负债表/现金流量表
阶段二:文本行清洗(clean_lines)
目标:移除页眉页脚、保留所有数据行。
规则:
1. 移除 "--- Page N ---" 页面标记 → 替换为 '---PAGE---' 内部标记
2. 移除公司名、年报标题行(从模板 header_filters 加载)
3. 移除核数师报告干扰行(auditor_lines + auditor_signatures)
4. 移除 "Note"/"附註"、"RMB'000"/"人民幣千元" 等纯标记行
5. 保留空行作为条目分隔符
6. 【关键】不删除 1-3 位纯数字行 ← 这是 note 编号和小数值,曾导致大规模数据丢失
7. 仅移除单独出现的页码数字(紧跟 ---PAGE--- 的行)
阶段三:结构化解析(parse_statement_lines)
目标:将文本行组装为 {name_en, name_zh, note, value_current, value_previous} 结构化条目。
文本格式模式(双向兼容)
模板的 text_order 字段决定文本收集顺序:
模式 A: en_first(先英文后中文,如 Pop Mart)
Revenue ← 英文科目名(可能跨多行)
收益 ← 中文科目名(可能跨多行)
5 ← 附注编号
2,513,471 ← 当年值
1,683,434 ← 上年值
模式 B: zh_first(先中文后英文,如农夫山泉)
收益 ← 中文科目名
REVENUE ← 英文科目名
5 ← 附注编号
22,877,297 ← 当年值
24,021,041 ← 上年值
模式 C: auto(自适应,推荐用于新公司)
解析器检测当前行 is_pure_chinese,自动选择先中文或先英文
解析器关键规则
- 数字行判断:匹配
\(?\d[\d,]*\)?的模式,同时接受–/—/-(破折号 = 零值/不适用) - 附注编号识别:仅 1-2 位纯数字或带字母后缀(如
34(a)),也接受逗号分隔多引用(如16, 34(c)、5,8);3 位以上数字一律当作数值。字母后缀放宽为[a-z]{1,4}(如20(vii),小米 1810.HK 实测)——单字母正则\([a-z]\)会漏掉多字母附注导致科目名被污染。 - 节标题识别:模板
section_header_keywords集合(小写精确匹配)标记is_section_header=True - 合计行识别:含
total或總关键词的科目标为is_total=True - 多行名称合并:英文/中文名跨行时,通过空白行前后的语言特征判断连续性
阶段四:数据核查(强制)
按模板 balance_sheet_validation.type 选择验证策略:
4.1 资产负债表等式验证
类型 A: assets_eq_liab_plus_equity(标准结构,有 Total assets 行)
验证: Total assets == Total liabilities + Total equity
适用: A股、美股、Pop Mart 等多数公司
类型 B: net_assets_eq_equity(总资产减流动负债列示,无 Total assets 行)
验证: 非流动资产 + 流动资产 - 流动负债 - 非流动负债 = 净资产 = 权益总额
适用: 农夫山泉等部分港股
4.2 损益表连贯性验证
验证项:综合收益总额 == 净利润 + 其他全面收益(近似验证)
说明:因归属拆分可能存在微小舍入差异,允许 1% 以内偏差
4.3 现金流与资产负债表交叉验证
验证项:现金流量表期末现金 vs 资产负债表现金科目
查找策略:按模板 cash_item_aliases 候选列表逐个 fallback 查找
- 2020年农夫山泉: "Cash and cash equivalents"
- 2021+年农夫山泉: "Cash and bank balances"(会计政策变更后)
异常处理:
1. 两者相等 → ✓ 通过
2. CF表内有"as stated in the statement of financial position"调节项且等于BS现金 → ✓ 经调节后通过
3. 差额 = BS 中的 "Long-term bank deposits" → ⚠️ 提示长期存款调节项
4. 其他情况 → ⚠️ 提示差额,建议人工核查 CF 表分析段
4.4 异常波动预警
触发条件:同比变动 > 30%
预警动作:在报表中标注 "⚠️ 变动 >30%"
检查项:排除 IPO 融资、重大并购、会计准则变更等合理原因
行业偏离检测:与行业均值偏离 > 50% 时启动人工复核
4.5 数据缺失处理
单年缺失:该格显示 "-",邻近年份有值时标记 "⚠️ 数据待补"
跨年口径不一致:
- 同名科目取最早出现年份的命名
- 新增科目在无数据的年份显示 "-"
- 科目名变更在备注栏标注 "原名: XXX"
- 现金等关键科目用 cash_item_aliases 候选列表 fallback 查找
阶段五:单位换算
默认流程:原始数据为 RMB'000(千元)→ 除以 100,000 → 亿元(保留 2 位小数)
def to_yi(val):
"""千元 → 亿元: val / 100000"""
return val / 100000 if val is not None else None
- 列标题同步更新为"人民币亿元"
- 同比百分比和变动金额同步换算
阶段六:HTML 报告生成
输出结构:按年份分组,每年依次呈现三张报表。
表格布局
| 科目名称 | 2020年 | 2019年 |
|-----------------|----------------|----------------|
| 物业厂房设备 | 2.38 | 1.04 | ← 数据行
| | ▲ 130.1% | - | ← 同比%行
| | +1.34 | - | ← 变动金额行(BS专有)
| | 0.6% | - | ← 占比行(蓝,BS置于变动金额下方)
占比指标规范(2026-09-09 港交所项目新增,用户确认)
范围:仅综合损益表与综合资产负债表默认加占比;综合全面收益表、现金流量表默认不加。
分母口径:
- 损益表:各科目当年值 ÷ 当年营业总收入(港交所口径为"收入及其他收益";A股即"营业总收入")。每股盈利(EPS)行不出占比。
- 资产负债表:三段各自独立——资产段科目 ÷ 资产总额、负债段科目 ÷ 负债总额、权益段科目 ÷ 权益总额。
- 不出占比的行:合计行(总资产/总负债/权益总额/负债及权益总额——自身恒为 100%)与信息行(如流动资产净值——非构成项)。
呈现位置与样式:
- 当年列小字区最底部:损益表为 数据→同比→占比;BS 为 数据→同比→变动金额→占比(用户确认:BS 中占比紧邻变动金额下方)。
- 字体字号与同比行完全一致(13px),文字颜色蓝色 #1e6091。
- 格式:一位小数 xx.x%;负占比沿用括号风格 (x.x%)。
- HTML 报告的口径说明区须注明占比分母口径,避免日后回看歧义。
颜色规范(中国A股惯例)
同比变动方向基于绝对值变动判断(使收入增长和成本增长均显示为红色):
- 变动方向 =
abs(curr) > abs(prev)→ 增长 → 红色#e17055▲ - 变动方向 =
abs(curr) < abs(prev)→ 减少 → 绿色#00b894▼ - 符号翻转特殊处理:
prev > 0, curr < 0(正→负)→ 强制减少 → 绿色 ▼prev < 0, curr > 0(负→正)→ 强制增长 → 红色 ▲- 合计行背景:淡蓝色
#e8f4fd - 数据值颜色:各科目(含开支类)数据值统一为黑色,不再用蓝色区分开支类(2026-09-09 用户确认取消"蓝开支")。支出仍以括号
( )表示负数。
字号与字体
- 数据数值:16px,DIN 字体系列
- 合计行:17px 加粗
- 同比注释:13px(常规字重,不加粗)
- 变动金额:12px(常规字重,不加粗)
- 占比行:13px(与同比完全一致),蓝色
#1e6091(常规字重,不加粗)
> 注:同比、占比、变动金额三类小字不加粗(2026-09-09 用户确认取消加粗,与数据行靠字号差异区分层次)。
异常情况分类与处理策略
类别一:数据格式不一致
| 异常 | 触发场景 | 处理策略 | 模板字段 |
|---|---|---|---|
| 文本顺序差异 | Pop Mart 先英文后中文;农夫山泉先中文后英文 | 解析器双向兼容,模板 text_order 字段控制 |
text_order: en_first/zh_first/auto |
| 报表标题命名差异 | 港股 BS 可能叫"綜合財務狀況表"或"資產負債表" | 模板 statement_titles 配置多个候选关键词 |
statement_titles.{表}.start_keywords[] |
| 损益表与OCI是否合并 | Pop Mart 合并(一张表);农夫山泉分开(两张表) | 模板 income_includes_oci 控制;extract_income 的结束关键词设为 BS 起始 |
income_includes_oci: true/false |
| 页眉页脚差异 | 不同公司年报页眉含不同公司名、年报标题格式 | 模板 header_filters 集中配置所有过滤词 |
header_filters.* |
类别二:字段缺失与结构差异
| 异常 | 触发场景 | 处理策略 | 模板字段 |
|---|---|---|---|
| BS 无 Total assets 行 | 农夫山泉采用"总资产减流动负债"列示 | 模板 balance_sheet_style: net_assets,验证逻辑切换为净资产等式 |
balance_sheet_style + balance_sheet_validation.type |
| 节标题关键词差异 | 不同公司节标题措辞略有不同 | 模板 section_header_keywords 集合配置 |
section_header_keywords[] |
| 董事签名干扰 | BS 末尾常有董事签名(中英文) | 模板 director_signatures 列表,解析时跳过 |
director_signatures[] |
| 核数师报告末页干扰 | 损益表前一页是核数师报告末页,含审计师姓名/日期/责任段 | 模板 auditor_lines + auditor_signatures 双重过滤 |
header_filters.auditor_lines/signatures |
类别三:跨年会计政策变更
| 异常 | 触发场景 | 处理策略 | 模板字段 |
|---|---|---|---|
| 现金科目名跨年变更 | 农夫山泉 2020 用 "Cash and cash equivalents",2021+ 拆分为 "Cash and bank balances" + "Long-term bank deposits" | 模板 cash_item_aliases 候选列表,按优先级 fallback 查找 |
cash_item_aliases[] |
| CF 期末现金 ≠ BS 现金 | 因含长期/三月以上定期存款等调节项 | CF 表末尾"結餘分析"段做调节;验证逻辑识别 Long-term bank deposits 等调节项 | cashflow_reconciliation_keyword |
| 新增/取消科目 | 业务范围变化导致科目增减 | 数据缺失处理:无数据年份显示 "-",邻近年份有值时标记"⚠️ 数据待补" | (内置逻辑) |
类别四:PDF 提取层异常
| 异常 | 触发场景 | 处理策略 |
|---|---|---|
| 扫描件无文字层 | 旧年报或部分页是扫描图片 | pdfkit.py extract_text --ocr_fallback 启用 OCR |
| 文字层乱码(CID 映射损坏) | 字体 ToUnicode 缺失,提取出"伪汉字"(如「䌪䪍㟺㔺ⷭ」)而非 \ufffd 替换符;数字通常仍正常 | 文字层提取全部失效 → 走 OCR 兜底(见「类别八」);页码用 PDF 书签定位 |
| 加密 PDF | 部分年报有密码保护 | pdfkit.py decrypt 先解密 |
| 跨页表格断裂 | 一张表跨 2-3 页,中间有页眉页脚 | clean_lines 已处理,保留空行作分隔符 |
| 页码索引基准混淆 | pdfkit search_text 返回的 page 与 extract_text --pages 索引基准 |
两者用同一编号系统,可直接复用;但 --pages 接受 0-based 数组,需显式列出所有页号 |
类别五:解析器边缘情况
| 异常 | 触发场景 | 处理策略 |
|---|---|---|
| clean_lines 误删 1-3 位数字 | 股本、note 编号等被当作页码删除 | 不删除纯数字行,改为解析器上下文判断 |
| 破折号 "–" 未被识别为值 | 含零值科目整行被跳过 | is_number_or_dash 接受 –/—/- |
| 3 位数字被当作 note 编号 | 减值拨回科目数值错位 | is_note_line 仅匹配 1-2 位数字;字母后缀放宽为 [a-z]{1,4}(^\d{1,2}(\([a-z]{1,4}\))?$,支持 20(vii)),逗号多引用 ^\d{1,2},\s*\d{1,2}(\([a-z]{1,4}\))?$ |
| 带逗号数值被误判为"逗号分隔多附注" | 3,873 等数值被当作 note 跳过 |
逗号分隔 note 要求两段均≤2位:^\d{1,2},\s*\d{1,2}(\([a-z]\))?$ |
| BS/CF 末尾注释行被解析为条目 | "The above consolidated balance sheet..."、以上綜合資產負債表、以上綜合現金流量表應與隨附附註 及后续董事签名、页码被误解析 |
extract 阶段遇到上述注释关键词即 break 停止(v1 与 v2 均已内置;模板 end_keywords 亦建议加入) |
| 跨行中文名被空白行截断 | 联营公司投资等科目英文名残留 | 空白行 + 前瞻纯中文检测 → 继续合并 |
| 逗号分隔多附注未被识别 | 现金流量表租赁负债付款被命名为 "16, 34(c)" | is_note_line 增加 ^\d{1,2},\s*\d+(\([a-z]\))?$ 匹配 |
| 单位换算 ÷10000 | 数值放大 10 倍 | 千元 → 亿元 = ÷100000(不是 ÷10000) |
| '資產' in zh 子串匹配 | 无形资产等误渲染为节标题 | 改用 is_section_header 精确标志 |
类别六:招股书附录一(会计师报告)vs 年报(2026-09-09 蜜雪招股章程实测)
招股书历史财务资料与年报结构差异大,v2 引擎不适用,需独立多栏解析器(参考实现:E:/AIstorage/investment/mxjt/mixue/build_prospectus.py):
- 多期间多栏布局:损益/OCI/CF 为 5 列(3 个年度 + 2 个 9M 期间),BS 为 4 列;数值可能一行多个或一行一个,按 token 流收集。行完成判据 = "首位是附注则需 ncols+1 个 token,否则 ncols 个"。
- 附注编号带字母:如
15(a)/15(b)/15(c),需并入 token 数字类集合,否则被当作文字行导致空科目。 - 无科目名的孤立数值行:PDF 文本层小计行常无标签(如 CF"税前利润经调整后"、P&L"应占利润合计"、BS"母公司拥有人应占权益合计"),需按上下文补名,勿丢弃。
- 点线引导符可能只有单个「.」(如「扣除税項 .」),DOTS 正则放宽为 1 个以上
(\s*\.\s*){1,}$。 - 科目查找必须全名精确匹配:「流動資產總值」是「非流動資產總值」的子串、「資產淨值」是「流動資產淨值」的子串,子串匹配会取错行。
- 独立表头行(非流動資產/權益/以下各方應佔:等)与多行科目名前缀难区分,用精确表头名单先行切分。
- 每股收益单位为人民币元(非千元),不能 ÷100000。
- 校验仍全部适用且推荐:BS 等式、P&L 利润勾稽、OCI 勾稽、CF 期初+净增+汇兑=期末、CF 期末现金=BS 现金(仅年度列可比,9M 列 BS 无对应时点)。
类别七:港交所式多栏 BS 年报(2026-09-09 香港交易所 00388 六年实测)
港交所(及部分采用同款列示方式的港股公司)年报结构与 v2 引擎假设差异大,需专用解析器。参考实现:E:/AIstorage/investment/gjs/hkex/parse_hkex.py(解析+验证)+ render_hkex.py(HTML)。
- 六列 BS:綜合財務狀況表每科目为"流動/非流動/合計 × 当年/上年"6 值,权益段仅"合計"2 值;段标记行「資產/負債/股本權益」切换值数期望。行级附注判定:值 token 数 = 期望值数+1 → 首 token 为附注。
- 中文单语 + 百萬元:中文报表在 PDF 独立区段(2020 年报在 p128-134 区段);单位"百萬港元"→ 亿元 = ÷100;EPS 形如 "9.11元" 须保持字符串不换算。
- 两列表附注计数规则:值 token 数==3 且首位为 1-2 位纯数字 → 首位是附注(如「收入 5 16,835 13,565」「香港交易所股東 46 11,505 9,391」「就非控股權益行使的出售選擇權所支付款項 39 (51) –」——末例首位 39 实为附注,已用财务活动净流出勾稽验证)。
- 停止标志禁忌:2023+ 年报把"(非HKFRS計量項目)"标注移入 CF 表内首行,"非HKFRS"不能用作停止标志;安全标志为「主要業務活動之現金流量」(带右引号」)、「以上綜合」、「綜合財務報表附註」。
- 表头年份粘连:y 聚类后表头年份行成单 token("20232022"),用
re.fullmatch(r'(\d{4})+', text)过滤;「附註」「百萬元」表头行整行过滤。 - 科目名逐年微调:其他全面收益╱(虧損)→ 其他全面(虧損)╱收益;折舊及攤銷 → 折舊、攤銷及減值;EBITDA → EBITDA(非HKFRS計量項目)。验证与查找用前缀匹配(startswith('其他全面'))而非全等/子串(防"以公平值計量及列入其他全面收益的財務資產…"误配)。
- 无名值行:P&L「(4,439) (3,997)」为營運支出總額(前条目为其他營運支出时补名);CF「現金及等同現金項目分析」段末确认行与期末现金重复 → 丢弃。
- CF 与 BS 现金口径差:CF 期末现金是公司自有资金口径,BS「現金及等同現金項目」含结算参与者保证金相关现金(差额约 1,160亿–1,700亿港元),非错误,勾稽说明须解释(附註21 分析段)。
- 推荐解析架构:PyMuPDF
get_text("words")+ y 坐标聚类(容差 4pt)成逻辑行 → 行级 token 状态机;比纯文本流更抗列错位。附注样式 note_like:含数字且无中文且匹配[\d,()\[\]a-z\-–]+。 - 勾稽验证集(六年 66 项全通过):BS 等式(两年)×2、损益(溢利=除税前+税项;EBITDA=收入减支出+营运支出总额;营运溢利=EBITDA+折旧摊销;除税前=营运溢利+区间中间项求和)、全面收益(總額=溢利+其他全面收益)、CF(期初+净变动+汇兑=期末)。
类别八:文字层乱码年报 → OCR 兜底(小米 1810.HK 五年实测,2026-09-09)
触发场景:PDF 有文字层但字体 ToUnicode 映射损坏,中英文提取为"伪汉字"(如「䌪䪍㟺㔺ⷭ」),数字仍正常。search_text 搜不到关键词、extract_text 输出乱码——v2 引擎的"模板 + 文本流"路线完全不可用。
完整流程(参考实现脚本见 scripts/parse_ocr_text.py + scripts/validate_ocr_data.py):
- 乱码检测:提取一页看字符——若为 CJK 扩展 A(U+3400-U+4DBF)/私有区(U+E000-U+F8FF) 的伪汉字(非 \ufffd),即为 CID 映射损坏。文字层方案放弃。
- 页码定位:乱码时改用 PyMuPDF
doc.get_toc()读 PDF 书签定位三大报表页码(书签是结构化元数据,不受乱码影响)。 - OCR 提取:用
pdf-glmocr-hybrid技能,--pages指定报表页(避开全 PDF 慢扫)。参数num_ctx=16384(密表早停根因)+num_predict=8192+ 简短中文 prompt。 - 解析 OCR 输出(v2 引擎不适用,需独立解析器):
- OCR 输出格式不统一:损益表常为 Markdown 表格(| 收入 | 5 | 457,286,687 | ... |),OCI/BS/CF 为空格分隔文本流(逐行/同行混合)。解析器须兼容两种。
- 行内解析优先 + 跨行缓冲兜底:先从行尾收 2 个数值 token、剥附注;行内不足双值时 tokens 进跨行缓冲,凑齐双值再 flush(处理 OCI 逐行格式)。
- 纯数字附注陷阱:附注15/26是纯数字,token 流模式下会被误判为数值——必须"行内从尾部收值"优先,杜绝附注混入值列。
- 节标题做 section 标记:BS 的「保修撥備/定期銀行存款/借款」在流动与非流动段同名,靠 section 隔离,跨年对照用(section, name)键,避免同名科目错位对照。
- OCR 噪声:temperature=0 下偶发整块/行级重复(按"最长实质块 + 条目级整段去重"处理);系统性误读(「權益法入賬」→「法人賬」)全局替换修正。 - 验证(191 项全通过):损益逐级加总、OCI 加总、BS 分段加总+总等式、CF 三段加总+现金勾稽、跨年对照(年报 N 的上年列 vs 年报 N-1 的当年列,配科目名映射表处理重命名)。跨年对照是 OCR 质量的终极试金石——两次独立 OCR 结果一致即高置信。
- OCR 漏读补漏:单值行(如「因攤薄 —」)可能漏读一值,用段加总勾稽反推差额 + PyMuPDF
get_text("words")数字层核对 PDF 原文补正。
科目名跨年映射(小米实测,同理适用于其他公司):物業及設備→物業、廠房及設備;長期/短期銀行存款→定期銀行存款;貿易應收款項→貿易應收款項及應收票據;保修撥備→撥備;年內利潤↔年度利潤(2023 用「年內」);資本開支→購買物業、廠房及設備、土地使用權及無形資產。
公司模板配置规范
模板文件位于 templates/{company_id}.json,字段定义见 templates/_schema.json。
必填字段
| 字段 | 类型 | 说明 |
|---|---|---|
company_id |
string | 公司唯一标识(文件名前缀) |
company_name |
string | 公司中文全称 |
company_name_en |
string | 公司英文全称 |
header_filters |
object | 页眉页脚过滤词集合 |
statement_titles |
object | 三大报表起止关键词 |
balance_sheet_style |
enum | standard / net_assets |
text_order |
enum | en_first / zh_first / auto |
关键可选字段
| 字段 | 默认值 | 说明 |
|---|---|---|
income_includes_oci |
false | 损益表与OCI是否合并 |
cash_item_aliases |
["Cash and cash equivalents"] |
现金科目候选列表(fallback 查找) |
cashflow_reconciliation_keyword |
"ANALYSIS OF BALANCES" |
CF 表调节段标题(遇到停止解析) |
balance_sheet_validation.type |
assets_eq_liab_plus_equity |
验证策略 |
director_signatures |
[] |
董事签名列表(解析时跳过) |
extraction_page_offset |
1 | 损益表起始页的前置缓冲页数 |
新公司接入指南(5 步)
Step 1:复制默认模板
cp templates/_default_hk.json templates/{company_id}.json
Step 2:填充公司基本信息
编辑 {company_id}.json,填入 company_name、company_name_en、stock_code、market。
Step 3:定位三大报表页码
PDFKIT="C:/Users/wangs/.workbuddy/plugins/marketplaces/cb_teams_marketplace/plugins/document-skills/skills/pdfkit-py/scripts/pdfkit.py"
PY="C:/Users/wangs/.workbuddy/binaries/python/envs/default/Scripts/python.exe"
# 搜索三大报表标题(用英文最稳定)
for KW in "PROFIT OR LOSS" "FINANCIAL POSITION" "CASH FLOWS"; do
"$PY" "$PDFKIT" search_text --input 年报.pdf --find "$KW" --engine pymupdf
done
记录损益表起始页号,验证:单页提取后内容首行应为报表中文标题。
Step 4:实测文本格式与 BS 结构
# 提取损益表起始页,观察科目顺序
"$PY" "$PDFKIT" extract_text --input 年报.pdf --pages "[损益表起始页]" --format text
判断:
- 文本顺序:科目名是先中文还是先英文?→ 设置 text_order
- BS 结构:搜索 "Total assets",找到 → balance_sheet_style: standard;找不到 → net_assets
- 损益表与OCI:搜索 "COMPREHENSIVE INCOME",与损益表同页 → income_includes_oci: true
Step 5:填充模板并测试
将 Step 2-4 的发现填入模板 JSON,然后运行:
"$PY" scripts/generate_report_v2.py \
--template {company_id} \
--years 2020,2021,2022,2023,2024,2025 \
--input-dir /path/to/extracted_txt/ \
--output /path/to/report.html
检查输出:
- [ ] 三大报表科目数量在预期范围(Income ~20, Balance ~35, Cashflow ~50)
- [ ] 资产负债表等式验证全部通过 ✓
- [ ] 现金流勾稽提示合理(差额 = 长期存款等调节项)
- [ ] 抽查 5 个关键科目(营收/净利润/总资产或净资产/现金)与 PDF 原文一致
执行检查清单
在最终输出前,逐项确认:
- [ ] 阶段一:所有 PDF 的三大报表均已定位并提取
- [ ] 阶段二:clean_lines 未误删数据行(抽查 3-5 行数值是否与 PDF 原文一致)
- [ ] 阶段三:每张表解析后的科目数量在预期范围内(~20 + ~35 + ~50)
- [ ] 阶段四-1:所有年份资产负债表等式验证通过(按 balance_sheet_validation.type)
- [ ] 阶段四-3:所有年份 CF 期末现金与 BS 现金勾稽合理(相等或差额可解释)
- [ ] 阶段四-4:同比 >±30% 的科目已标注
- [ ] 阶段五:单位标签已从"千元"改为"亿元"
- [ ] 阶段六:HTML 无外部依赖、响应式正常、打印样式正常
- [ ] 抽查 5 个关键科目与 PDF 原文一致
依赖
pdfkit-py:PDF 文本提取(PyMuPDF 底层)openpyxl:可选的 Excel 输出- Python 3.10+
脚本
scripts/generate_report.py:v1 单公司脚本(Pop Mart 版,参考实现,含完整 HTML 生成器)scripts/generate_report_v2.py:v2 通用引擎(从模板加载配置,含解析+验证,HTML 生成需复用 v1 函数并传入 template)scripts/parse_a_share.py:A 股独立解析器(参考实现:东鹏饮料 605499.SH 5 年合并报表)scripts/generate_a_share_report.py:A 股 HTML 报告生成器(红涨绿跌/DIN字体/亿元单位)scripts/parse_ocr_text.py:文字层乱码年报的 OCR 文本流解析器(参考实现:小米 1810.HK 五年;行内解析优先+跨行缓冲+节标题 section 标记+条目级去重,见「类别八」)scripts/validate_ocr_data.py:OCR 版数据验证器(参考实现:小米 1810.HK;损益/OCI/BS/CF 全套勾稽 + 跨年对照 + 科目名映射表)templates/_schema.json:模板字段 JSON Schema 规范templates/_default_hk.json:港股默认模板(新公司接入起点)templates/_default_a_share.json:A 股默认模板(新公司接入起点,含 A 股特定字段)templates/nongfu_spring.json:农夫山泉模板(zh_first + net_assets BS + 现金科目 fallback)templates/popmart.json:Pop Mart 模板(en_first + standard BS)A股年报处理指南.md:A 股年报解析详细工作流(vs 港股差异、智能跨行合并、节标题处理、防死循环等)
已知限制与未来扩展
- HTML 生成器尚未完全模板化:v2 引擎目前只输出数据摘要,完整 HTML 生成需复用 v1 函数并传入 template。下一步可将 v1 的
generate_html/build_table/build_bs_check/build_cf_check改造为接受 template 参数。 - A股支持已验证(2026-08-19 完成东鹏饮料 5 年合并报表提取):A 股年报结构与港股差异较大,详见
A股年报处理指南.md。当前实现使用独立脚本scripts/parse_a_share.py,未集成到 v2 通用引擎。下一步可基于_default_a_share.json将 A 股字段下沉到 v2 引擎。 - 所有者权益变动表结构特殊:A 股该表为多列横向结构(实收资本/其他权益工具/资本公积/...),PDF 提取后表格列结构被打散,当前实现仅在 JSON 保留 raw_lines,不在 HTML 呈现。后续可专门写一个
parse_equity_statement()处理此结构。 - 美股支持待验证:美股 10-K 报表结构与港股差异较大(如无 OCI 单独表、BS 用 "Balance Sheet" 而非 "Statement of Financial Position"),需新增
_default_us.json模板。 - 多币种支持:当前默认 RMB'000 → 亿元,港股部分公司用 HKD'000 或 USD'000,需扩展
currency字段为对象含汇率换算。A 股单位为"元",换算系数为 ÷100,000,000(已在_default_a_share.json的unit_to_yi_divisor字段记录)。