之前探索了LLM如何联合两份文档分析公司的年度财务数据。
https://blog.csdn.net/liliang199/article/details/159282803
针对多份文档难以一次性分析的困境,可以采用类似MapReduce分布式思路处理。
这里参考网络资料,尝试先map提取每个文档要素,然后reduce汇总所有要素,呈现分析结果。
1 问题说明
以下是问题说明。
在2024年10家A股上市公司的年度财务报告中,
哪家公司的研发投入最高?
具体数额是多少?
基于2024年真实财报数据,选取的10家公司,具体为
比亚迪 (002594) 、宁德时代 (300750) 、百济神州 (688235) 、中国移动 (600941) 、美的集团 (000333) 、海光信息 (688041) 、韦尔股份 (603501) 、中芯国际 (688981) 、中国汽研 (601965) 、研奥股份 (300923) 、
2 数据准备
2.1 准备目录
创建目录financial_reports,将10家公司的2024年年度报告PDF文件放入该目录。
文件名需与代码中 COMPANY_PDF_MAP 定义的名称一致,比如,
比亚迪 002594_2024_n.pdf
2.2 下载文件
需要获取这些公司的2024年年度报告PDF,这里假设文件已下载。
以下是待测试公司的pdf文件和对应的下载链接。
比亚迪 002594_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-03-25/1222881496.PDF
宁德时代 300750_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-03-15/1222806982.PDF
百济神州 688235_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-04-29/1223387705.PDF
中国移动 600941_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-03-21/1222856315.PDF
美的集团 000333_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-03-29/1222951181.PDF
海光信息 688041_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2024-04-12/1219582015.PDF
韦尔股份 603501_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-04-16/1223104428.pdf
中芯国际 688981_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-03-28/1222924320.PDF
中国汽研 601965_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-04-26/1223305545.PDF
研奥股份 300923_2024_n.pdf
https://static.cninfo.com.cn/finalpage/2025-04-21/1223148777.PDF
2.3 PDF解析
这里使用pdfplumber解析PDF文本,示例代码如下
import os
import json
import pdfplumber
import pandas as pd
from typing import Dict, Optional, List
from openai import OpenAI
# ==================== 配置区域 ====================
# PDF文件存放目录(请修改为实际路径)
PDF_DIR = "./financial_reports"
# 公司名称与PDF文件名映射(假设PDF文件名包含公司名,也可用其他规则)
# 格式:{公司名: 文件名}
COMPANY_PDF_MAP = {
"比亚迪": "002594_2024_n.pdf",
"宁德时代": "300750_2024_n.pdf",
"百济神州": "688235_2024_n.pdf",
"中国移动": "600941_2024_n.pdf",
"美的集团": "000333_2024_n.pdf",
"海光信息": "688041_2024_n.pdf",
"韦尔股份": "603501_2024_n.pdf",
"中芯国际": "688981_2024_n.pdf",
"中国汽研": "601965_2024_n.pdf",
"研奥股份": "300923_2024_n.pdf"
}
# =================================================
# 初始化OpenAI客户端
client = OpenAI()
def extract_text_from_pdf(pdf_path: str, max_pages: int = 500) -> str:
"""
从PDF中提取文本,最多提取max_pages页(财报太长,只取前N页通常包含管理层讨论)
"""
text_parts = []
try:
with pdfplumber.open(pdf_path) as pdf:
# 财报关键数据通常在"管理层讨论与分析"章节,位于前几十页
pages_to_read = min(len(pdf.pages), max_pages)
for i in range(pages_to_read):
page = pdf.pages[i]
page_text = page.extract_text()
if page_text:
text_parts.append(page_text)
except Exception as e:
print(f"解析PDF失败 {pdf_path}: {e}")
return ""
return "\n".join(text_parts)
txt = extract_text_from_pdf("./financial_reports/002594_2024_n.pdf")
print(txt[:100])
输出示例如下所示
比亚迪股份有限公司2024........
3 MapReduce处理
在做好上述准备后,就可以分别进行map信息抽取,然后进行reduce汇总分析了。
3.1 map-要素提取
通过精心设计的Prompt强制输出JSON,并处理markdown代码块包裹。
LLM在提取时直接将亿元、万元转换为元,确保数值可比。
这里将所有这些逻辑,分装到如下所示的map函数中。
def map_extract_rnd_expense(company_name: str, pdf_text: str) -> Optional[Dict]:
"""
使用LLM从PDF文本中提取研发费用数据
返回字典包含:company_name, rnd_expense_total (单位元), unit, source_page
"""
prompt = f"""
你是一位资深的财务分析师。请从以下公司年度报告文本片段中,提取该公司在2024年度的"研发投入"或"研发费用"数据。
要求:
1. 优先提取"研发投入"总额(包含费用化和资本化部分);如果未明确披露研发投入,则提取"研发费用"金额。
2. 如果金额以"亿元"、"万元"为单位,请转换为"元"的整数形式(例如:531.95亿元 → 53195000000)。
3. 输出必须是严格的JSON格式,包含以下字段:
- "company_name": 公司全称(中文)
- "rnd_expense_total": 数值(整数,单位:元)
- "unit": 原始单位(如"元"、"亿元"、"万元")
- "source_page": 页码(如果文本中没有页码,可填"未知")
4. 如果未找到任何研发相关数据,返回 {{"rnd_expense_total": null}}。
公司名称:{company_name}
文本内容:
{pdf_text[:]} # 限制长度避免超过token上限
"""
try:
response = client.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": "你是一个专业的财务数据提取助手,只输出JSON格式的结果。"},
{"role": "user", "content": prompt}
],
temperature=0,
max_tokens=500
)
content = response.choices[0].message.content.strip()
# 尝试提取JSON(可能被markdown代码块包裹)
if content.startswith("```json"):
content = content.split("```json")[1].split("```")[0]
elif content.startswith("```"):
content = content.split("```")[1].split("```")[0]
data = json.loads(content)
return data
except Exception as e:
print(f"LLM调用失败 {company_name}: {e}")
return None
data = extract_rnd_expense(company_name="比亚迪", pdf_text=txt)
输出示例如下
{'company_name': '比亚迪股份有限公司',
'rnd_expense_total': 54160964000,
'unit': '元',
'source_page': '33'}
3.2 reduce-信息汇总
使用Pandas展示排行榜,包含原始金额和亿元单位,方便阅读。
在这里,首先通过extract_text_from_pdf抽取每个文档的要素,类似于map过程。
而将抽取的所有文档的要素汇总的pandas dataframe的过程,就相当于reduce过程。
示例代码如下所示。
由于没有合适的mapreduce框架,在如下函数中没有将map和reduce进行分离。
但理论上如下map和reduce过程是可以分离,方便进行大批量分布式处理。
def reduce_process_all_companies() -> pd.DataFrame:
"""
遍历所有公司,提取研发费用,返回DataFrame
"""
results = []
for company, pdf_file in COMPANY_PDF_MAP.items():
pdf_path = os.path.join(PDF_DIR, pdf_file)
if not os.path.exists(pdf_path):
print(f"警告:文件不存在 {pdf_path},跳过")
continue
print(f"正在处理 {company} ...")
# 1. 提取PDF文本(仅前50页)
pdf_text = map_extract_text_from_pdf(pdf_path, max_pages=100)
if not pdf_text:
print(f" 未能提取文本,跳过")
continue
# 2. LLM提取研发费用
extracted = extract_rnd_expense(company, pdf_text)
if extracted and extracted.get("rnd_expense_total"):
results.append({
"company": company,
"rnd_2024": extracted["rnd_expense_total"],
"unit": extracted.get("unit", "元"),
"source_page": extracted.get("source_page", "未知")
})
print(f" 提取成功:{extracted['rnd_expense_total']} 元")
else:
print(f" 未能提取到研发费用数据")
# 构建DataFrame
df = pd.DataFrame(results)
if df.empty:
print("没有成功提取任何数据")
return df
# 确保数值列为整数
df["rnd_2024"] = df["rnd_2024"].astype(int)
return df
df = reduce_process_all_companies()
df
输出示例如下,由于海光信息提取异常,所以未最终显示,说明代码具备兼容异常的能力。
正在处理 比亚迪 ...
提取成功:54160964000 元
正在处理 宁德时代 ...
提取成功:18606756000 元
正在处理 百济神州 ...
提取成功:14139839000 元
正在处理 中国移动 ...
提取成功:34027000000 元
正在处理 美的集团 ...
提取成功:16232771000 元
正在处理 海光信息 ...
未能提取到研发费用数据
正在处理 韦尔股份 ...
提取成功:3245293134 元
正在处理 中芯国际 ...
提取成功:5447122000 元
正在处理 中国汽研 ...
提取成功:335308620 元
正在处理 研奥股份 ...
提取成功:23434286 元
company rnd_2024 unit source_page
0 比亚迪 54160964000 元 34
1 宁德时代 18606756000 千元 25
2 百济神州 14139839000 千元 37
3 中国移动 34027000000 百万元 77
4 美的集团 16232771000 千元 51
5 韦尔股份 3245293134 元 37
6 中芯国际 5447122000 千元 15
7 中国汽研 335308620 元 22/278
8 研奥股份 23434286 元 21
3.3 信息聚合呈现
这里借助于Pandas的Dataframe聚合分析数据,能有效避免LLM运行聚合统计分析的短板。
示例代码如下。
def analyze_results(df: pd.DataFrame):
"""
对结果进行排序并输出结论
"""
if df.empty:
return
# 按研发费用降序排序
df_sorted = df.sort_values(by="rnd_2024", ascending=False).reset_index(drop=True)
# 添加亿元显示列
df_sorted["rnd_亿元"] = df_sorted["rnd_2024"] / 100000000
print("\n" + "="*60)
print("2024年上市公司研发费用排行榜(单位:元)")
print("="*60)
print(df_sorted[["company", "rnd_2024", "rnd_亿元", "source_page"]].to_string(index=False))
top_company = df_sorted.iloc[0]
print("\n" + "="*60)
print(f"✅ 结论:研发投入最高的公司是【{top_company['company']}】,")
print(f" 投入金额为 {top_company['rnd_亿元']:.2f} 亿元({top_company['rnd_2024']:,} 元)。")
print("="*60)
if __name__ == "__main__":
# 检查PDF目录
if not os.path.isdir(PDF_DIR):
print(f"错误:PDF目录 {PDF_DIR} 不存在,请创建并放入财报PDF文件。")
exit(1)
# 分析结果
analyze_results(df)
输出示例如下。
============================================================
2024年上市公司研发费用排行榜(单位:元)
============================================================
company rnd_2024 rnd_亿元 source_page
比亚迪 54160964000 541.609640 34
中国移动 34027000000 340.270000 77
宁德时代 18606756000 186.067560 25
美的集团 16232771000 162.327710 51
百济神州 14139839000 141.398390 37
中芯国际 5447122000 54.471220 15
韦尔股份 3245293134 32.452931 37
中国汽研 335308620 3.353086 22/278
研奥股份 23434286 0.234343 21
============================================================
✅ 结论:研发投入最高的公司是【比亚迪】,
投入金额为 541.61 亿元(54,160,964,000 元)。
============================================================
reference
LLM复杂数值的提取计算场景示例
https://blog.csdn.net/liliang199/article/details/159282803
LLM应对文档有效信息分散的策略探索
https://blog.csdn.net/liliang199/article/details/159159167
LLM长上下文和数值类有效输出的关系探索
https://blog.csdn.net/liliang199/article/details/159175752
LLM数值提取-计算场景示例