1、类的创建
内容类型枚举
python
class DoclingContentType(str, Enum):
IMAGE = "image"
TABLE = "table"
TEXT = "text"
EQUATION = "equation"
逐行解释:
python
class DoclingContentType(str, Enum):
- 定义了一个名为
DoclingContentType的类 (str, Enum)表示这个类同时继承自str和Enum- 这意味着它既是枚举类型,又是字符串类型
python
IMAGE = "image"
- 定义枚举常量
IMAGE,其值为字符串"image" - 用于标识文档中的图像内容
python
TABLE = "table"
- 定义枚举常量
TABLE,值为"table" - 用于标识文档中的表格内容
python
TEXT = "text"
- 定义枚举常量
TEXT,值为"text" - 用于标识文档中的文本内容
python
EQUATION = "equation"
- 定义枚举常量
EQUATION,值为"equation" - 用于标识文档中的数学公式
作用:这个枚举定义了文档中可能包含的四种内容类型,便于在代码中统一管理和使用。
内容类型枚举 (DoclingContentType) 的实际使用
什么是枚举?
枚举就像是一个固定的选项菜单,只能选择其中定义好的值。
实际使用场景:
python
# 场景1:文档解析器识别内容类型
def process_document_element(element):
# 假设我们有一个文档元素,需要判断它是什么类型
if element.contains_image():
content_type = DoclingContentType.IMAGE
print(f"发现 {content_type} 内容") # 输出: 发现 image 内容
elif element.contains_table():
content_type = DoclingContentType.TABLE
print(f"发现 {content_type} 内容") # 输出: 发现 table 内容
elif element.contains_math():
content_type = DoclingContentType.EQUATION
else:
content_type = DoclingContentType.TEXT
return content_type
# 使用
element_type = process_document_element(some_doc_element)
python
# 场景2:根据不同内容类型进行不同处理
def handle_content(content_type: DoclingContentType, content_data):
if content_type == DoclingContentType.TEXT:
# 处理文本:提取文字,分析语义
return process_text(content_data)
elif content_type == DoclingContentType.TABLE:
# 处理表格:提取行列数据
return extract_table_data(content_data)
elif content_type == DoclingContentType.IMAGE:
# 处理图片:OCR识别或保存图片
return process_image(content_data)
elif content_type == DoclingContentType.EQUATION:
# 处理公式:数学公式识别
return parse_equation(content_data)
# 使用示例
text_result = handle_content(DoclingContentType.TEXT, "这是一段文字")
table_result = handle_content(DoclingContentType.TABLE, table_data)
python
# 场景3:存储和序列化
document_elements = [
{"type": DoclingContentType.TEXT, "content": "文章标题", "position": {...}},
{"type": DoclingContentType.IMAGE, "content": image_data, "position": {...}},
{"type": DoclingContentType.TABLE, "content": table_data, "position": {...}},
{"type": DoclingContentType.EQUATION, "content": "E=mc²", "position": {...}}
]
# 遍历处理
for element in document_elements:
if element["type"] == DoclingContentType.TABLE:
print("这是一个表格,需要特殊处理")
边界框数据类
python
@dataclass
class _BBox:
page_no: int
x0: float
y0: float
x1: float
y1: float
逐行解释:
python
@dataclass
- 这是一个装饰器,用于自动生成特殊方法
- 它会自动创建
__init__、__repr__等方法 - 让类定义更简洁,不需要手动写构造函数
python
class _BBox:
- 定义名为
_BBox的类 - 下划线前缀通常表示这是内部使用的类
python
page_no: int
- 定义
page_no字段,类型为整数 (int) - 表示边界框所在的页码
python
x0: float
y0: float
- 定义
x0和y0字段,类型为浮点数 (float) - 表示边界框左上角的坐标 (x0, y0)
python
x1: float
y1: float
- 定义
x1和y1字段,类型为浮点数 (float) - 表示边界框右下角的坐标 (x1, y1)
作用:这个数据类用于表示文档中某个元素(如图像、表格等)的位置信息,包含页码和边界框坐标。
边界框数据类 (_BBox) 的实际使用
什么是边界框?
边界框就是用一个矩形框出文档中某个元素的位置。
实际使用场景:
python
# 场景1:在PDF页面中定位一个图片
# 假设我们在第2页发现一个图片,坐标从(50, 100)到(200, 300)
image_bbox = _BBox(
page_no=2, # 在第2页
x0=50.0, # 左上角x坐标
y0=100.0, # 左上角y坐标
x1=200.0, # 右下角x坐标
y1=300.0 # 右下角y坐标
)
print(f"图片在页面 {image_bbox.page_no}")
print(f"位置: ({image_bbox.x0}, {image_bbox.y0}) 到 ({image_bbox.x1}, {image_bbox.y1})")
print(f"宽度: {image_bbox.x1 - image_bbox.x0}")
print(f"高度: {image_bbox.y1 - image_bbox.y0}")
python
# 场景2:文档中多个元素的定位
# 假设我们解析一个PDF,找到了各种元素的位置
document_bboxes = [
_BBox(page_no=1, x0=100.0, y0=50.0, x1=400.0, y1=80.0), # 标题文本
_BBox(page_no=1, x0=50.0, y0=100.0, x1=200.0, y1=300.0), # 图片
_BBox(page_no=1, x0=250.0, y0=100.0, x1=450.0, y1=250.0), # 表格
_BBox(page_no=2, x0=100.0, y0=150.0, x1=300.0, y1=180.0) # 公式
]
# 找出第一页的所有元素
page1_elements = [bbox for bbox in document_bboxes if bbox.page_no == 1]
python
# 场景3:结合内容类型和边界框
@dataclass
class DocumentElement:
content_type: DoclingContentType
bbox: _BBox
content: any
# 创建一个文档元素
title_element = DocumentElement(
content_type=DoclingContentType.TEXT,
bbox=_BBox(page_no=1, x0=100.0, y0=50.0, x1=400.0, y1=80.0),
content="RAGFlow 技术文档"
)
table_element = DocumentElement(
content_type=DoclingContentType.TABLE,
bbox=_BBox(page_no=1, x0=250.0, y0=100.0, x1=450.0, y1=250.0),
content=[["姓名", "年龄"], ["张三", "25"], ["李四", "30"]]
)
# 使用
elements = [title_element, table_element]
for element in elements:
print(f"类型: {element.content_type}")
print(f"位置: 第{element.bbox.page_no}页")
print(f"坐标: ({element.bbox.x0}, {element.bbox.y0})")
继承RAGFlowPdfParser
python
class DoclingParser(RAGFlowPdfParser):
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
self.page_images: list[Image.Image] = []
self.page_from = 0
self.page_to = 10_000
def check_installation(self) -> bool:
if DocumentConverter is None:
self.logger.warning("[Docling] 'docling' is not importable, please: pip install docling")
return False
try:
_ = DocumentConverter()
return True
except Exception as e:
self.logger.error(f"[Docling] init DocumentConverter failed: {e}")
return False
类定义和继承
python
class DoclingParser(RAGFlowPdfParser):
- 定义了一个名为
DoclingParser的类 - 继承自
RAGFlowPdfParser,意味着它拥有父类的所有功能,并可以扩展或重写
初始化方法 __init__
python
def __init__(self):
- 类的构造函数,在创建对象实例时自动调用
python
self.logger = logging.getLogger(self.__class__.__name__)
- 创建一个日志记录器
self.__class__.__name__获取当前类的名称("DoclingParser")- 这样日志消息会显示来自 "DoclingParser" 类
python
self.page_images: list[Image.Image] = []
- 初始化实例变量
page_images - 类型注解表明这是一个
Image.Image对象的列表(来自PIL库) - 初始化为空列表,用于存储从PDF提取的页面图像
python
self.page_from = 0
self.page_to = 10_000
- 设置默认的页面范围
page_from = 0:从第0页开始(Python中通常从0开始计数)page_to = 10_000:到第10,000页结束(使用下划线提高可读性)- 这为PDF处理设置了很大的默认页面范围
安装检查方法 check_installation
python
def check_installation(self) -> bool:
- 定义一个返回布尔值的方法,用于检查必要的依赖是否安装
python
if DocumentConverter is None:
self.logger.warning("[Docling] 'docling' is not importable, please: pip install docling")
return False
- 检查
DocumentConverter类是否可用(是否为None) - 如果不可用,记录警告日志并返回
False - 提示用户需要执行
pip install docling
python
try:
_ = DocumentConverter()
return True
except Exception as e:
self.logger.error(f"[Docling] init DocumentConverter failed: {e}")
return False
- 尝试创建
DocumentConverter的实例来测试其功能 _ = DocumentConverter():创建实例但不保存(使用_表示不关心变量名)- 如果成功,返回
True表示安装正确 - 如果出现异常,记录错误日志并返回
False
实际使用场景
python
# 创建解析器实例
parser = DoclingParser()
# 检查安装
if parser.check_installation():
print("Docling 已正确安装,可以开始解析PDF")
else:
print("需要安装 Docling: pip install docling")
# 访问实例变量
print(f"默认页面范围: {parser.page_from} 到 {parser.page_to}")
print(f"页面图像列表: {parser.page_images}") # 初始为空列表
2、函数方法
(1)images
方法定义
python
def __images__(self, fnm, zoomin: int = 1, page_from=0, page_to=600, callback=None):
def __images__(self, fnm, ...):定义了一个名为__images__的实例方法self: 指向当前对象实例的引用fnm: 文件名或文件内容,可以是字符串路径或字节数据zoomin: int = 1: 缩放因子,默认值为1(原大小)page_from=0: 起始页码,默认从第0页开始page_to=600: 结束页码,默认到第600页callback=None: 回调函数,用于进度通知等,默认为空
页码设置
python
self.page_from = page_from
self.page_to = page_to
- 将传入的起始页码和结束页码保存到实例变量中,供其他方法使用
PDF文件处理
python
try:
opener = pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm))
try:开始异常处理块- 检查
fnm的类型:- 如果是字符串或路径对象(
str, PathLike),直接使用pdfplumber.open(fnm)打开文件 - 否则,假定是字节数据,用
BytesIO(fnm)包装后再用pdfplumber打开
- 如果是字符串或路径对象(
BytesIO用于在内存中处理二进制数据,避免写入临时文件
处理PDF页面
python
with opener as pdf:
pages = pdf.pages[page_from:page_to]
self.page_images = [p.to_image(resolution=72 * zoomin, antialias=True).original for p in pages]
with opener as pdf:使用上下文管理器确保PDF文件正确关闭pages = pdf.pages[page_from:page_to]切片获取指定页码范围内的页面- 列表推导式遍历每个页面:
p.to_image()将PDF页面转换为图像resolution=72 * zoomin设置分辨率(72是标准DPI,乘以缩放因子)antialias=True开启抗锯齿,使图像更平滑.original获取原始图像数据- 结果保存到
self.page_images列表中
异常处理
python
except Exception as e:
self.page_images = []
self.logger.exception(e)
- 如果处理过程中出现任何异常:
- 将
self.page_images设为空列表 - 使用logger记录异常详细信息
- 将
这个方法的主要功能:将PDF文件的指定页面范围转换为图像,并保存到内存中供后续处理使用。
(2)_make_line_tag
方法定义
python
def _make_line_tag(self, bbox: _BBox) -> str:
_make_line_tag:下划线开头表示这是内部方法bbox: _BBox:参数类型注解,接受一个_BBox对象-> str:返回值类型注解,返回字符串
边界框检查
python
if bbox is None:
return ""
- 检查传入的边界框是否为
None - 如果是
None,直接返回空字符串,避免后续操作出错
提取坐标值
python
x0, x1, top, bott = bbox.x0, bbox.x1, bbox.y0, bbox.y1
- 从边界框对象中提取四个坐标值:
x0:左边界 x 坐标x1:右边界 x 坐标top:上边界 y 坐标(原y0)bott:下边界 y 坐标(原y1)
坐标系统转换
python
if hasattr(self, "page_images") and self.page_images and len(self.page_images) >= bbox.page_no:
_, page_height = self.page_images[bbox.page_no-1].size
top, bott = page_height - top, page_height - bott
这是最关键的部分,进行坐标系统的转换:
python
if hasattr(self, "page_images") and self.page_images and len(self.page_images) >= bbox.page_no:
- 检查三个条件:
self是否有page_images属性page_images列表不为空page_images的长度足够(页码从1开始,但列表索引从0开始)
python
_, page_height = self.page_images[bbox.page_no-1].size
- 获取指定页面的图像尺寸
bbox.page_no-1:页码转索引(页码从1开始,列表索引从0开始)image.size返回(width, height),这里用_忽略宽度,只取高度
python
top, bott = page_height - top, page_height - bott
- 坐标系统转换:将 y 坐标从"从上到下"转换为"从下到上"
- 这是因为 PDF 的坐标原点在左下角,而图像处理通常在左上角
生成格式化字符串
python
return "@@{}\t{:.1f}\t{:.1f}\t{:.1f}\t{:.1f}##".format(
bbox.page_no, x0, x1, top, bott
)
- 使用固定格式生成位置标签字符串
- 格式:
@@页码\tx0\tx1\ty0\ty1## {:.1f}:浮点数格式,保留1位小数
实际使用示例
python
# 假设有一个边界框
bbox = _BBox(page_no=1, x0=100.5, y0=200.3, x1=300.7, y1=400.9)
# 假设页面高度为 800
self.page_images = [Image.new('RGB', (600, 800))] # 创建一个 600x800 的测试图像
# 调用方法
result = self._make_line_tag(bbox)
print(result) # 输出: @@1 100.5 300.7 599.7 399.1##
坐标转换的详细过程:
- 原始坐标:
(x0=100.5, y0=200.3, x1=300.7, y1=400.9) - 页面高度:800
- 转换后:
top = 800 - 200.3 = 599.7bott = 800 - 400.9 = 399.1
- 最终坐标:
(100.5, 300.7, 599.7, 399.1)
这个标签的用途
这种格式化的位置标签可能用于:
- 文本定位:标记文本在页面中的具体位置
- 后续处理:其他组件可以解析这个标签来定位元素
- 数据导出:便于存储和传输位置信息
- 可视化:用于高亮显示或交互操作
这个方法的核心作用就是标准化位置信息的表示格式,并处理不同坐标系统之间的转换。
(3)extract_positions
方法定义
python
@staticmethod
def extract_positions(txt: str) -> list[tuple[list[int], float, float, float, float]]:
@staticmethod:静态方法装饰器,表示这个方法不依赖于类实例txt: str:输入参数,包含位置标签的文本- 返回值类型:一个列表,每个元素是元组,包含:
list[int]:页码列表- 4个
float:left, right, top, bottom 坐标
初始化结果列表
python
poss = []
- 初始化一个空列表,用于存储解析后的位置信息
正则表达式匹配
python
for tag in re.findall(r"@@[0-9-]+\t[0-9.\t]+##", txt):
- 使用正则表达式在文本中查找所有符合格式的位置标签
- 正则表达式
r"@@[0-9-]+\t[0-9.\t]+##"解析:@@:匹配开头[0-9-]+:匹配数字和横线(用于页码范围)\t:匹配制表符[0-9.\t]+:匹配数字、小数点和制表符(用于坐标)##:匹配结尾
标签解析
python
pn, left, right, top, bottom = tag.strip("#").strip("@").split("\t")
tag.strip("#"):去掉结尾的##.strip("@"):去掉开头的@@.split("\t"):按制表符分割成5个部分
类型转换
python
left, right, top, bottom = float(left), float(right), float(top), float(bottom)
- 将坐标字符串转换为浮点数
页码处理
python
poss.append(([int(p) - 1 for p in pn.split("-")], left, right, top, bottom))
pn.split("-"):将页码字符串按横线分割(处理页码范围)[int(p) - 1 for p in pn.split("-")]:将页码转换为整数并减1(从1-based转为0-based)- 将解析结果添加到列表中
完整的处理流程
步骤1:原始文档解析
首先有一个函数将PDF/文档转换为包含位置信息的结构化文本:
python
def parse_document_to_annotated_text(self, pdf_path):
"""将PDF文档解析为包含位置标签的文本"""
annotated_text = ""
# 使用docling解析文档
converter = DocumentConverter()
result = converter.convert(pdf_path)
# 遍历文档中的每个元素(文本、表格、图片等)
for element in result.elements:
# 获取元素的位置信息
bbox = _BBox(
page_no=element.page_number,
x0=element.bbox.x0, y0=element.bbox.y0,
x1=element.bbox.x1, y1=element.bbox.y1
)
# 生成位置标签
position_tag = self._make_line_tag(bbox)
# 将元素内容与位置标签结合
if element.type == DoclingContentType.TEXT:
annotated_text += f"{position_tag}{element.text}"
elif element.type == DoclingContentType.TABLE:
# 表格可能转换为Markdown或HTML
table_content = self._table_to_markdown(element.content)
annotated_text += f"{position_tag}{table_content}"
elif element.type == DoclingContentType.IMAGE:
annotated_text += f"{position_tag}[图像: {element.alt_text}]"
return annotated_text
步骤2:实际生成的文本示例
经过上述处理,实际的文本可能是这样的:
python
text = """
@@1\t100.5\t200.5\t300.5\t400.5##RAGFlow 技术文档
@@1\t50.0\t550.0\t450.0\t500.0##本文介绍RAGFlow的核心概念和使用方法。
@@1\t60.0\t540.0\t520.0\t600.0##第一章 概述
@@2\t100.0\t500.0\t100.0\t300.0##RAGFlow是一个基于检索增强生成的文档处理系统。
@@2\t50.0\t550.0\t320.0\t400.0##| 组件 | 功能 |
|------|------|
| 解析器 | 提取文档内容 |
| 检索器 | 查找相关信息 |
@@3\t80.0\t520.0\t150.0\t350.0##[图像: 系统架构图]
@@3\t100.0\t500.0\t400.0\t450.0##第二章 安装部署
"""
步骤3:后续处理流程
然后使用 extract_positions 来提取这些位置信息:
python
class DoclingParser(RAGFlowPdfParser):
def process_document(self, pdf_path):
# 1. 解析文档生成带位置标签的文本
annotated_text = self.parse_document_to_annotated_text(pdf_path)
# 2. 提取所有位置信息
positions = self.extract_positions(annotated_text)
# 3. 同时获取纯文本内容(去掉位置标签)
clean_text = self.remove_position_tags(annotated_text)
return {
'text': clean_text, # 纯文本内容
'positions': positions, # 位置信息
'annotated_text': annotated_text # 带标签的原始文本
}
@staticmethod
def remove_position_tags(text: str) -> str:
"""移除位置标签,返回纯文本"""
return re.sub(r"@@[0-9-]+\t[0-9.\t]+##", "", text)
步骤4:实际使用场景
python
# 完整的使用流程
parser = DoclingParser()
# 处理PDF文档
result = parser.process_document("ragflow_docs.pdf")
print("纯文本内容:")
print(result['text'])
print("\n位置信息:")
for pages, left, right, top, bottom in result['positions']:
print(f"页码: {pages}, 位置: ({left}, {top}) -> ({right}, {bottom})")
print("\n带标签的原始文本:")
print(result['annotated_text'])
设计目的
这种设计的价值在于:
- 位置保持:在提取文本内容的同时保留精确的位置信息
- 可逆处理:可以通过位置信息重新定位到原文
- 多模态处理:统一处理文本、表格、图像等不同类型的内容
- 后续分析:便于实现点击定位、高亮显示、布局分析等功能
(4)crop
方法定义和初始化
python
def crop(self, text: str, ZM: int = 1, need_position: bool = False):
imgs = []
text: 包含位置标签的文本ZM: 缩放因子(可能未使用)need_position: 是否返回位置信息imgs: 存储裁剪出的图像片段
提取位置信息
python
poss = self.extract_positions(text)
if not poss:
return (None, None) if need_position else None
- 从文本中提取所有位置标签
- 如果没有位置信息,直接返回
扩展位置范围
python
GAP = 6
pos = poss[0]
poss.insert(0, ([pos[0][0]], pos[1], pos[2], max(0, pos[3] - 120), max(pos[3] - GAP, 0)))
- 在位置列表开头添加一个扩展区域:
- 使用第一个位置的页码
- 向上扩展120像素(显示更多上文)
- 确保不超出页面边界
python
pos = poss[-1]
poss.append(([pos[0][-1]], pos[1], pos[2], min(self.page_images[pos[0][-1]].size[1], pos[4] + GAP), min(self.page_images[pos[0][-1]].size[1], pos[4] + 120)))
- 在位置列表末尾添加一个扩展区域:
- 使用最后一个位置的页码
- 向下扩展120像素(显示更多下文)
- 确保不超出页面边界
核心裁剪逻辑
python
positions = []
for ii, (pns, left, right, top, bottom) in enumerate(poss):
if bottom <= top:
bottom = top + 4
- 遍历所有位置信息
- 如果底部坐标小于等于顶部,设置最小高度4像素
python
img0 = self.page_images[pns[0]]
x0, y0, x1, y1 = int(left), int(top), int(right), int(min(bottom, img0.size[1]))
crop0 = img0.crop((x0, y0, x1, y1))
imgs.append(crop0)
- 获取第一个页面的图像
- 计算裁剪区域坐标
- 执行裁剪并保存图像
python
if 0 < ii < len(poss)-1:
positions.append((pns[0] + self.page_from, x0, x1, y0, y1))
- 记录原始位置信息(排除首尾的扩展区域)
处理跨页内容
python
remain_bottom = bottom - img0.size[1]
for pn in pns[1:]:
if remain_bottom <= 0:
break
page = self.page_images[pn]
x0, y0, x1, y1 = int(left), 0, int(right), int(min(remain_bottom, page.size[1]))
cimgp = page.crop((x0, y0, x1, y1))
imgs.append(cimgp)
if 0 < ii < len(poss) - 1:
positions.append((pn + self.page_from, x0, x1, y0, y1))
remain_bottom -= page.size[1]
- 处理跨越多页的内容
- 计算剩余需要裁剪的高度
- 在后续页面上继续裁剪
图像合成
python
if not imgs:
return (None, None) if need_position else None
height = sum(i.size[1] + GAP for i in imgs)
width = max(i.size[0] for i in imgs)
pic = Image.new("RGB", (width, int(height)), (245, 245, 245))
- 创建新的空白画布
- 计算总高度(所有图像高度 + 间隔)
- 宽度取最宽图像的宽度
- 背景色为浅灰色 (245, 245, 245)
python
h = 0
for ii, img in enumerate(imgs):
if ii == 0 or ii + 1 == len(imgs):
img = img.convert("RGBA")
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
overlay.putalpha(128)
img = Image.alpha_composite(img, overlay).convert("RGB")
pic.paste(img, (0, int(h)))
h += img.size[1] + GAP
- 将裁剪的图像拼接到画布上
- 对首尾的扩展区域添加半透明遮罩(变暗效果)
- 更新垂直位置
返回值
python
return (pic, positions) if need_position else pic
- 根据参数返回图像和位置信息,或只返回图像
实际使用示例
python
# 假设有一段带位置标签的文本
text_with_positions = "@@1\t100\t300\t200\t250##重要内容@@1\t100\t300\t260\t280##"
# 调用crop方法
result_image, positions = parser.crop(text_with_positions, need_position=True)
# 结果:
# - result_image: 合成的长图像,包含目标区域及其上下文
# - positions: 原始目标区域的位置信息
方法功能总结
这个方法的主要作用是:
- 上下文提取:不仅裁剪目标区域,还包含上下文区域
- 跨页处理:支持跨越多页的内容
- 视觉区分:用遮罩区分原始内容和扩展内容
- 位置追踪:保持原始位置信息的准确性
常用于生成包含上下文引用的图像片段,便于展示和理解文档中的特定内容。
(5)_iter_doc_items
这是一个非常重要的迭代器方法,它负责从Docling解析结果中提取结构化的文档内容。
方法定义
python
def _iter_doc_items(self, doc) -> Iterable[tuple[str, Any, Optional[_BBox]]]:
- 返回一个迭代器,每个元素是三元组:
(内容类型, 内容, 位置信息) doc是Docling库解析文档后返回的对象
第一部分:提取文本内容
python
for t in getattr(doc, "texts", []):
- 遍历文档中的所有文本元素
getattr(doc, "texts", [])安全地获取texts属性,如果没有则返回空列表
python
parent = getattr(t, "parent", "")
ref = getattr(parent, "cref", "")
label = getattr(t, "label", "")
- 获取文本元素的层级信息:
parent: 父元素ref: 父元素的引用路径label: 文本元素的类型标签
python
if (label in ("section_header","text",) and ref in ("#/body",)) or label in ("list_item",):
- 过滤条件 :只处理特定类型的文本
- 主体部分(
#/body)的标题和正文 - 或者列表项
- 主体部分(
python
text = getattr(t, "text", "") or ""
- 获取文本内容,确保不是None
提取位置信息(边界框)
python
bbox = None
if getattr(t, "prov", None):
pn = getattr(t.prov[0], "page_no", None)
bb = getattr(t.prov[0], "bbox", None)
bb = [getattr(bb, "l", None), getattr(bb, "t", None), getattr(bb, "r", None), getattr(bb, "b", None)]
if pn and bb and len(bb) == 4:
bbox = _BBox(page_no=int(pn), x0=bb[0], y0=bb[1], x1=bb[2], y1=bb[3])
prov属性包含位置证明信息- 提取:页码(
page_no)和边界框坐标(l, t, r, b) - 如果信息完整,创建
_BBox对象
python
yield (DoclingContentType.TEXT.value, text, bbox)
- 生成文本内容项:
("text", "文本内容", 位置对象)
第二部分:提取公式内容
python
for item in getattr(doc, "texts", []):
if getattr(item, "label", "") in ("FORMULA",):
- 再次遍历文本元素,寻找公式类型
label为 "FORMULA" 表示数学公式
python
text = getattr(item, "text", "") or ""
bbox = None
if getattr(item, "prov", None):
pn = getattr(item.prov, "page_no", None)
bb = getattr(item.prov, "bbox", None)
bb = [getattr(bb, "l", None), getattr(bb, "t", None), getattr(bb, "r", None), getattr(bb, "b", None)]
if pn and bb and len(bb) == 4:
bbox = _BBox(int(pn), bb[0], bb[1], bb[2], bb[3])
yield (DoclingContentType.EQUATION.value, text, bbox)
- 提取公式内容和位置信息
- 生成公式内容项:
("equation", "公式内容", 位置对象)
实际使用示例
python
# 假设doc是Docling解析后的文档对象
parser = DoclingParser()
for content_type, content, bbox in parser._iter_doc_items(doc):
if content_type == "text":
print(f"文本: {content}")
if bbox:
print(f" 位置: 第{bbox.page_no}页 ({bbox.x0}, {bbox.y0})")
elif content_type == "equation":
print(f"公式: {content}")
处理的数据结构示例
假设Docling解析后的数据结构如下:
python
doc = {
"texts": [
{
"label": "section_header",
"text": "第一章 引言",
"parent": {"cref": "#/body"},
"prov": [{
"page_no": 1,
"bbox": {"l": 100.0, "t": 50.0, "r": 300.0, "b": 80.0}
}]
},
{
"label": "FORMULA",
"text": "E = mc^2",
"prov": {
"page_no": 2,
"bbox": {"l": 150.0, "t": 200.0, "r": 250.0, "b": 220.0}
}
}
]
}
方法的作用总结
这个方法的核心作用是:
- 内容提取:从复杂的Docling解析结果中提取有用的文本和公式
- 内容分类:区分普通文本、标题、列表项、公式等
- 位置关联:将内容与其在文档中的精确位置关联起来
- 数据标准化:统一输出格式,便于后续处理
(6)_transfer_to_sections
这是一个非常重要的方法,它负责将Docling解析的结果转换为统一的章节格式。
方法定义和目的
python
def _transfer_to_sections(self, doc) -> list[tuple[str, str]]:
"""
和 MinerUParser 保持一致:返回 [(section_text, line_tag), ...]
"""
- 方法目标:与
MinerUParser保持输出格式一致 - 返回值:
(章节文本, 位置标签)的元组列表 - 这是标准化输出的关键步骤
初始化结果列表
python
sections: list[tuple[str, str]] = []
- 初始化空列表,用于存储转换后的章节数据
遍历文档项
python
for typ, payload, bbox in self._iter_doc_items(doc):
- 使用前面分析的
_iter_doc_items方法遍历文档内容 - 获取:
(内容类型, 内容, 位置信息)三元组
处理文本内容
python
if typ == DoclingContentType.TEXT.value:
section = payload.strip()
if not section:
continue
- 如果是文本类型 (
"text") - 去除首尾空白字符
- 如果去除后为空字符串,跳过该项
处理公式内容
python
elif typ == DoclingContentType.EQUATION.value:
section = payload.strip()
- 如果是公式类型 (
"equation") - 同样去除空白字符,但允许空公式(可能包含特殊符号)
跳过其他类型
python
else:
continue
- 跳过图片、表格等其他类型的内容
- 只保留文本和公式
生成位置标签
python
tag = self._make_line_tag(bbox) if isinstance(bbox, _BBox) else ""
- 如果有有效的边界框对象,生成位置标签
- 如果没有位置信息,使用空字符串
添加到结果列表
python
sections.append((section, tag))
- 将
(文本内容, 位置标签)添加到结果列表
返回结果
python
return sections
实际转换示例
假设 _iter_doc_items 返回:
python
[
("text", "第一章 引言", _BBox(page_no=1, x0=100, y0=50, x1=300, y1=80)),
("text", "这是正文内容", _BBox(page_no=1, x0=50, y0=100, x1=550, y1=120)),
("equation", "E = mc^2", _BBox(page_no=2, x0=150, y0=200, x1=250, y1=220)),
("image", "[图片数据]", None) # 这个会被跳过
]
经过 _transfer_to_sections 处理后:
python
[
("第一章 引言", "@@1\t100.0\t300.0\t...##"),
("这是正文内容", "@@1\t50.0\t550.0\t...##"),
("E = mc^2", "@@2\t150.0\t250.0\t...##")
]
设计意图和重要性
1. 标准化输出
python
# 不同的解析器都返回相同格式
miner_parser = MinerUParser()
docling_parser = DoclingParser()
# 统一接口,便于后续处理
miner_sections = miner_parser.parse(pdf_path) # 返回 [(text, tag), ...]
docling_sections = docling_parser._transfer_to_sections(doc) # 返回 [(text, tag), ...]
2. 数据过滤和清理
- 过滤掉空文本
- 跳过不需要的内容类型(图片、表格等)
- 统一处理文本格式
3. 位置信息保留
python
# 后续可以轻松还原位置信息
for section_text, position_tag in sections:
if position_tag:
positions = self.extract_positions(position_tag)
# 可以进行精确定位或图像裁剪
4. 支持多种使用场景
场景1:纯文本处理
python
sections = self._transfer_to_sections(doc)
pure_text = "\n".join([text for text, tag in sections])
场景2:带位置的文本检索
python
sections = self._transfer_to_sections(doc)
for text, tag in sections:
if "关键词" in text:
# 找到相关内容,可以通过tag精确定位
cropped_image = self.crop(tag + text)
场景3:多解析器统一处理
python
def parse_pdf_unified(pdf_path):
# 尝试多种解析器,但输出格式统一
parsers = [MinerUParser(), DoclingParser()]
for parser in parsers:
try:
if hasattr(parser, '_transfer_to_sections'):
sections = parser._transfer_to_sections(doc)
else:
sections = parser.parse(pdf_path)
return sections
except Exception as e:
continue
这个方法的核心价值在于创建了一个统一的接口,让不同的PDF解析器能够以相同的格式输出结果,极大地简化了后续的处理流程。
(7)cropout_docling_table
这是一个专门用于裁剪文档中表格图像的方法!
方法定义
python
def cropout_docling_table(self, page_no: int, bbox: tuple[float, float, float, float], zoomin: int = 1):
- 专门用于裁剪表格的方法
page_no: 表格所在的页码(从1开始)bbox: 表格的边界框坐标(left, top, right, bottom)zoomin: 缩放因子(可能未使用)
检查图像数据
python
if not getattr(self, "page_images", None):
return None, ""
- 检查
self.page_images是否存在且不为空 - 如果没有图像数据,返回空结果
计算页面索引
python
idx = (page_no - 1) - getattr(self, "page_from", 0)
if idx < 0 or idx >= len(self.page_images):
return None, ""
page_no - 1: 将页码转换为0-based索引- self.page_from: 考虑页面范围的偏移量- 检查索引是否在有效范围内
获取页面图像和尺寸
python
page_img = self.page_images[idx]
W, H = page_img.size
left, top, right, bott = bbox
- 获取指定页面的图像
- 获取图像的宽度和高度
- 解构边界框坐标
坐标系统转换
python
x0 = float(left)
y0 = float(H - top) # 关键:y坐标转换
x1 = float(right)
y1 = float(H - bott) # 关键:y坐标转换
- 关键步骤:将y坐标从"从上到下"转换为"从下到上"
- PDF坐标系统:原点在左下角,y向上增加
- 图像坐标系统:原点在左上角,y向下增加
H - top和H - bott完成坐标转换
边界检查和安全处理
python
x0, y0 = max(0.0, min(x0, W - 1)), max(0.0, min(y0, H - 1))
x1, y1 = max(x0 + 1.0, min(x1, W)), max(y0 + 1.0, min(y1, H))
- 确保坐标不超出图像边界
max(0.0, min(x0, W - 1)): 限制在 [0, 宽度-1] 范围内max(x0 + 1.0, ...): 确保宽度至少为1像素
执行裁剪操作
python
try:
crop = page_img.crop((int(x0), int(y0), int(x1), int(y1))).convert("RGB")
except Exception:
return None, ""
- 使用PIL的
crop方法裁剪图像 .convert("RGB"): 确保输出为RGB格式- 异常处理:如果裁剪失败返回空结果
返回结果
python
pos = (page_no-1 if page_no>0 else 0, x0, x1, y0, y1)
return crop, [pos]
- 返回裁剪的图像和位置信息
- 位置信息使用0-based页码
实际使用示例
python
# 假设在文档处理过程中发现表格
parser = DoclingParser()
parser.__images__("document.pdf") # 先加载图像
# 表格位置信息(来自Docling解析)
table_page_no = 2
table_bbox = (100.5, 200.3, 400.7, 500.9) # (left, top, right, bottom)
# 裁剪表格图像
table_image, positions = parser.cropout_docling_table(table_page_no, table_bbox)
if table_image:
table_image.save("extracted_table.png")
print(f"表格裁剪成功,位置: {positions[0]}")
坐标转换的详细过程
假设:
- 页面高度 H = 800
- 表格边界框: (100, 200, 400, 500)
转换过程:
原始PDF坐标:
top = 200 (距离页面顶部的距离)
bottom = 500 (距离页面顶部的距离)
转换后图像坐标:
y0 = 800 - 200 = 600 (距离图像顶部的距离)
y1 = 800 - 500 = 300 (距离图像顶部的距离)
最终裁剪区域: (100, 300, 400, 600)
与之前crop方法的区别
| 特性 | crop 方法 |
cropout_docling_table 方法 |
|---|---|---|
| 输入 | 带位置标签的文本 | 直接的页码和边界框 |
| 输出 | 合成的长图像 | 单个表格图像 |
| 用途 | 文本段落的可视化 | 表格提取和展示 |
| 坐标处理 | 在_make_line_tag中处理 |
在方法内部处理 |
这个方法专门为表格提取设计,是Docling文档处理流程中的重要组成部分!
(8)_transfer_to_tables
这是一个专门用于提取文档中表格和图片的方法!
方法定义
python
def _transfer_to_tables(self, doc):
- 专门处理文档中的表格和图片
- 返回统一格式的数据,便于后续处理
第一部分:提取表格
python
tables = []
for tab in getattr(doc, "tables", []):
- 初始化结果列表
- 遍历文档中的所有表格,安全地获取
tables属性
提取表格位置信息
python
img = None
positions = ""
if getattr(tab, "prov", None):
pn = getattr(tab.prov[0], "page_no", None)
bb = getattr(tab.prov[0], "bbox", None)
if pn is not None and bb is not None:
left = getattr(bb, "l", None)
top = getattr(bb, "t", None)
right = getattr(bb, "r", None)
bott = getattr(bb, "b", None)
if None not in (left, top, right, bott):
img, positions = self.cropout_docling_table(int(pn), (float(left), float(top), float(right), float(bott)))
- 提取表格的位置证明信息(
prov) - 获取页码和边界框坐标
- 如果所有坐标都有效,调用
cropout_docling_table裁剪表格图像
提取表格HTML内容
python
html = ""
try:
html = tab.export_to_html(doc=doc)
except Exception:
pass
- 尝试将表格导出为HTML格式
- 异常处理:如果导出失败,保持空字符串
保存表格数据
python
tables.append(((img, html), positions if positions else ""))
- 将表格数据保存为:
((图像, HTML内容), 位置信息)
第二部分:提取图片
python
for pic in getattr(doc, "pictures", []):
- 遍历文档中的所有图片
提取图片位置信息
python
img = None
positions = ""
if getattr(pic, "prov", None):
pn = getattr(pic.prov[0], "page_no", None)
bb = getattr(pic.prov[0], "bbox", None)
if pn is not None and bb is not None:
left = getattr(bb, "l", None)
top = getattr(bb, "t", None)
right = getattr(bb, "r", None)
bott = getattr(bb, "b", None)
if None not in (left, top, right, bott):
img, positions = self.cropout_docling_table(int(pn), (float(left), float(top), float(right), float(bott)))
- 与表格类似的逻辑,提取图片位置并裁剪图像
提取图片标题
python
captions = ""
try:
captions = pic.caption_text(doc=doc)
except Exception:
pass
- 尝试提取图片的标题文本
- 异常处理:如果提取失败,保持空字符串
保存图片数据
python
tables.append(((img, [captions]), positions if positions else ""))
- 将图片数据保存为:
((图像, [标题列表]), 位置信息)
返回结果
python
return tables
实际处理结果示例
假设文档包含:
- 1个表格(第2页)
- 2张图片(第3页)
处理后的 tables 列表:
python
[
# 表格数据
(
(表格图像对象, "<table><tr><td>数据</td></tr></table>"),
[(1, 100.0, 400.0, 300.0, 600.0)] # 位置信息
),
# 第一张图片数据
(
(图片1图像对象, ["图1: 系统架构"]), # 图像和标题
[(2, 50.0, 300.0, 200.0, 400.0)] # 位置信息
),
# 第二张图片数据
(
(图片2图像对象, ["图2: 性能对比"]),
[(2, 350.0, 600.0, 200.0, 400.0)]
)
]
设计意图分析
1. 统一输出格式
python
# 统一的返回格式,便于后续处理
for (img, content), positions in tables:
if isinstance(content, str):
# 处理表格(HTML内容)
process_table(content, img)
elif isinstance(content, list):
# 处理图片(标题列表)
process_image(content[0], img)
2. 多模态数据提取
- 表格:图像 + HTML结构化数据
- 图片:图像 + 文本标题
- 两者都包含精确的位置信息
3. 容错处理
- 大量使用
getattr(..., None)避免属性不存在时报错 - try-except 块处理可能失败的操作
4. 与前面方法的关系
完整文档处理流程:
doc (原始Docling对象)
↓
_transfer_to_sections() # 提取文本和公式 → 用于文本处理
_transfer_to_tables() # 提取表格和图片 → 用于视觉元素处理
↓
统一的数据格式 → 便于RAG系统索引和使用
实际应用场景
python
def process_document_completely(self, pdf_path):
"""完整的文档处理流程"""
converter = DocumentConverter()
doc = converter.convert(pdf_path)
# 加载页面图像
self.__images__(pdf_path)
# 提取所有类型的内容
sections = self._transfer_to_sections(doc) # 文本内容
tables_and_images = self._transfer_to_tables(doc) # 表格和图片
return {
'text_content': sections,
'visual_content': tables_and_images,
'complete_coverage': True # 确保覆盖所有内容类型
}
这个方法确保了文档中的所有视觉元素(表格、图片)都能被正确提取并标准化,为后续的文档理解和检索提供完整的数据支持!
(9)parse_pdf
这是整个Docling解析器的核心入口方法!它整合了我们前面分析的所有组件,完成了从PDF到结构化数据的完整转换流程。
方法定义和参数
python
def parse_pdf(
self,
filepath: str | PathLike[str],
binary: BytesIO | bytes | None = None,
callback: Optional[Callable] = None,
*,
output_dir: Optional[str] = None,
lang: Optional[str] = None,
method: str = "auto",
delete_output: bool = True,
):
- filepath: PDF文件路径
- binary: 可选的二进制数据(内存中的PDF)
- callback: 进度回调函数
- output_dir: 临时输出目录
- lang: 语言设置
- method: 解析方法
- delete_output: 是否删除临时文件
1. 安装检查
python
if not self.check_installation():
raise RuntimeError("Docling not available, please install `docling`")
- 检查Docling依赖是否安装
- 如果没有安装,抛出明确错误
2. 处理二进制输入
python
if binary is not None:
tmpdir = Path(output_dir) if output_dir else Path.cwd() / ".docling_tmp"
tmpdir.mkdir(parents=True, exist_ok=True)
name = Path(filepath).name or "input.pdf"
tmp_pdf = tmpdir / name
with open(tmp_pdf, "wb") as f:
if isinstance(binary, (bytes, bytearray)):
f.write(binary)
else:
f.write(binary.getbuffer())
src_path = tmp_pdf
- 如果提供的是二进制数据,先写入临时文件
- 创建临时目录,确保父目录存在
- 将二进制数据写入临时PDF文件
3. 处理文件路径输入
python
else:
src_path = Path(filepath)
if not src_path.exists():
raise FileNotFoundError(f"PDF not found: {src_path}")
- 如果是文件路径,检查文件是否存在
4. 进度回调
python
if callback:
callback(0.1, f"[Docling] Converting: {src_path}")
- 通知调用方解析开始(10%进度)
5. 加载页面图像
python
try:
self.__images__(str(src_path), zoomin=1)
except Exception as e:
self.logger.warning(f"[Docling] render pages failed: {e}")
- 调用前面分析的
__images__方法加载PDF页面图像 - 为后续的
cropout_docling_table准备图像数据 - 异常处理:即使图像加载失败,也继续文本解析
6. 使用Docling解析文档
python
conv = DocumentConverter()
conv_res = conv.convert(str(src_path))
doc = conv_res.document
if callback:
callback(0.7, f"[Docling] Parsed doc: {getattr(doc, 'num_pages', 'n/a')} pages")
- 创建Docling转换器
- 解析PDF文档,得到复杂的结果对象
- 进度回调(70%进度)
7. 提取结构化数据
python
sections = self._transfer_to_sections(doc)
tables = self._transfer_to_tables(doc)
if callback:
callback(0.95, f"[Docling] Sections: {len(sections)}, Tables: {len(tables)}")
- 关键步骤:使用前面分析的两个方法提取数据
_transfer_to_sections: 提取文本和公式_transfer_to_tables: 提取表格和图片- 进度回调(95%进度)
8. 清理临时文件
python
if binary is not None and delete_output:
try:
Path(src_path).unlink(missing_ok=True)
except Exception:
pass
- 如果是二进制输入且设置了删除标志,清理临时文件
9. 完成并返回结果
python
if callback:
callback(1.0, "[Docling] Done.")
return sections, tables
- 最终进度回调(100%)
- 返回提取的所有数据
完整的数据流
输入 (PDF文件/二进制数据)
↓
临时文件处理 (如果需要)
↓
self.__images__() → 加载页面图像
↓
DocumentConverter().convert() → 原始doc对象
↓
_transfer_to_sections() → 文本数据
_transfer_to_tables() → 视觉数据
↓
输出 (sections, tables)
实际使用示例
python
# 使用文件路径
parser = DoclingParser()
sections, tables = parser.parse_pdf("document.pdf")
# 使用二进制数据
with open("document.pdf", "rb") as f:
binary_data = f.read()
sections, tables = parser.parse_pdf("document.pdf", binary=binary_data)
# 带进度回调
def progress_callback(progress, message):
print(f"{progress*100:.0f}%: {message}")
sections, tables = parser.parse_pdf("document.pdf", callback=progress_callback)
返回的数据结构
python
# sections: 来自 _transfer_to_sections
[
("章节标题", "@@1\t100\t200\t300\t400##"),
("正文内容", "@@1\t50\t550\t100\t120##"),
("数学公式", "@@2\t150\t250\t200\t220##")
]
# tables: 来自 _transfer_to_tables
[
((表格图像, "<table>...</table>"), "位置信息"),
((图片图像, ["图片标题"]), "位置信息")
]
设计价值
这个 parse_pdf 方法的价值在于:
- 统一入口: 封装了所有复杂的处理逻辑
- 灵活输入: 支持文件路径和二进制数据
- 进度反馈: 通过callback提供实时进度
- 资源管理: 自动处理临时文件清理
- 错误恢复: 即使部分步骤失败也能继续
- 完整提取: 返回文档的所有重要内容
这就是整个Docling解析器的"大脑",它将我们前面分析的所有零散组件整合成了一个完整、健壮的PDF解析流程!
总结:
各函数的职责总结
| 函数 | 主要职责 | 依赖 |
|---|---|---|
parse_pdf |
总协调,流程控制 | 所有其他函数 |
check_installation |
环境检查 | 无 |
__images__ |
加载页面图像 | 无 |
_iter_doc_items |
提取原始文档项 | doc对象 |
_transfer_to_sections |
标准化文本数据 | _iter_doc_items, _make_line_tag |
_transfer_to_tables |
标准化视觉数据 | cropout_docling_table |
_make_line_tag |
位置信息编码 | _BBox, 坐标转换 |
extract_positions |
位置信息解码 | 正则表达式 |
cropout_docling_table |
裁剪具体区域 | self.page_images |
crop |
裁剪文本区域 | self.page_images, extract_positions |
对于文本内容:
doc (结构数据)
↓
_iter_doc_items() → 提取文本和位置
↓
_make_line_tag() → 生成标准化标签
↓
sections = [("文本", "@@页码\t坐标##")]
对于表格/图片:
doc (结构数据) self.page_images (图像数据)
↓ ↓
_transfer_to_tables() → cropout_docling_table()
↓ ↓
((图像, 内容), 位置) ←─────────── 裁剪结果