1. 简介
版面分析作为RAG的第一步工作,其效果对于下游工作至关重要。
前常见的 PDF 解析方法包括三种
- 基于规则:根据 PDF 的组织特征确定每个部分的规则(风格和内容)缺点:不通用(PDF格式不固定)
- 基于深度学习:目标检查和 OCR 结合的流行解决方案
- 基于多模态大模型:对复杂的结构进行Pasing或提取PDF中的关键信息
1.1 基于规则的pdf解析
代表框架:
- pdfplumber
- PyMuPDF
- pypdf
1.1.1 pdfplumber
pdfplumber 按照每一页处理 pdf,获得 pdf 的页面文字提取表格,最终将 pdf 文件生成对应的 txt 文件。
缺点:按页进行处理有时候一段上下文连续的文字,分散到两页并不好进行处理。
## 纯文本
file_name = './example_data/焦点科技__2019年__年度报告.pdf'
output_file = './example_data/焦点科技__2019年__年度报告.txt'
with pdfplumber.open(file_name) as pdf, open(output_file, 'wt', encoding='utf-8') as f:
for page in pdf.pages: # pdf.pages是⼀个Page对象的列表
text = page.extract_text() # 调⽤Page对象的extract_text()⽅法提取⽂本
f.write(text)
f.write('\n')
## 表格
with pdfplumber.open(file_name) as pdf, open(output_file, 'wt', encoding='utf-8') as f:
for page in pdf.pages: # pdf.pages是⼀个Page对象的列表
text = page.extract_table() # 提取每⻚的表格⽂字信息
# page.extract_tables() 提取多个表格
print(text)
if text:
f.write(str(text))
f.write('\n')
## 表格保存为excel
from openpyxl import Workbook
with pdfplumber.open(file_name) as pdf:
for i in pdf.pages:
table = i.extract_table()
if table:
workbook = Workbook() # 创建⼀个⼯作簿
sheet = workbook.active # 获取当前活跃的sheet,默认是第⼀个sheet
for row in table:
sheet.append(row)
workbook.save(output_file) # 保存⼯作簿
break
## 提取图片保存到本地
with pdfplumber.open(file_name) as pdf:
for i in pdf.pages:
print('⻚码:', i.page_number)
print('⻚宽:', i.width)
print('⻚⾼:', i.height)
print('⻚⽂本:', i.extract_text())
print('⻚表格:', i.extract_table())
print('⻚图⽚:', i.images)
for img in i.images:
# 获取图⽚的⼆进制流
img_data = img['stream'].get_data()
with open(output_file, 'wb') as f:
f.write(img_data)
break # 解析第⼀⻚
1.1.2 pymupdf
pymupdf 支持将文字修改写入 pdf 的界面,支持提取所有类型的文档,支持的功能特别多,基本上各种文件都可以处理
import fitz
pdf_path = '/example_data/安靠智电__2019年__年度报告.pdf'
doc = fitz.open(pdf_path)
# 基本的pdf信息
title = doc.metadata['title'] # 标题
author = doc.metadata['author'] # 作者
create_time = doc.metadata['creationDate'] # 创建时间
num_pages = doc.page_count # ⻚数
page = doc.load_page(0) # 加载第⼀⻚
page_width = page.rect.width # ⻚宽
page_height = page.rect.height # ⻚⾼
page_text = page.get_text() # ⻚⽂本
page_images = page.get_images(full=True) # ⻚图⽚
print('标题:', title)
print('作者:', author)
print('创建时间:', create_time)
print('⻚数:', num_pages)
print('⻚码:', 1)
print('⻚宽:', page_width)
print('⻚⾼:', page_height)
print('⻚⽂本:', page_text)
print('⻚图⽚:', page_images)
num_pages = doc.page_count # ⻚数
for page_index in range(num_pages):
page = doc.load_page(page_id=page_index)
page_text = page.get_text() # ⻚⽂本
print(page_text)
break
for page_index in range(num_pages):
page = doc.load_page(page_id=page_index)
image_list = page.get_images() # 图⽚
print(image_list)
# 两种⽅式
# 1
for img in image_list:
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"] # 图⽚⼆进制数据
image_ext = base_image["ext"] # 图⽚格式
# image_path = f'./example_data/__焦点科技__2019年__年度报告__page_{page_index}__image_{xref}.{image_ext}'
# with open(image_path, 'wb') as f:
# f.write(image_bytes)
# 2
pix = fitz.Pixmap(doc, xref)
pix.save(f'./example_data/焦点科技__2019年__年度报告__page_{page_index}__image_{xref}.png')
break
## 获取表格
for page_index in range(num_pages):
page = doc.load_page(page_id=page_index)
tables = page.find_tables() # 表格
print(tables)
# 提取表格数据并存储为csv⽂件
for table in tables:
df = table.to_pandas()
print(df.head())
df.to_csv(
f'./example_data/焦点科技__2019年__年度报告_page_{page_index}__table_{table}.csv',
index=False)
break
## PDF文档分割
output_dir = './example_data/{0}.pdf'
for page_index in range(num_pages):
print(page_index)
# 创建⼀个新的Document对象,包含当前⻚⾯
new_pdf = fitz.open()
new_pdf.insert_pdf(doc, from_page=page_index, to_page=page_index)
# 保存为单独的pdf⽂件
new_pdf.save(output_dir.format(page_index))
new_pdf.close()
break
doc.close()
1.1.3 pypdf
pypdf 就是一种基于规侧广泛使用的解析器,也是 LangChain 和 Llamalndex 中解析 PDF文件的标准方法。
pip install PyPDF2
import PyPDF2
filename = "./example_data/焦点科技_2019年__年度报告.pdf"
pdf_file = open(filename, 'rb')
reader = PyPDF2.PdfReader(pdf_file)
page_num = 0
page = reader.pages[page_num]
text = page.extract_text()
print('--------------------------------------------------')
print(text)
pdf_file.close()
'''
焦点科技股份有限公司 2019年年度报告全⽂
1
焦点科技股份有限公司
Focus Technology Co., Ltd.
(南京江北新区星⽕路软件⼤厦 A 座12F)
⼆〇⼀九年年度报告
⼆〇⼆〇年⼆⽉
'''
1.2 基于深度学习的pdf解析
基于深度学习模型的方法能够准确地识别整个文档的布局,包括表格和段落。它甚至可以理解表中的结构。这意味着它可以将文档划分为定义明确、完整的信息单元。同时保留预期的含义和结构。
代表性的开源框架:
- Unstructured: 已经集成到langchain中。使用hi_res策略设置
infer_table_structure=True
可以很好的识别表格信息。然而 fast策略因为不使用目标检测模型,在识别图像和表格方面表现比较差。 - Layout-parser:如果需要识别复杂的结构化PDF ,建议使用最大的模型以获得更高的精度,但是会比较慢。此外,Layout解析器的模型在过去几年中似乎没有更新。
- PP-StructureV2: 可以组合各种模型用于文档分析,性能高于平均水平。
Unstructured
from unstructured.partition.pdf import partition_pdf
filename = "../paper/bert.pdf"
# infer_table_structure=True automatically selects hi_res strategy
elements = partition_pdf(filename=filename, infer_table_structure=True)
tables = [el for el in elements if el.category == "Table"]
print(tables[0].text)
print('--------------------------------------------------')
print(tables[0].metadata.text_as_html)
1.3 基于多模态的pdf解析(复杂结构)
LlamaIndex例子:检索相关图像(PDF页面)并将其发送到GPT4-V 以响应查询。
- 将每个PDF页面视为一个图像,让GPT4-V 对每个页面进行图像推理,为图像推理构建文本矢量存储索引,根据图像矢量存储查询答案。
- 使用Table Transformer从检索到的图像中裁剪表信息,然后将这些裁剪的图像发送到GPT4-V 进行查询相应。
- 对裁剪的表图像应用OCR,并将数据发送到GPT4 以回答查询。
经测试确定第三种方法最有效。
此外可以试用多模态模型从图像中提取或者总结关键信息(PDF文件可以很容易转换为图像)
1.3.1 解析pdf文档存在哪些挑战
挑战在于准确提取整个页面的布局,并将表格、标题、段落和图片在内的内容翻译成文档的文本表示。这个过程涉及到处理文本提取、图像识别中的不确定之处,以及表中行-列关系的混乱。
1.3.1.1 挑战一:如何从表格和图像中提取数据问题?
使用unstructured
框架作为示例,检测到的表格数据可以直接导出为HTML:
复制HTML标记并将其另存为HTML文件,使用Chrome打开如下:
1.3.1.2 挑战二:如何重新排列检测到的块?特别对于双列PDF?
在确定布局后,unstructured
框架会将每个页面划分成几个矩形块:
每个矩形块的详细信息可以通过以下格式获取:
其中(x1, y1)是左上顶点的坐标,(x2, y2)是右下顶点的坐标
此时,可以选择重新调整页面的阅读顺序。unstructured
有一个内置排序算法。但是实际排序结果不是很满意。
因此有必要设计一种排序算法。最简单的方法就是先按左上角顶点的水平座标排序,如果水平座标相同,则按垂直坐标排序。伪代码如下:
layout.sort(key=lambda z:(z.bbox.x1, z.bbox.y1, z.bbox.x2, z.bbox.y2))
然而还发现,即使同一列中的块,其水平坐标也可能发生变化。如下图,提取到的紫色线条块的水平坐标bbox.x1 实际更靠左。排序时他将位于路线块之前,显然违反了阅读顺序。
在这种情况下使用一种可能的算法:
- 首先,对左上角所有x坐标x1 进行排序,可以得到X1_min
- 然后,对所有右下角x坐标x2进行排序,可以得到x2_max
- 接下来,将页面中心线的x坐标确定
x1_min = min([el.bbox.x1 for el in layout]) ; x2_max = max([el.bbox.x2 for el in layout]); mid_line_x_cooedinate = (x2_max + x1_min)/2
- 接下来,如果bbox.x1< mid_line_x_cordinate ,则块被分类为左列的一部分。否则被视为右边的一部分。
- 分类完成后,根据列中的y坐标对每个块进行排序。
- 最后,将右侧列连接到左侧列的右侧
left_column, right_column = [], []
for el in layout:
if el.bbox.x1 < mid_line_x_cooedinate:
left_column.append(el)
else:
right_column.append(el)
left_column.sort(key = lambda z: z.bbox.y1)
right_column.sort(key = lambda z: z.bbox.y1)
sorted_layout = left_column + right_column
值得一提的是这种改进也与单列pdf兼容.
1.3.1.3 挑战三:如何提取多级标题?
提取多级标题的目的是提高LLM答案的准确性。
该算法依然依赖与上面提到的布局快。我们可以设置type=‘Section-hearder’的块,并计算高度差(bbox.y2-bbox.y1)。高度差最大的块对应第一级标题,其次是第二级标题…
补充:PPT类文档解析问题
- 难点:如何对PPT 中的大量的流程图,架构图进行提取,因为这些图多以形状元素在PPT中呈现,如果只提取文字,大量潜藏的信息会大量丢失。
- 解决办法:将PPT转为pdf形式,然后用户上述处理PDF 的形式进行解析。
参考:
版面分析–优化策略篇
【知识库构建——知识文本分块】
【知识库构建——文档切分优化策略篇】