第四章:文档解析技术:PDF/Word/HTML解析实战、表格与图表信息提取

兄弟们,咱们直接进入正题。上一章聊了数据源怎么来,这一章就是“怎么把数据吃进去”的关键环节。我敢说,在金融RAG项目里,80%的坑都埋在这个阶段。你模型再强,向量库再快,喂进去一堆乱码、错位的表格、或者把图表当成了背景图,那出来的结果就是“一本正经地胡说八道”。

今天,我就把我在银行、券商项目里踩过的雷,以及怎么用Unstructured和LlamaParse这些工具把复杂金融文档“扒皮抽筋”的经验,全盘托出。

4.1 金融文档的“三座大山”:PDF、Word、HTML

金融圈里,PDF是老大,Word是老二,HTML是老三。但每个都不是省油的灯。

  • PDF:最头疼。有扫描件(图片型)、有原生PDF(文字型)、还有混合型。年报、招股书、尽调报告,动不动几百页,里面表格嵌套表格,图表里还有注释。
  • Word:看似简单,但金融文档的Word经常是“格式灾难”。多级列表、修订模式、文本框、嵌入的Excel对象……你以为是文字,其实是对象。
  • HTML:网页版的财经新闻、监管公告。看似结构清晰,但广告、导航栏、动态加载的内容,都是噪音。
避坑指南: 千万别迷信“万能解析器”。没有一种工具能搞定所有格式。我的策略是:先分类,再选工具,最后做后处理。比如,对于扫描件PDF,必须先OCR;对于原生PDF,优先用PyMuPDF(fitz)或pdfplumber;对于Word,python-docx是基础,但遇到复杂表格,得配合pandas。

4.2 表格与图表信息提取:金融RAG的“心脏手术”

金融文档里,表格是灵魂。利润表、资产负债表、现金流量表,这些数据一旦提取错位,整个分析就废了。图表(折线图、K线图、饼图)虽然不直接进向量库,但图表标题、坐标轴标签、图例里的文字,往往是关键上下文。

我见过最离谱的案例:某团队用通用解析器提取年报,结果把“营业收入”和“营业成本”两列数据搞反了,导致RAG回答的财务分析完全错误。所以,表格提取必须做到“像素级对齐”。

4.2.1 表格提取实战:从“乱码”到“结构化”

这里我推荐一个组合拳:pdfplumber + Camelot + 人工校验

  • pdfplumber:适合提取有明确边框的表格,速度快,能保留文本位置信息。
  • Camelot:基于视觉和文本的两种模式(Lattice和Stream),对于无边框或复杂合并单元格的表格,效果更好。

下面是我常用的一个代码片段,专门处理那种“表头跨行、数据跨列”的金融表格:

import pdfplumber
import pandas as pd

def extract_financial_table(pdf_path, page_num):
    with pdfplumber.open(pdf_path) as pdf:
        page = pdf.pages[page_num]
        # 提取表格,设置参数以处理合并单元格
        table = page.extract_table({
            "vertical_strategy": "lines", 
            "horizontal_strategy": "lines",
            "intersection_tolerance": 5
        })
        if table:
            # 第一行作为表头
            headers = table[0]
            data = table[1:]
            df = pd.DataFrame(data, columns=headers)
            # 清洗:去除空行、合并空值
            df = df.dropna(how='all').fillna(method='ffill')
            return df
    return None

# 使用示例
df = extract_financial_table("年报.pdf", 10)
print(df.head())
我的经验: 对于PDF中的表格,不要直接相信extract_table()的默认输出。金融表格经常有“续表”或者跨页的表头。我一般会先提取页面上的所有文本块,用正则匹配“单位:万元”或者“续表”这类关键词,然后再决定是否合并上下两个表格。

4.2.2 图表信息提取:不只是“看图说话”

图表提取,我的原则是:不追求还原图表,只提取元数据。比如,一张K线图,我需要知道它的标题是“2023年Q1股价走势”,X轴是日期,Y轴是价格,图例里有“开盘价、收盘价”。这些信息会作为上下文,和对应的表格数据一起存入向量库。

对于图表中的文字,我常用PaddleOCR或者EasyOCR。但注意,金融图表里的数字字体很小,而且经常有反光或者阴影。我建议先对图片做预处理:灰度化、二值化、降噪。

import cv2
import easyocr

def extract_chart_text(image_path):
    # 读取图片
    img = cv2.imread(image_path)
    # 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化
    _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # OCR识别
    reader = easyocr.Reader(['ch_sim', 'en'])
    results = reader.readtext(thresh)
    # 提取文字和坐标
    texts = [(bbox, text) for (bbox, text, conf) in results if conf > 0.5]
    return texts

4.3 使用Unstructured与LlamaParse处理复杂金融文档

说到这,我得隆重介绍两个“大杀器”:UnstructuredLlamaParse。它们不是银弹,但能解决80%的“脏活累活”。

4.3.1 Unstructured:金融文档的“瑞士军刀”

Unstructured是一个专门处理非结构化数据的库。它能把PDF、Word、HTML、图片等,统一解析成结构化的“元素”(Element),比如Title、NarrativeTable、ListItem、FigureCaption等。这对于构建RAG的chunking策略非常有用。

我特别喜欢它的分区(partition)功能。比如,对于一份招股书,它能自动识别出“封面”、“目录”、“正文”、“表格”、“页脚”,然后分别处理。这样,你就不会把页脚的“第1页”当成正文内容喂给模型了。

from unstructured.partition.pdf import partition_pdf

# 解析PDF,自动分区
elements = partition_pdf(
    filename="招股说明书.pdf",
    strategy="hi_res",  # 高分辨率模式,适合复杂文档
    infer_table_structure=True,  # 自动识别表格结构
    include_page_breaks=True
)

# 遍历元素,提取表格和文本
for element in elements:
    if element.category == "Table":
        print(f"发现表格:{element.text[:100]}...")
        # 可以进一步用Camelot或pandas处理
    elif element.category == "Title":
        print(f"标题:{element.text}")
实战要点: Unstructured的strategy参数很关键。对于扫描件,用“hi_res”;对于原生PDF,用“auto”或“fast”就行。另外,一定要设置infer_table_structure=True,否则表格会被当成普通文本,那就白干了。

4.3.2 LlamaParse:专为RAG设计的“解析引擎”

LlamaParse是LlamaIndex团队推出的,专门为RAG场景优化。它的核心优势是:能处理超长文档、能理解复杂布局、能输出Markdown格式。这对于金融文档简直是量身定做。

我拿它处理过一份300页的券商研报,里面有大量的嵌套表格、脚注、以及用不同颜色标注的“买入/卖出”评级。LlamaParse不仅能正确提取表格,还能把脚注和正文关联起来,输出成结构化的Markdown。

import nest_asyncio
nest_asyncio.apply()
from llama_parse import LlamaParse

# 初始化解析器
parser = LlamaParse(
    api_key="your_api_key",
    result_type="markdown",  # 输出Markdown格式
    verbose=True,
    parsing_instruction="请特别注意表格和图表中的数字,以及脚注信息。"
)

# 解析文档
documents = parser.load_data("券商研报.pdf")

# 查看解析结果
for doc in documents:
    print(doc.text[:500])  # 输出前500个字符
避坑指南: LlamaParse虽然强大,但它是收费的(有免费额度)。而且,对于中文金融文档,一定要在parsing_instruction里明确指定语言和关注点。比如,告诉它“注意‘单位:亿元’这样的表头信息”。另外,它的输出是Markdown,但Markdown里的表格可能不是标准的,需要二次清洗。

4.4 实战流程:从PDF到RAG-ready数据

说了这么多,我画一张流程图,把整个解析流程串起来。这张图是我在项目里反复迭代出来的,你直接拿去用。

金融文档解析实战流程图 原始金融文档 文档类型分类 PDF解析 (Unstructured) Word解析 (python-docx) HTML解析 (BeautifulSoup) 表格/图表信息提取 (Camelot + OCR) 结构化数据 (Markdown/JSON)

这张图的核心思想就是:先分类,再解析,最后统一提取结构化信息。不要试图用一个函数搞定所有事情。

4.5 我的“压箱底”经验总结

最后,我掏心窝子说几句。做金融文档解析,技术只是基础,更重要的是对业务的理解

  1. 不要追求100%的准确率。金融文档太复杂了,能到95%就是优秀。剩下的5%,通过人工抽检和RAG的“置信度过滤”来兜底。
  2. 保留元数据。解析时,一定要保留页码、段落ID、表格ID、来源文件名。这些信息在RAG检索时,能帮你做“溯源”,让模型知道答案来自哪一页。
  3. 建立“解析失败”的容错机制。比如,如果Unstructured解析某个PDF报错,自动降级到PyMuPDF;如果OCR识别率低于80%,标记为“低质量”,不进入向量库。
  4. 定期更新解析规则。金融监管政策在变,文档格式也在变。我每季度会重新跑一次测试集,看看解析准确率有没有下降。
一个小技巧: 对于特别复杂的表格(比如合并单元格、斜线表头),我有时候会直接把它截图,然后用多模态模型(比如GPT-4V)去“读”,虽然慢,但准确率极高。这可以作为最后的“杀手锏”。

好了,这一章的内容就这么多。文档解析是RAG的“地基”,地基不稳,楼盖得再高也是危房。下一章,我们会聊怎么把解析出来的数据切分成合适的“块”(Chunking),那是另一个充满艺术性的技术活。


专注资料整理