LLM如何与程序协作来结构化文本财报数据

之前探索了基于Pydantic结构化文本格式的财报数据。

https://blog.csdn.net/liliang199/article/details/159817287

这里进一步探索LLM和程序融合协作的方式,结构化文本财报数据。

由程序对文本财报数据进行初步清洗,LLM负责对清洗后的财报数据进行对齐。

所用示例参考和修改自网络资料。

1 环境准备

1.1 测试数据

这里采用A股海光 信息2024年的财报作为测试输入,原始链接如下

https://static.cninfo.com.cn/finalpage/2025-03-01/1222675682.PDF

先将pdf下载到本地,路径如下

./688041_2024_n.pdf

1.2 LLM配置

这里采用OpenAI方式访问LLM,所以需要配置api_key和base_url。

示例代码如下所示。

复制代码
import os
 
model_name = gpt_model_name # LLM名称,比如deepseek-r1, qwen3.5-8b
os.environ['OPENAI_API_KEY'] = gpt_api_key # LLM供应商提供的api key
os.environ['OPENAI_BASE_URL'] = gpt_api_url # LLM供应商提供llm访问api的url

from openai import OpenAI

client = OpenAI()

2 协作过程

LLM善于处理语义理解,由于LLM运行延迟大成本高,直接用LLM处理所有清洗效率较低。

针对目标固定的小任务,如空值、单位行、跨页重复表头、特殊符号,程序处理速度快效率高。

所以这里尝试同时利用程序和LLM的优势。

程序处理确定性脏数据,如pdf解析表格提取、内容初步提取和清洗,减少大模型幻觉。

大模型负责语义层面的理解与结构化对齐,如表头别名,这部分程序不太好处理,引入LLM减少程序硬编码需求。

2.1 PDF解析与表格提取

这部分使用 pdfplumber 提取页面中的表格,自动识别边框并返回 pandas.DataFrame 列表。

对于无边框的伪表格,可以改用 extract_text() + 正则,但财报通常有明确边框。

复制代码
import pdfplumber
import pandas as pd
import re

def extract_tables_from_pdf(pdf_path: str, pages: list = None) -> list:
    """
    从PDF中提取所有表格,返回DataFrame列表
    - pages: 指定页码(从0开始),None表示全部
    """
    tables = []
    with pdfplumber.open(pdf_path) as pdf:
        for i, page in enumerate(pdf.pages):
            if pages and i not in pages:
                continue
            # 提取页面表格(基于线条/边框)
            page_tables = page.extract_tables()
            for table in page_tables:
                # print(table)
                if table and len(table) >= 1:  # 忽略空表
                    df = pd.DataFrame(table[0:], columns=table[0])
                    tables.append(df)
    return tables

# 使用示例(假设PDF文件名为 "./688041_2024_n.pdf")
file_path = "./688041_2024_n.pdf"
tables = extract_tables_from_pdf(file_path, pages=[7,8])

输出示例如下,可见格式还是比较混乱的。

传真 010-83010886 010-83010886 0 传真 010-83010886 010-83010886 1 电子信箱 investor@hygon.cn investor@hygon.cn, 公司披露年度报告的媒体名称及网址 \\ 0 公司披露年度报告的媒体名称及网址 1 公司披露年度报告的证券交易所网址 2 公司年度报告备置地点 《上海证券报》www.cnstock.com、《中国证券报》\\nwww.cs.com.cn、《证券时报》www.stcn.com、《证券日\\n报》www.zqrb.cn 0 《上海证券报》www.cnstock.com、《中国证券报》\\nwww.cs.com.cn、... 1 www.sse.com.cn 2 董事会办公室 , 公司股票简况 None None None None 0 公司股票简况 None None None None 1 股票种类 股票上市交易所及板块 股票简称 股票代码 变更前股票简称 2 A股 上海证券交易所科创板 海光信息 688041 不适用, 公司聘请的会计师事\\n务所(境内) 名称 立信会计师事务所(特殊普通合伙) 0 公司聘请的会计师事\\n务所(境内) 名称 立信会计师事务所(特殊普通合伙) 1 None 办公地址 上海市黄浦区南京东路61号四楼 2 None 签字会计师姓名 禹正凡、马旭 3 报告期内履行持续督\\n导职责的保荐机构 名称 中信证券股份有限公司 4 None 办公地址 广东省深圳市福田区中心三路8号卓越时\\n代广场(二期)北座 5 None 签字的保荐代表人姓名 李艳梅、彭捷 6 None 持续督导的期间 2022年8月12日至2025年12月31日, 主要会计数据 2024年 2023年 \\ 0 主要会计数据 2024年 2023年 1 营业收入 9,162,148,135.92 6,011,998,991.03 2 归属于上市公司股\\n东的净利润 1,930,990,510.51 1,263,178,600.37 3 归属于上市公司股\\n东的扣除非经常性\\n损益的净利润 1,815,777,649.92 1,136,358,033.60 4 经营活动产生的现\\n金流量净额 977,081,091.31 813,705,258.76 本期比上\\n年同期增\\n减(%) 2022年 0 本期比上\\n年同期增\\n减(%) 2022年 1 52.40 5,125,266,686.59 2 52.87 803,698,128.25 3 59.79 748,622,856.83 4 20.08 -43,255,599.33 , 2024年末 2023年末 \\ 0 2024年末 2023年末 1 归属于上市公司股\\n东的净资产 20,250,959,179.95 18,705,083,962.67 2 总资产 28,559,492,036.59 22,902,547,952.79 本期末比\\n上年同期\\n末增减\\n(%) 2022年末 0 本期末比\\n上年同期\\n末增减\\n(%) 2022年末 1 8.26 17,053,149,859.87 2 24.70 21,934,487,694.40 , 主要财务指标 2024年 2023年 本期比上年同期增\\n减(%) 2022年 0 主要财务指标 2024年 2023年 本期比上年同期增\\n减(%) 2022年 1 基本每股收益(元/股) 0.83 0.54 53.70 0.38 2 稀释每股收益(元/股) 0.83 0.54 53.70 0.38 3 扣除非经常性损益后的基本每股\\n收益(元/股) 0.78 0.49 59.18 0.35 4 加权平均净资产收益率(%) 9.92 7.11 增加2.81个百分点 8.49 5 扣除非经常性损益后的加权平均\\n净资产收益率(%) 9.32 6.40 增加2.92个百分点 7.91 6 研发投入占营业收入的比例(%) 37.61 46.74 减少9.13个百分点 40.33

2.2 内容提取与清洗

这部分针对财报表格的典型脏数据,空值、单位行、跨页重复表头、特殊符号,如页码干扰。

首先由程序做确定性清洗,示例代码如下

复制代码
def clean_financial_table(df: pd.DataFrame) -> pd.DataFrame:
    """
    清洗原始表格:
    - 移除完全为空的行/列
    - 移除包含"单位"、"页码"等干扰行
    - 合并因跨页导致的重复表头
    - 标准化数值(去除逗号、千分位)
    """
    # 删除全为空的行
    df.dropna(how='all', inplace=True)
    df.dropna(axis=1, how='all', inplace=True)
    
    # 删除包含特定关键词的行(单位行、页码行)
    keywords_to_drop = ['单位', '币种', '页', '/', '报告']
    for kw in keywords_to_drop:
        mask = df.astype(str).apply(lambda x: x.str.contains(kw, na=False)).any(axis=1)
        df = df[~mask]
    
    # 处理合并单元格导致的空值:用前向填充(假设表头在第一行)
    df.fillna(method='ffill', inplace=True)
    
    # 数值列清理:去除逗号、全角数字转换
    for col in df.columns[1:]:  # 假设第一列是指标名
        df[col] = df[col].astype(str).str.replace(',', '').str.replace(',', '')
        df[col] = df[col].str.replace(r'[^\d\.\-]', '', regex=True)  # 保留数字、负号、点
    return df

# 模拟原始表格(从PDF提取后的DataFrame示例)
raw_data = {
    '主要会计数据': ['营业收入', '归属于上市公司股东的净利润', '经营活动产生的现金流量净额'],
    '2024年': ['9,162,148,135.92', '1,930,990,510.51', '977,081,091.31'],
    '2023年': ['6,011,998,991.03', '1,263,178,600.37', '813,705,258.76'],
    '2022年': ['5,125,266,686.59', '803,698,128.25', '-43,255,599.33']
}
df_raw = pd.DataFrame(raw_data)
df_clean = clean_financial_table(df_raw)
print(df_clean)

输出示例如下

主要会计数据 2024年 2023年 2022年

0 营业收入 9162148135.92 6011998991.03 5125266686.59

1 归属于上市公司股东的净利润 1930990510.51 1263178600.37 803698128.25

2 经营活动产生的现金流量净额 977081091.31 813705258.76 -43255599.33

2.3 LLM结构化对齐

财报表格的表头可能是多级。

如"主要会计数据"下分"本期比上年同期增减(%)"),不同公司用词不同,这部分程序不太好处理。

这里使用大模型**理解表格语义,**识别财报表格的表头,并将文本数据对齐到目标JSON Schema。

假设目标Schema如下:

{

"company": "海光信息",

"year": 2024,

"financial_indicators": [

{"name": "营业收入", "2024": 9162148135.92, "2023": 6011998991.03, "2022": 5125266686.59, "yoy_growth": 52.40},

...

]

}

这里使用OpenAI SDK调用大模型,传入清洗后的表格文本和Schema定义。

示例代码如下所示

复制代码
import openai
import json

from openai import OpenAI
client = OpenAI()

def llm_align_financial_table(table_text: str, target_schema: dict) -> dict:
    """
    使用大模型将文本表格转换为目标JSON
    """
    prompt = f"""
你是一个财务数据标准化专家。请将以下表格内容转换为指定的JSON格式。

表格内容:
{table_text}

目标Schema定义(仅作结构参考,实际字段名可微调):
{json.dumps(target_schema, ensure_ascii=False, indent=2)}

要求:
1. 识别表格中的指标名称、年份、数值,并转换为数字类型(去除逗号)。
2. 如果存在"本期比上年同期增减(%)"列,提取为 yoy_growth。
3. 输出严格符合JSON格式,不要包含解释文本。
4. 公司名称从上下文或表格注释中提取(若无则填null)。
"""
    response = client.chat.completions.create(
        model=model_name,
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)

# 将清洗后的DataFrame转为文本表示
table_text = df_clean.to_string(index=False)

target_schema = {
    "company": "string",
    "year": 2024,
    "financial_indicators": [
        {"name": "string", "2024": "float", "2023": "float", "2022": "float", "yoy_growth": "float"}
    ]
}

# 实际调用(需设置OPENAI_API_KEY)
# result = llm_align_financial_table(table_text, target_schema)
# print(json.dumps(result, indent=2, ensure_ascii=False))

1.4 复杂场景处理

财报主要会计数据上方实际上还有"单位:元 币种:人民币"以及"本期比上年同期增减(%)"列。

程序难以自动判断列索引,但大模型可以从文本上下文中推理。

因此可以采用一些增强的策略,将整页文本(包括表格),以及表格的区域坐标和文本内容一同送入大LLM,辅助LLM进行语义理解和数据对齐。

示例代码如下

复制代码
def extract_page_context(pdf_path: str, page_num: int) -> dict:
    """
    提取页面的完整文本 + 表格列表 + 每个表格的坐标
    """
    with pdfplumber.open(pdf_path) as pdf:
        page = pdf.pages[page_num]
        full_text = page.extract_text()
        tables = page.extract_tables()
        table_bboxes = [table.bbox for table in page.find_tables()]  # 获取表格边界框
    return {
        "text": full_text,
        "tables": tables,
        "bboxes": table_bboxes
    }

# 将上下文信息一并喂给大模型,让它自主判断哪部分属于"主要会计数据"

3 完整流水线

以下是整合上述各模块的完整清洗+LLM对齐的流水线示例。

复制代码
def pipeline(pdf_path: str, target_schema: dict) -> dict:
    # 步骤1: 提取所有表格
    raw_tables = extract_tables_from_pdf(pdf_path, pages=[7,8,9])  # 按实际页码
    # print(raw_tables)
    # 步骤2: 过滤出目标表格(比如通过表头关键词匹配)
    target_df = None
    for df in raw_tables:
        # print("->", df.iloc[0,0],  "\nxxx", df)
        if df.iloc[0,0] and "主要会计数据" in str(df.iloc[0,0]):
            target_df = df
            break
    if target_df is None:
        raise ValueError("未找到主要会计数据表格")
    # 步骤3: 清洗
    cleaned_df = clean_financial_table(target_df)
    # 步骤4: 大模型对齐
    table_text = cleaned_df.to_string(index=False)
    result = llm_align_financial_table(table_text, target_schema)
    return result

# 执行
output = pipeline(file_path, target_schema)
print(output)

输出如下所示,数据与原始文本一致,且符合schema定义。

通过程序清洗+LLM对齐,较好的从比较混乱的文本表格中对原始财务数据进行结构化。

{'company': None, 'year': 2024, 'financial_indicators': [{'name': '营业收入', '2024': 9162148135.92, '2023': 6011998991.03, '2022': 5125266686.59, 'yoy_growth': 52.4}, {'name': '归属于上市公司股东的净利润', '2024': 1930990510.51, '2023': 1263178600.37, '2022': 803698128.25, 'yoy_growth': 52.87}, {'name': '归属于上市公司股东的扣除非经常性损益的净利润', '2024': 1815777649.92, '2023': 1136358033.6, '2022': 748622856.83, 'yoy_growth': 59.79}, {'name': '经营活动产生的现金流量净额', '2024': 977081091.31, '2023': 813705258.76, '2022': -43255599.33, 'yoy_growth': 20.08}]}

reference


LLM如何基于Pydantic结构化文本格式的财报数据

https://blog.csdn.net/liliang199/article/details/159817287

相关推荐
麦聪聊数据4 小时前
企业数据流通与敏捷API交付实战(五):异构数据跨库联邦与零代码发布
数据库·sql·低代码·restful
Elastic 中国社区官方博客4 小时前
当 TSDS 遇到 ILM:设计不会拒绝延迟数据的时间序列数据流
大数据·运维·数据库·elasticsearch·搜索引擎·logstash
Omics Pro4 小时前
虚拟细胞:开启HIV/AIDS治疗新纪元的关键?
大数据·数据库·人工智能·深度学习·算法·机器学习·计算机视觉
J2虾虾5 小时前
MySQL的基本操作
数据库·mysql
arvin_xiaoting5 小时前
OpenClaw学习总结_III_自动化系统_3:CronJobs详解
数据库·学习·自动化
杨云龙UP5 小时前
Oracle 中 NOMOUNT、MOUNT、OPEN 怎么理解? 在不同场景下如何操作?_20260402
linux·运维·数据库·oracle
jzwugang5 小时前
postgresql链接详解
数据库·postgresql
2601_949815336 小时前
MySQL输入密码后闪退?
数据库·mysql·adb
lifewange6 小时前
Redis的测试要点和测试方法
数据库·redis·缓存
_下雨天.6 小时前
MySQL高可用
数据库·mysql