第四章:文档解析技术:PDF/Word/HTML解析实战、表格与图表信息提取
兄弟们,咱们直接进入正题。上一章聊了数据源怎么来,这一章就是“怎么把数据吃进去”的关键环节。我敢说,在金融RAG项目里,80%的坑都埋在这个阶段。你模型再强,向量库再快,喂进去一堆乱码、错位的表格、或者把图表当成了背景图,那出来的结果就是“一本正经地胡说八道”。
今天,我就把我在银行、券商项目里踩过的雷,以及怎么用Unstructured和LlamaParse这些工具把复杂金融文档“扒皮抽筋”的经验,全盘托出。
4.1 金融文档的“三座大山”:PDF、Word、HTML
金融圈里,PDF是老大,Word是老二,HTML是老三。但每个都不是省油的灯。
- PDF:最头疼。有扫描件(图片型)、有原生PDF(文字型)、还有混合型。年报、招股书、尽调报告,动不动几百页,里面表格嵌套表格,图表里还有注释。
- Word:看似简单,但金融文档的Word经常是“格式灾难”。多级列表、修订模式、文本框、嵌入的Excel对象……你以为是文字,其实是对象。
- HTML:网页版的财经新闻、监管公告。看似结构清晰,但广告、导航栏、动态加载的内容,都是噪音。
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())
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处理复杂金融文档
说到这,我得隆重介绍两个“大杀器”:Unstructured 和 LlamaParse。它们不是银弹,但能解决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}")
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个字符
4.4 实战流程:从PDF到RAG-ready数据
说了这么多,我画一张流程图,把整个解析流程串起来。这张图是我在项目里反复迭代出来的,你直接拿去用。
这张图的核心思想就是:先分类,再解析,最后统一提取结构化信息。不要试图用一个函数搞定所有事情。
4.5 我的“压箱底”经验总结
最后,我掏心窝子说几句。做金融文档解析,技术只是基础,更重要的是对业务的理解。
- 不要追求100%的准确率。金融文档太复杂了,能到95%就是优秀。剩下的5%,通过人工抽检和RAG的“置信度过滤”来兜底。
- 保留元数据。解析时,一定要保留页码、段落ID、表格ID、来源文件名。这些信息在RAG检索时,能帮你做“溯源”,让模型知道答案来自哪一页。
- 建立“解析失败”的容错机制。比如,如果Unstructured解析某个PDF报错,自动降级到PyMuPDF;如果OCR识别率低于80%,标记为“低质量”,不进入向量库。
- 定期更新解析规则。金融监管政策在变,文档格式也在变。我每季度会重新跑一次测试集,看看解析准确率有没有下降。
好了,这一章的内容就这么多。文档解析是RAG的“地基”,地基不稳,楼盖得再高也是危房。下一章,我们会聊怎么把解析出来的数据切分成合适的“块”(Chunking),那是另一个充满艺术性的技术活。