引言
书籍文件往往篇幅巨大、结构复杂,不同章节、目录、致谢等混杂在同一文档中。RAGFlow 在处理 “book” 类文档时,整合了多种解析器(docx、pdf、txt、html、doc),通过自动识别版面结构、过滤非正文、并结合视觉模型生成图片摘要,实现对长文档的精准切分与高质量抽取。本篇将带你逐步拆解源码,了解 RAGFlow 如何优雅地解析一本“书”。
省流版
核心逻辑
book 模式 面向长篇书籍类文档(docx/pdf/txt/html/doc),通过对不同文件类型自动选择最优解析策略,融合文本语义分块与视觉摘要,实现高质量内容抽取。其目标是:在保持上下文完整性的同时最大限度地过滤无效内容(如目录、致谢页等),为后续知识检索提供干净、结构化的输入。
设计亮点
- 语义级正文过滤:通过正则与上下文规则剔除目录、致谢等非正文部分;
- 文本块连续性优化:pdf 文档通过 _naive_vertical_merge 与 _merge_with_same_bullet 修复 OCR 切分断裂、项目符号分裂等问题。
手撕版
书籍的解析支持文件格式为 docx|pdf|txt|html|doc 五种格式。其中官方建议由于一本书篇幅较长,并非所有部分都有用,如果是 PDF 格式,请为每本书设置页码范围,以消除负面影响并节省计算时间。
if re.search(r"\.docx$", filename, re.IGNORECASE):
...
elif re.search(r"\.pdf$", filename, re.IGNORECASE):
...
elif re.search(r"\.txt$", filename, re.IGNORECASE):
...
elif re.search(r"\.(htm|html)$", filename, re.IGNORECASE):
...
elif re.search(r"\.doc$", filename, re.IGNORECASE):
...
1. docx
1.1 解析器初始化
docx 的解析器是直接引用的 naive 模式下的 docx 解析器,主要对 docx 文档中的段落和表格分别进行处理。详细的 docx 解析器技术拆解可参考《naive parser 语义切块(docx 篇)》。
doc_parser = naive.Docx()
段落和表格处理后的最终输出格式如下:
sections, tbls = doc_parser(
filename, binary=binary, from_page=from_page, to_page=to_page)
'''
# sections
[
(清洗后的文本, 合并后的图片对象, 样式名),
("这是段落文本", PILImage对象, "Normal"),
("这是图注", PILImage对象, "Caption"),
...
]
# tbls
[
((None, "<table>...</table>"), ""),
((None, "<table>...</table>"), ""),
...
]
'''
1.2 目录表格 过滤
过滤 docx 文档中的目录表格。
tbls = _remove_docx_toc_tables(tbls)
通过关键字正则判断是否是目录表格,满足其中一条则认为是目录表格。
_TOC_TABLE_KEYWORDS = re.compile(r"(table of contents|目录|目次)", re.IGNORECASE)
_TOC_DOT_PATTERN = re.compile(r"[\.|·]{3,}\s*\d+")
1.3 非正文内容过滤
过滤解析后文本中的非正文内容,例如:目录,致谢等。
remove_contents_table(sections, eng=is_english(
random_choices([t for t, _ in sections], k=200)))
通过关键字正则判断是否是非正文内容。
re.match(r"(contents|目录|目次|table of contents|致谢|acknowledge)$",
re.sub(r"( | |\u3000)+", "", get(i).split("@@")[0], flags=re.IGNORECASE))
1.4 使用视觉模型识别并总结图片摘要
使用 VLM 对文档中的图片进行摘要总结,并以规定格式输出。详细的 vision_figure_parser_docx_wrapper 技术拆解可参考《naive parser 语义切块(docx 篇)》。
tbls=vision_figure_parser_docx_wrapper(sections=sections,tbls=tbls,callback=callback,**kwargs)
将包含图片信息的对象数组转换成 figures_data,如以下格式:
(
(figure_data[1], [figure_data[0]]), # 原始图片信息,图片描述信息
[(0, 0, 0, 0, 0)], # 位置信息
)
1.5 移除正文中的图片内容
sections=[(item[0],item[1] if item[1] is not None else "") for item in sections if not isinstance(item[1], Image.Image)]
docx 处理流程结束后输出,sections(正文文本内容)和 tbls(表格内容,图片内容)。
2. pdf
2.1 布局识别器
与 naive parser 下 pdf 文档的处理一样,分为 DeepDOC 和 Plain Text 两种布局识别器。
if parser_config.get("layout_recognize", "DeepDOC") == "Plain Text":
pdf_parser = PlainParser()
else:
pdf_parser = Pdf()
Plain Text 布局识别器实现请参考《naive parser 语义切块(pdf 篇)》 下【Plain Text 布局识别器】模块。
2.2 DeepDOC 布局识别器
与《naive parser 语义切块(pdf 篇)》中相同,Pdf 继承基类 PdfParser。
pdf_parser = Pdf()
class Pdf(PdfParser)
PdfParser 基类的核心功能在《naive parser 语义切块(PDF 篇)》中已有详细说明,这里作简要概述:
-
__images__:负责将 PDF 页面数字化,生成可结构化的页面图像数据,为后续的布局分析与表格提取奠定基础。
-
_layouts_rec:基于页面图像与 OCR 文本框信息,执行每页的版面分析与坐标重建。
-
_table_transformer_job:针对识别出的表格区域,提取和解析表格内容,实现表格结构化处理。
-
_text_merge:通过规则合并相邻文本块,解决 OCR 输出中文本碎片化的问题。
-
_concat_downward:按照阅读顺序(Y 坐标从上到下,X 坐标从左到右)对文本框进行排序,恢复自然的文本流。
-
_filter_forpages:检测并过滤非正文页面,如目录页、致谢页等,以提升后续文本分析质量。
-
_extract_table_figure:负责表格与图像的提取与输出,支持跨页表格合并及图像截取。
-
__filterout_scraps:对碎片化文本进行二次清理与组装,进一步优化 OCR 文本的完整性。
2.2.1 Pdf 类
Pdf 类作为入口点,调用 PdfParser 中提供的功能实现整个复杂的文档处理流程,并记录了各阶段耗时,解析进度等信息,这点与 naive parser 下的 Pdf 类职能一致,但具体实现内容存在差异。
class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
# 静态的 pdf 页面转换为可结构化数据
self.__images__(
filename if not binary else binary,
zoomin,
from_page,
to_page,
callback)
# 对文档每页的布局分析,和坐标重建
self._layouts_rec(zoomin)
# 表格数据内容提取
self._table_transformer_job(zoomin)
# 基于规则合并文本
self._text_merge()
# 提取表格,图片内容输出
tbls = self._extract_table_figure(True, zoomin, True, True)
self._naive_vertical_merge()
# 检测并过滤非正文页面
self._filter_forpages()
self._merge_with_same_bullet()
callback(0.8, "Text extraction ({:.2f}s)".format(timer() - start))
return [(b["text"] + self._line_tag(b, zoomin), b.get("layoutno", ""))
for b in self.boxes], tbls
2.2.1.1 _naive_vertical_merge
其中 _naive_vertical_merge 主要目的是将同一列中垂直方向相邻的文本框进行合并。
先过滤无效文本:
1)跨页数字符号过滤:移除跨页的页码、编号等无关文本
if b["page_number"] < b_["page_number"] and re.match(r"[0-9 •一—-]+$", b["text"]):
bxs.pop(i)
continue
2)空文本过滤
if not b["text"].strip():
bxs.pop(i)
continue
再通过文本框布局识别,确认文本布局是否符合合并规则:
3)布局一致性检查:确保合并的文本框属于同一布局区域
if not b["text"].strip() or b.get("layoutno") != b_.get("layoutno"):
i += 1
continue
4)垂直距离阈值检查:防止合并距离过远的文本框
if b_["top"] - b["bottom"] > mh * 1.5:
i += 1
continue
5)水平重叠度检查:确保文本框在水平方向有足够重叠
overlap = max(0, min(b["x1"], b_["x1"]) - max(b["x0"], b_["x0"]))
if overlap / max(1, min(b["x1"] - b["x0"], b_["x1"] - b_["x0"])) < 0.3:
i += 1
continue
最后通过上述筛选的文本框,对其文本语义连续性分析,决定是否需要合并。
# 支持合并的特征(连接性标点)
concatting_feats = [
b["text"].strip()[-1] in ",;:'\",、‘“;:-",
len(b["text"].strip()) > 1 and b["text"].strip()[-2] in ",;:'\",‘“、;:",
b_["text"].strip() and b_["text"].strip()[0] in "。;?!?”)),,、:",
]
# 反对合并的特征
feats = [
b.get("layoutno", 0) != b_.get("layoutno", 0),
b["text"].strip()[-1] in "。?!?",
self.is_english and b["text"].strip()[-1] in ".!?",
b["page_number"] == b_["page_number"] and b_["top"] - b["bottom"] > self.mean_height[b["page_number"] - 1] * 1.5,
b["page_number"] < b_["page_number"] and abs(b["x0"] - b_["x0"]) > self.mean_width[b["page_number"] - 1] * 4,
]
# 强制分离特征
detach_feats = [b["x1"] < b_["x0"], b["x0"] > b_["x1"]]
if (any(feats) and not any(concatting_feats)) or any(detach_feats):
i += 1
continue
2.2.1.2 _merge_with_same_bullet
将具有相同项目符号的连续文本框合并为一个文本块,保持项目符号列表的结构。
pdf 中可能存在以下项目列表, 一个完整的项目符号列表可能被识别为多个独立的文本框,_merge_with_same_bullet 主要是将同一个项目列表内容进行合并。
# 带有项目符号列表示例
• 项目一:产品介绍
• 项目二:技术规格
• 项目三:价格信息
2.3 使用视觉模型识别并总结图片摘要
与 docx 处理一致,需要使用 VLM 对文档中的图片进行摘要总结,并以规定格式输出。
tbls=vision_figure_parser_pdf_wrapper(tbls=tbls,callback=callback,**kwargs)
3. TXT
与 naive 模式下获取 txt 文档方案一致,使用 get_text。如果传入的二进制内容,则使用从 rag.nlp 引入的方式自动推断出正确的编码,进行解码;否则直接从文件路径读取文本进行拼接返回。
txt = get_text(filename, binary)
与 docx 文档处理方案一致,获取文本后过滤文本中的非正文内容,例如:目录,致谢等。
remove_contents_table(sections, eng=is_english(
random_choices([t for t, _ in sections], k=200)))
4. HTML
与 naive 模式下处理 html 文档方案一致,使用 HtmlParser 解析器进行文档解析。详细技术实现可参考《naive parser 语义切块(html & json & doc 篇)》
sections = HtmlParser()(filename, binary)
与 docx 文档处理方案一致,获取文本后过滤文本中的非正文内容,例如:目录,致谢等。
remove_contents_table(sections, eng=is_english(
random_choices([t for t, _ in sections], k=200)))
5. doc
与 naive 模式下处理 doc 文档方案一致,详细技术实现可参考《naive parser 语义切块(html & json & doc 篇)》
doc_parsed = parser.from_buffer(binary)
与 docx 文档处理方案一致,获取文本后过滤文本中的非正文内容,例如:目录,致谢等。
remove_contents_table(sections, eng=is_english(
random_choices([t for t, _ in sections], k=200)))