# yudao-annual-report-cli **Repository Path**: ziyucoding/yudao-annual-report-cli ## Basic Information - **Project Name**: yudao-annual-report-cli - **Description**: 年报投研系统 - **Primary Language**: Python - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-07-10 - **Last Updated**: 2026-07-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 年报投研系统 Python CLI,用于把带可读文本层的年报 PDF 转换为结构化三表数据、规则化财务风险结论、PNG 图表和单文件 HTML 年报分析。 当前主流程是确定性解析和规则研判:`pdfplumber` 抽取文本,正则解析关键财务科目,`matplotlib` 生成静态 PNG,HTML 报告内嵌图表。当前不包含扫描件 OCR、LLM 分析、前端交互式图表或外部模板渲染。 ## 当前能力 - 一键入口:一句运行指令 + `data/raw/` 年报文件名即可跑完整流程。 - PDF 文本解析:用报表标题和典型表内科目共同定位三张合并报表,并按报表标题边界裁剪从命中页起最多 8 页文本,避免误命中目录或混入公司报表。 - 结构化输出:金额保留年报原始口径,单位写入 `meta.amount_unit`;除关键科目外,提取流动资产/流动负债合计和购建长期资产支付现金。 - 指标计算:覆盖盈利、现金流、杠杆、增长、周转和运营指标;周转天数、ROA/ROE 和总资产周转率使用双期平均余额,流动性比率使用流动资产/流动负债合计。 - 风险研判:生成应收账款风险、存货风险、盈利质量、资本开支、杠杆风险 5 类规则结论。 - 图表生成:数据充足时生成 6 张静态 PNG 图表;单张图失败会跳过并清理同名旧图,避免 HTML 引用过期结果。金额坐标轴统一展示为亿元,图表数值标签按亿元/万元/元自适应展示。 - HTML 报告:输出摘要卡片、关键图表、风险结论、三表勾稽和明细表,PNG 以 base64 内嵌。 `oneclick` 的自然语言指令会写入 findings JSON,便于追溯本次运行目的;HTML 年报分析头部不展示该指令,当前规则引擎也不会根据指令内容调整判断逻辑。 ## 流水线 ```text PDF 年报 -> 数据提取:pdfplumber 文本抽取 + 合并报表页定位 + 正则解析 -> 图表生成:structured JSON -> matplotlib PNG -> 财务研判:指标计算 + 规则风险分析 -> 报告生成:findings JSON + 图表 -> 单文件 HTML ``` 四个阶段由 `orchestrator/workflow.py` 编排: 1. `run_extraction()`:PDF -> `data/extracted/*_structured.json` 2. `run_chart_generation()`:structured JSON -> `reports/assets/{company}/{company}_{year}年报_*.png`(最多 6 张) 3. `run_analysis()`:structured JSON -> `data/analysis/*_findings.json` 4. `run_report_generation()`:findings JSON + chart assets -> `reports/{company}_{year}年报分析.html` 单独运行 `report` 时,会从 findings JSON 的 `source_data_path` 定位 structured JSON;未传入图表目录时会重新生成图表。 ## 快速开始 ```bash python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python main.py validate ``` 也可以运行跨平台安装脚本 `python setup.py`;macOS/Linux 可使用 `./setup.sh`。两者都会创建 `venv`、安装依赖、初始化 `.env` 和必要目录,并在最后执行环境校验。 将文本型年报 PDF 放入 `data/raw/`: ```text data/raw/ ├── 宁德时代2025年年度报告.pdf ├── 比亚迪2025年年度报告.pdf ├── 海康威视2025年年度报告.pdf └── 大华股份2025年年度报告.pdf ``` 一键运行: ```bash python main.py oneclick "生成年报分析,重点关注盈利质量、现金流和偿债风险" 宁德时代2025年年度报告.pdf ``` 完整流程也可以用 PDF 路径运行: ```bash python main.py process data/raw/宁德时代2025年年度报告.pdf ``` 分阶段运行: ```bash python main.py extract data/raw/宁德时代2025年年度报告.pdf python main.py analyze data/extracted/宁德时代2025年年度报告_structured.json python main.py report data/analysis/宁德时代2025年年度报告_findings.json ``` `oneclick` 的年报参数支持现有 PDF 路径、`data/raw/` 下的完整文件名、不带 `.pdf` 的 stem,以及能唯一匹配的局部名称。局部匹配不区分大小写并按字面量处理;匹配到多个文件时必须改用完整文件名。`--skip-extraction` 只复用按 PDF stem 推导出的默认 structured JSON,不会搜索任意自定义输出文件。 ## CLI 命令 | 命令 | 作用 | 主要输入 | 主要输出 | | --- | --- | --- | --- | | `oneclick` | 从 `data/raw/` 定位年报并跑完整流程,同时记录本次指令 | 指令 + 年报名 | HTML 年报分析 | | `process` | 用 PDF 路径跑完整流程 | PDF 路径 | HTML 年报分析 | | `extract` | 解析 PDF 和三张合并报表关键科目 | PDF 路径 | structured JSON | | `analyze` | 计算指标并生成规则风险结论 | structured JSON | findings JSON | | `report` | 根据 findings JSON 生成图表和 HTML | findings JSON | HTML 年报分析 | | `validate` | 按当前配置检查依赖和必要目录 | 无 | 终端诊断;发现问题时退出码为 1 | 常用选项: - `--output/-o`:指定 JSON 或 HTML 输出路径。 - `--skip-extraction`:在 `oneclick` 或 `process` 中跳过 PDF 解析,复用同名 structured JSON。 ## 项目结构 ```text yudao-annual-report-cli/ ├── main.py # Click CLI 入口 ├── config/ │ └── settings.py # 路径和日志配置 ├── extractor/ │ ├── pdf_parser.py # pdfplumber 定位合并报表页并提取文本 │ ├── text_parser.py # 文本正则解析器 │ └── validator.py # 会计恒等式和字段完整性验证 ├── analyzer/ │ ├── amounts.py # 金额单位换算与展示格式 │ ├── metrics.py # 财务指标计算 │ └── risk_analyzer.py # 规则化风险研判 ├── visualizer/ │ └── chart_generator.py # matplotlib PNG 图表生成器 ├── reporter/ │ └── html_builder.py # 单文件 HTML 年报分析生成器 ├── orchestrator/ │ ├── workflow.py # 四阶段流程编排 │ ├── paths.py # 输出文件名安全化 │ └── validator.py # 环境验证 ├── tests/ │ └── test_basic.py # 当前 pytest 测试集 ├── data/ │ ├── raw/ # 输入 PDF │ ├── extracted/ # structured JSON │ └── analysis/ # findings JSON └── reports/ ├── assets/{company}/ # PNG 图表 └── *年报分析.html ``` ## 数据契约 ### Structured JSON `data/extracted/{pdf_stem}_structured.json` ```json { "meta": { "company_name": "宁德时代", "report_year": 2025, "amount_unit": "千元", "extract_time": "2026-07-09T14:13:42.846000", "pdf_path": "data/raw/宁德时代2025年年度报告.pdf" }, "balance_sheet": { "assets": { "current_assets": { "cash": {"current": 333512927.0, "prior": 303511993.0, "matched_keyword": "货币资金"}, "total": {"current": 638481543.0, "prior": 510142089.0, "matched_keyword": "流动资产合计"} }, "total": {"current": 974827544.0, "prior": 786658123.0, "matched_keyword": "资产总计"} }, "liabilities": { "current_liabilities": { "total": {"current": 399625988.0, "prior": 317171534.0, "matched_keyword": "流动负债合计"} }, "total": {"current": 603801220.0, "prior": 513201949.0, "matched_keyword": "负债合计"} } }, "income_statement": { "revenue": {"current": 423701834.0, "prior": 362012554.0, "matched_keyword": "营业收入"} }, "cash_flow_statement": { "operating_cash_flow": {"current": 133219982.0, "prior": 96990345.0, "matched_keyword": "经营活动产生的现金流量净额"}, "capital_expenditure": {"current": 42344558.0, "prior": 31179943.0, "matched_keyword": "购建固定资产、无形资产和其他长"} } } ``` 结构化 JSON 中的金额不做重缩放;指标计算直接使用原始口径,展示层通过 `analyzer.amounts` 转换。当前工作区已验证样例的单位: - 宁德时代:`千元` - 比亚迪:`千元` - 海康威视:`元` - 大华股份:`元` ### Findings JSON `data/analysis/{pdf_stem}_findings.json` ```json { "meta": {"company_name": "宁德时代", "report_year": 2025, "amount_unit": "千元"}, "source_data_path": "data/extracted/宁德时代2025年年度报告_structured.json", "instruction": "生成年报分析,重点关注盈利质量、现金流和偿债风险", "generated_at": "2026-07-09T14:16:54.449000", "metrics": { "profitability_ratios": {"net_margin": 0.1812}, "cash_flow_ratios": {"cash_to_profit_ratio": 1.7349} }, "findings": [ { "category": "盈利质量", "risk_level": "低", "title": "净利润vs经营现金流", "description": "经营现金流(1,332.20亿元)充沛,是净利润(767.86亿元)的1.73倍,盈利质量优秀,现金回流能力强。", "data_evidence": {"净利润": "767.86亿元", "经营活动现金流": "1,332.20亿元"} } ] } ``` `generated_at` 保留精确时间用于审计;HTML 展示的报告生成日期只保留 `YYYY-MM-DD`。 ## 指标与风险口径 - 增长率为 `(当期 - 上期) / abs(上期)`;上期为 0 或任一期缺失时返回 `null`。 - 应收账款周转天数使用平均应收账款 / 当期营业收入,存货周转天数使用平均存货 / 当期营业成本;ROA、ROE 和总资产周转率也使用双期平均余额。上期余额缺失时退回当期余额。 - 流动比率、速动比率和现金比率分别使用流动资产合计、扣除存货后的流动资产、货币资金作为分子,流动负债合计作为分母。 - 应收账款和存货风险按“科目增速减营收增速”的百分点差判断,阈值会随营收增速绝对值调整,避免负增长场景被乘法规则误判;数据缺失时结论为中风险并注明无法判断。 - 现金利润比只在净利润为正时计算;盈利质量规则会分别处理盈利、亏损、盈亏平衡和数据缺失场景。 - 资本开支强度使用现金流量表的“购建固定资产、无形资产和其他长期资产支付的现金 / 营业收入”。超过 15% 为中风险,现金支出为负或数据缺失时标记为中风险并要求核对;固定资产账面净变动只作为证据,不代替资本开支。 - 杠杆风险按资产负债率判断:`> 70%` 为高风险,`> 60%` 且 `<= 70%` 为中风险,其余为低风险;数据缺失时为中风险。 ## 图表与报告 数据完整时,每家公司生成以下 6 张 PNG 图表: - 营业收入与净利润对比 - 盈利能力分析 - 经营现金流 vs 净利润 - 资产结构分析 - 应收账款与存货分析 - 财务杠杆分析 图表保存到 `reports/assets/{company}/`,文件名格式为 `{company}_{year}年报_{chart_name}.png`。公司名、年份和图表名中的路径分隔符及文件系统非法字符会替换为 `_`。生成带年份文件名时,`ChartGenerator` 会尝试清理同公司旧版无年份图表;每张图生成前也会删除同名旧文件,生成失败后保持该文件不存在。HTML 只内嵌本次实际存在的 PNG,因此图表数量可能少于 6;报告本身通过 base64 内嵌图片,不依赖外部前端资源。 ## 单位规则 - `WorkflowOrchestrator._detect_amount_unit()` 从报表文本中的 `单位:...` 识别 `元`、`千元`、`万元`、`亿元`,默认值为 `千元`。 - structured JSON 保留原始数值和原始单位,不做二次缩放。 - HTML 摘要、三表明细、风险描述和风险证据固定展示为亿元。 - 图表金额坐标轴展示为亿元;图表数值标签通过 `format_compact_amount()` 按亿元、万元或元自适应展示。 - 用户可见金额和百分比数值统一保留 2 位小数。 - 展示层遇到 `meta.amount_unit` 中的未知单位会直接报错,不能静默按默认“千元”换算。 - 新增用户可见金额格式时,应优先复用 `analyzer.amounts`,避免各模块重复写单位换算逻辑。 ## 配置与环境校验 `config/settings.py` 从 `.env` 读取以下配置,环境变量名称不区分大小写: - `DATA_RAW_DIR`、`DATA_EXTRACTED_DIR`、`DATA_ANALYSIS_DIR` - `REPORTS_DIR` - `LOG_LEVEL`、`LOG_FILE` 业务命令通过 `load_settings()` 创建缺失的数据、报告和日志父目录。`python main.py validate` 本身不会创建目录,而是检查当前配置指向的 raw、extracted、analysis、reports 目录及主流程依赖;任一项缺失时输出问题列表并以退出码 1 结束,便于安装脚本和 CI 识别失败。 当前 `LOG_LEVEL` 和 `LOG_FILE` 会进入 `Settings`,`LOG_FILE` 的父目录也会创建,但代码尚未把它们绑定到 Loguru sink;运行日志仍输出到终端,不应假定 `logs/app.log` 一定存在。 ## 依赖说明 当前主流程直接使用: - `pdfplumber`:文本型 PDF 解析 - `click` / `rich`:CLI 和终端输出 - `pydantic` / `pydantic-settings`:配置加载 - `matplotlib` / `numpy`:PNG 图表生成 - `python-dotenv` / `loguru`:环境配置和日志 - `pytest`:测试 `requirements.txt` 只保留当前主流程和开发测试直接使用的依赖。 ## 测试 ```bash pytest ``` 当前测试覆盖: - 配置加载 - 分析阶段写出 metrics/findings - PDF 报表标题边界裁剪 - 现金流别名、现金及现金等价物净增加/减少标签、括号负数和空值占位符解析 - 利润表跨行标签和 EPS 小数解析 - 小额整数、附注号、全角负号和资本性现金支出解析,以及流动资产/流动负债合计的指标口径 - 负增长风险比较、零利润研判、平均余额周转指标和标准流动性比率 - 目录页误命中、公司报表边界、失败图表旧文件清理和自定义目录环境校验 - 金额单位换算、未知单位拒绝、图表文件名安全化及 HTML 引用的年份一致性 - HTML 不展示 `oneclick` 指令,勾稽数据缺失时标记为无法验证 - `validate` 非零退出码、显式必填字段路径和 `data/raw` 年报定位逻辑 ## 已知边界 - 当前解析器适合文本型 PDF;扫描件或图片型 PDF 不在当前支持范围。 - 当前只抽取关键财务科目,不完整还原三张财务报表所有行。 - 报表定位优先匹配“合并”报表标题,复杂版式或标题异常的 PDF 可能需要补充解析规则。 - 公司名和报告年份从 PDF 文件名推导;文件名不符合“公司名 + 四位年份 + 年度报告”形式时,默认报告命名和双期标签可能退回通用值。 - 每张报表从起始页最多读取 8 页。提取完整性或会计恒等式异常当前只产生警告,不会阻止 structured JSON 写出;使用结果前应检查终端警告和 HTML 勾稽状态。 - 图表按单张容错生成,缺少关键数据时最终 HTML 可能少于 6 张图。 - 图表优先使用 macOS 的 Arial Unicode 字体文件,找不到时退回本机默认字体;非 macOS 环境的中文显示效果取决于已安装字体。 - `data/`、`reports/` 是默认运行产物目录,`logs/` 是预留的日志目录;涉及真实年报或公司数据时不要提交到公共仓库。