news 2026/10/14 13:17:57

基于bbox与XY-cut的PDF多栏解析与水印剔除实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于bbox与XY-cut的PDF多栏解析与水印剔除实战

1. 为什么单栏解析在真实 PDF 面前会翻车

做过 RAG 文档解析的朋友大概率都经历过这个场景:拿一份排版规整的单栏 PDF 测试,解析效果堪称完美,段落顺序正确、表格结构清晰,心里美滋滋觉得大功告成。结果换一份真实的学术论文或者企业年报进来,整个链路直接崩掉——正文里混进了页眉的期刊名、页脚的页码、侧边栏的作者简介,甚至水印文字被当成正文切成了碎片塞进向量库。检索的时候返回一堆莫名其妙的短句,回答质量断崖式下跌。

这个问题的根源在于,绝大多数 PDF 解析教程默认了一个假设:页面上的文字是从上到下、从左到右线性排列的。但真实世界的 PDF 完全不是这么回事。多栏排版、浮动图表、页眉页脚、水印、脚注,这些东西在 PDF 内部都是以绝对坐标定位的文本块,它们之间没有天然的阅读顺序。你不做版面分析,直接按文本流顺序抽取,得到的必然是一锅乱炖。

这一篇要聊的就是怎么用bbox(bounding box,边界框)这个武器,配合XY-cut算法和PyMuPDF,把多栏排版和水印 PDF 这两个硬骨头啃下来。核心思路是:先拿到页面上每个文本块的坐标信息,然后通过几何关系推断出正确的阅读顺序,最后把水印这类干扰项识别并剔除。这套方法不依赖任何深度学习模型,纯几何计算,速度快、可解释性强,特别适合在 RAG 的文档预处理阶段做第一道清洗。

适合谁看?如果你正在搭建 RAG 知识库,被 PDF 解析的版面问题折磨过,或者你用过 PyMuPDF 但只停留在get_text()这个层面,那这篇内容应该能帮你打开新思路。我会把参数怎么调、阈值怎么定、踩过哪些坑都讲清楚,代码可以直接抄。

2. bbox 与 XY-cut 的核心原理拆解

2.1 bbox 到底是什么,为什么它是版面分析的基石

bbox 说白了就是一个矩形框的坐标表示,通常用四个数描述:(x0, y0, x1, y1),分别代表左上角和右下角的坐标。在 PDF 的坐标系里,原点一般在页面左下角,x 轴向右,y 轴向上。不过 PyMuPDF 为了方便开发者,把坐标系翻转成了左上角为原点、y 轴向下,这一点在写代码的时候要特别注意,不然算出来的上下关系全是反的。

PyMuPDF 里获取 bbox 的方式很直接。用page.get_text("dict")拿到的是一个嵌套字典,里面blocks列表的每个元素就是一个文本块,每个块有自己的bbox,块里面还有lines,每行有bbox,每行里面还有spans,每个 span 也有bbox。这个层级结构非常关键,因为不同粒度的分析需要用到不同层级的 bbox。做栏检测用 block 级别的就够了,做行内文字对齐可能要用到 span 级别。

为什么 bbox 这么重要?因为它是页面上所有视觉元素的“身份证”。有了坐标,你就能回答一系列关键问题:这个文本块在页面的左半边还是右半边?它和另一个块是上下关系还是左右关系?它的宽度是否横跨了整个页面(可能是标题或页眉)?它的高度是否异常小(可能是水印或脚注)?这些问题单靠文本内容是回答不了的,必须借助几何信息。

2.2 XY-cut 算法的直觉理解与适用边界

XY-cut 的核心思想可以用切蛋糕来类比。假设你面前有一块长方形蛋糕,上面摆着各种装饰。你想按阅读顺序把装饰一个个取下来。最自然的做法是:先看能不能竖着切一刀,把蛋糕分成左右两块,使得这一刀不切到任何装饰;如果不能,再横着切一刀,分成上下两块。切完之后对每一块递归地重复这个过程,直到每块里只剩一个装饰为止。最后按切割的层次结构,就能还原出一个合理的阅读顺序。

对应到 PDF 页面上,“装饰”就是文本块,“切一刀”就是找一条水平或垂直的空白缝隙,这条缝隙不穿过任何文本块的 bbox。优先做垂直切割(因为多栏排版更常见),垂直切不动了再做水平切割。递归下去,最终每个叶子节点就是一个独立的文本块,按深度优先遍历的顺序输出,就是阅读顺序。

XY-cut 的优势在于简单、快速、无需训练。但它的边界也很明显:遇到不规则的版面(比如文字环绕图片、斜排文字、复杂的表格嵌套)就容易失效。另外,它对空白缝隙的判定依赖一个阈值——缝隙宽度小于多少才算“切不动”。这个阈值设大了会切得太碎,设小了又切不开。我的经验是,垂直切割的缝隙阈值设为页面宽度的 3% 到 5% 比较稳妥,水平切割的阈值可以稍微小一点,2% 到 3% 即可。

还有一个容易被忽略的点:XY-cut 对页眉页脚的处理。页眉页脚通常是横跨整个页面宽度的,它们会阻断垂直切割。所以正确的做法是先把页眉页脚区域识别出来并剔除,再对剩余区域做 XY-cut。识别方法后面会详细讲。

2.3 PyMuPDF 在版面分析中的角色定位

PyMuPDF(也叫 fitz)在整套方案里扮演的是“数据提供者”的角色。它负责把 PDF 页面渲染成可操作的数据结构,包括文本块、图片、绘图路径等。相比 pdfplumber、pdfminer 这些库,PyMuPDF 的优势是速度快、API 简洁、对坐标信息的暴露足够充分。

具体来说,page.get_text("dict")返回的结构里,每个 block 除了 bbox 还有type字段,0 表示文本块,1 表示图片块。这个信息在做版面分析时很有用,因为图片块的位置会影响文本块的阅读顺序判断。另外,page.get_text("rawdict")会返回更细粒度的字符级信息,包括每个字符的 bbox,这在处理水印识别时特别有用——水印文字往往有特定的旋转角度或透明度,字符级的坐标能帮你更精确地定位它们。

PyMuPDF 还有一个隐藏利器:page.get_texttrace()。这个接口返回的是文本的底层绘制指令,包含每个字符的绘制顺序、颜色、透明度等信息。水印通常是用低透明度或浅灰色绘制的,通过 get_texttrace 可以拿到这些属性,从而把水印和正文区分开。不过这个接口在旧版本里可能不存在,建议用 1.23 以上的版本。

3. 多栏排版的识别与阅读顺序重建

3.1 从文本块坐标推断栏结构

拿到页面上所有文本块的 bbox 之后,第一步是判断这个页面到底是几栏。最朴素的方法是看文本块的 x 坐标分布。把所有文本块的 x0(左边界)拿出来,做一个一维的聚类。如果是单栏,x0 会集中在一个很小的范围内;如果是双栏,x0 会明显分成两簇;三栏就是三簇。

聚类算法不需要太复杂,简单的直方图统计就够了。把页面宽度分成 100 个桶,统计每个桶里落了多少个文本块的 x0,然后找直方图里的峰值。峰值之间的间距如果接近页面宽度的一半,基本可以判定是双栏。这里有个经验值:如果两个峰值的间距小于页面宽度的 20%,那可能只是段落缩进造成的,不算真正的分栏。

但光看 x0 还不够,因为有些文本块可能横跨两栏(比如标题、跨栏图表)。所以还需要结合文本块的宽度来判断。如果一个文本块的宽度超过了页面宽度的 60%,它大概率是跨栏元素,不应该参与栏的划分。把这些跨栏元素先拎出来,剩下的文本块再做聚类,准确率会高很多。

3.2 用 XY-cut 递归切割确定阅读顺序

确定了栏结构之后,理论上可以简单地按“先左栏后右栏、栏内从上到下”的规则来排序。但实际版面往往没这么规整,栏内可能有子区域,区域之间可能有嵌套关系。这时候 XY-cut 的递归切割就能派上用场。

具体实现上,我通常这样做:先对页面做一次全局的 XY-cut,但在切割时加一个约束——垂直切割的优先级高于水平切割,且垂直切割只在检测到多栏时才执行。切割过程中,如果某个区域的宽度小于页面宽度的 40%,就停止垂直切割,转为纯水平切割。这个约束的目的是防止在单栏区域里过度切割。

递归的终止条件是:当前区域内只剩一个文本块,或者区域内的文本块已经按 y 坐标排好序且没有重叠。终止后,按深度优先遍历切割树,叶子节点的顺序就是阅读顺序。这里有个细节:在水平切割时,如果两个文本块的 y 范围有重叠(比如左右并排的两个小方块),那它们应该被视为同一行,按 x 坐标排序。这个判断逻辑要写进切割算法里,否则会把并排的元素错误地分成上下关系。

3.3 跨栏元素的特殊处理策略

跨栏元素是多栏排版里最容易出问题的地方。常见的跨栏元素包括:文章标题、作者信息、摘要、跨栏图片、表格、脚注。这些东西如果被错误地分到某一栏里,阅读顺序就全乱了。

我的处理策略是分两步走。第一步,在栏检测阶段就把明显跨栏的块标记出来。判断标准是:文本块的宽度超过页面宽度的 60%,或者文本块的 x0 接近页面左边缘且 x1 接近页面右边缘。第二步,在阅读顺序重建时,跨栏元素按照它们的 y 坐标插入到对应位置。具体来说,如果一个跨栏元素的 y0 小于所有栏内元素的 y0,它就应该排在最前面;如果它的 y 范围落在两栏的某个 y 区间内,它就应该插入到那个区间的位置。

这里有个坑:摘要部分经常是跨栏的,但它的字体和正文不一样,有时候会被 PyMuPDF 识别成独立的块,有时候又会和正文混在一起。如果发现摘要被拆散了,可以尝试用字体大小和字体名称来做二次分组。PyMuPDF 的 span 信息里有size和font字段,把相同字体属性的相邻块合并,往往能还原出完整的摘要区域。

4. 水印识别与剔除的实战方法

4.1 水印在 PDF 里的几种存在形式

水印这东西,在 PDF 里的实现方式五花八门,识别难度也各不相同。常见的有这么几类:

第一类是文字水印,比如“机密”“草稿”“样本”这种,通常以较大的字号、浅灰色、带旋转角度绘制在页面中央或对角线上。这类水印在 PyMuPDF 的文本抽取里会作为普通文本块出现,但它的 bbox 往往横跨大半个页面,且文本内容重复率高。

第二类是图片水印,就是一张半透明的 PNG 或 SVG 被平铺或居中放置在页面上。这类水印在get_text("dict")里不会出现在文本块中,但会出现在图片块里。识别方法是看图片的尺寸和位置——如果一张图片的 bbox 覆盖了页面大部分区域,且透明度较低,大概率是水印。

第三类是矢量图形水印,用 PDF 的绘图指令直接画出来的,比如一条斜线加文字。这类水印既不在文本块里也不在图片块里,需要用page.get_drawings()来获取。处理起来最麻烦,但好在实际文档里不算常见。

第四类是“伪水印”,就是页眉页脚里的公司名、页码、日期这些。它们严格来说不算水印,但同样会干扰解析,处理逻辑和水印类似——识别出来然后剔除。

4.2 基于 bbox 特征的水印判定规则

识别水印的核心思路是找“异常”。正常正文的文本块,宽度不会太大也不会太小,高度和字号匹配,位置在栏内。水印则往往违反这些规律。我总结了几条判定规则,实测下来覆盖了大部分场景:

特征维度正常正文水印嫌疑
文本块宽度小于页面宽度的 60%超过页面宽度的 70%
文本块高度与字号匹配,通常 10-30pt异常大,超过 50pt
旋转角度0 度非 0 度(常见 45 度或 -45 度)
字体颜色黑色或深色浅灰、浅蓝等低对比度颜色
文本内容多样化重复出现相同短语
位置栏内页面中央或对角线

实际使用时不需要全部满足,通常满足两到三条就可以标记为水印嫌疑。比如一个文本块宽度超过页面 70% 且颜色是浅灰,基本可以确定是水印。旋转角度的获取稍微麻烦一点,PyMuPDF 的line信息里有dir字段,表示文字方向向量,通过它可以算出旋转角度。

4.3 水印剔除后的文本清洗与验证

识别出水印之后,剔除动作本身很简单——从文本块列表里过滤掉就行。但剔除之后要做验证,确保没有误伤正文。我的做法是:剔除前后分别统计页面上的字符总数,如果剔除量超过 30%,就要警惕是不是规则太激进了。另外,可以抽样检查被剔除的文本块内容,如果里面出现了正常的句子,说明判定规则需要调整。

还有一个细节:有些水印是跨页重复的。比如一份 50 页的报告,每一页都有“内部资料”的水印。这种情况下,可以在第一页识别出水印特征后,把特征(文本内容、字体、颜色)记录下来,后续页面直接按特征匹配剔除,效率更高。这个思路也可以用在页眉页脚的剔除上——第一页识别出页眉的文本模式和 bbox 范围,后续页面直接按位置过滤。

5. 完整实操流程与代码实现

5.1 环境准备与依赖安装

先把环境搭起来。PyMuPDF 的安装很简单:

pip install pymupdf

建议用 1.23 以上的版本,因为get_texttrace()和get_drawings()在旧版本里支持不完善。另外推荐装一个numpy,做坐标计算和聚类的时候会方便很多:

pip install numpy

如果你打算把解析结果直接喂给 RAG 链路,可能还需要langchain或llama-index之类的框架,但这些不是本篇的重点,先把 PyMuPDF 的版面分析跑通再说。

5.2 提取页面文本块与坐标信息

先写一个函数,把页面上所有文本块的 bbox、文本内容、字体信息都提取出来:

import fitz def extract_blocks(page): blocks = [] data = page.get_text("dict") for block in data["blocks"]: if block["type"] != 0: continue bbox = block["bbox"] text = "" font_sizes = [] fonts = [] for line in block["lines"]: for span in line["spans"]: text += span["text"] font_sizes.append(span["size"]) fonts.append(span["font"]) if not text.strip(): continue blocks.append({ "bbox": bbox, "text": text.strip(), "avg_size": sum(font_sizes) / len(font_sizes) if font_sizes else 0, "font": max(set(fonts), key=fonts.count) if fonts else "", "width": bbox[2] - bbox[0], "height": bbox[3] - bbox[1], }) return blocks

这段代码里,avg_size是块内所有 span 的平均字号,font是出现次数最多的字体名。这两个信息后面做水印判定和跨栏元素识别时会用到。注意bbox的格式是(x0, y0, x1, y1),PyMuPDF 的坐标系是左上角原点,y 轴向下。

5.3 实现 XY-cut 递归切割算法

接下来是核心的 XY-cut 实现。我把它拆成两个函数:一个负责找切割线,一个负责递归切割。

def find_vertical_cut(blocks, page_width, min_gap_ratio=0.03): """找垂直切割线,返回切割位置的 x 坐标,找不到返回 None""" if len(blocks) < 2: return None min_gap = page_width * min_gap_ratio # 按 x0 排序 sorted_blocks = sorted(blocks, key=lambda b: b["bbox"][0]) # 检查相邻块之间是否有足够大的空隙 for i in range(len(sorted_blocks) - 1): right_edge = sorted_blocks[i]["bbox"][2] next_left = sorted_blocks[i + 1]["bbox"][0] if next_left - right_edge > min_gap: # 检查这条切割线是否穿过其他块 cut_x = (right_edge + next_left) / 2 crosses = False for b in blocks: if b["bbox"][0] < cut_x < b["bbox"][2]: crosses = True break if not crosses: return cut_x return None def find_horizontal_cut(blocks, page_height, min_gap_ratio=0.02): """找水平切割线""" if len(blocks) < 2: return None min_gap = page_height * min_gap_ratio sorted_blocks = sorted(blocks, key=lambda b: b["bbox"][1]) for i in range(len(sorted_blocks) - 1): bottom_edge = sorted_blocks[i]["bbox"][3] next_top = sorted_blocks[i + 1]["bbox"][1] if next_top - bottom_edge > min_gap: cut_y = (bottom_edge + next_top) / 2 crosses = False for b in blocks: if b["bbox"][1] < cut_y < b["bbox"][3]: crosses = True break if not crosses: return cut_y return None def xy_cut(blocks, page_width, page_height, depth=0): """递归 XY-cut,返回排序后的块列表""" if len(blocks) <= 1: return blocks # 优先垂直切割 cut_x = find_vertical_cut(blocks, page_width) if cut_x is not None: left = [b for b in blocks if b["bbox"][2] <= cut_x] right = [b for b in blocks if b["bbox"][0] >= cut_x] if left and right: return xy_cut(left, page_width, page_height, depth+1) + \ xy_cut(right, page_width, page_height, depth+1) # 垂直切不动,尝试水平切割 cut_y = find_horizontal_cut(blocks, page_height) if cut_y is not None: top = [b for b in blocks if b["bbox"][3] <= cut_y] bottom = [b for b in blocks if b["bbox"][1] >= cut_y] if top and bottom: return xy_cut(top, page_width, page_height, depth+1) + \ xy_cut(bottom, page_width, page_height, depth+1) # 都切不动,按 y 坐标排序 return sorted(blocks, key=lambda b: (b["bbox"][1], b["bbox"][0]))

这里有几个关键点要说明。min_gap_ratio是切割缝隙的最小宽度占页面尺寸的比例,垂直切割用 3%,水平切割用 2%。这个值不是拍脑袋定的,是我在几十份不同排版的 PDF 上试出来的经验值。太小了会把段落之间的正常间距当成切割线,太大了又切不开紧凑的版面。如果你的文档排版特别紧凑,可以适当调小;如果排版很松散,可以调大。

另外,切割线的检查逻辑里有一个“是否穿过其他块”的判断。这是为了防止出现这种情况:两个块之间确实有空隙,但第三个块的 bbox 横跨了这个空隙。如果不检查,就会把这个块切坏。这个检查是 XY-cut 正确性的关键,不能省。

5.4 水印检测与过滤的代码实现

水印检测我写了一个基于多特征打分的函数:

def detect_watermark(block, page_width, page_height): """返回水印嫌疑分数,越高越可能是水印""" score = 0 bbox = block["bbox"] width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] # 特征1:宽度过大 if width > page_width * 0.7: score += 2 # 特征2:高度异常 if height > 50: score += 1 # 特征3:字号异常大 if block["avg_size"] > 30: score += 1 # 特征4:位置在页面中央区域 center_x = (bbox[0] + bbox[2]) / 2 center_y = (bbox[1] + bbox[3]) / 2 if abs(center_x - page_width/2) < page_width*0.2 and \ abs(center_y - page_height/2) < page_height*0.2: score += 1 # 特征5:文本内容短且重复性高 if len(block["text"]) < 20: score += 1 return score def filter_watermarks(blocks, page_width, page_height, threshold=3): """过滤水印块""" clean = [] removed = [] for b in blocks: if detect_watermark(b, page_width, page_height) >= threshold: removed.append(b) else: clean.append(b) return clean, removed

阈值设为 3 是保守做法,宁可漏杀不可错杀。因为水印漏掉了顶多是检索时多几条噪声,但正文被误删了就是信息丢失,后果更严重。如果你确定文档里水印特征很明显,可以把阈值降到 2。

对于旋转水印的检测,需要额外处理。PyMuPDF 的line信息里有dir字段,格式是(cos, sin)的方向向量。正常水平文字的 dir 是(1, 0),旋转 45 度的文字 dir 大约是(0.707, 0.707)。可以在提取块信息的时候把 dir 也存下来,然后在检测函数里加一条:如果 dir 明显偏离(1, 0),加 2 分。

5.5 把解析结果接入 RAG 链路的注意事项

解析出来的文本最终是要喂给 RAG 的,这里有几个实操细节值得注意。

第一,阅读顺序重建之后,块与块之间要加适当的分隔符。我通常用两个换行表示段落分隔,一个换行表示同一段落内的换行。不要把所有块直接拼接,那样段落边界就丢了,检索时按段落切分的效果会变差。

第二,表格和图片的处理要单独考虑。XY-cut 对表格的处理能力有限,如果页面里有复杂表格,建议先用专门的表格识别工具(比如camelot或pdfplumber的表格接口)把表格抽出来,剩下的文本再用 XY-cut 排序。图片块的位置信息可以保留,在文本里插入一个占位标记,方便后续做多模态检索。

第三,分块策略要和版面分析结果对齐。不要用固定字符数切分,而是以 XY-cut 输出的块为基本单位,相邻的小块可以合并,超过一定大小的块再按句子切分。这样切出来的块语义完整性更好,检索命中率明显更高。

第四,记得保留页码和 bbox 信息。在 RAG 的引用溯源场景里,用户往往需要知道答案来自哪一页的哪个位置。把页码和 bbox 作为元数据存进向量库,前端就能高亮显示原文位置,体验会好很多。

6. 常见问题排查与避坑经验

6.1 切割过碎或切不开的调参思路

XY-cut 最常见的两个极端就是切得太碎和切不开。切得太碎的表现是:一个完整的段落被拆成了好几个块,阅读顺序虽然对但语义断裂。切不开的表现是:两栏的文字被混在一起,阅读顺序完全错乱。

调参的核心就是那个min_gap_ratio。切得太碎说明阈值太小,把段落间距当成了栏间距,调大即可。切不开说明阈值太大,真正的栏间距没被识别出来,调小即可。但怎么判断是哪种情况?我的方法是打印出所有候选切割线的位置和对应的缝隙宽度,人工看一眼就清楚了。

还有一个隐藏问题:有些 PDF 的文本块 bbox 有重叠。比如行距很小的文档,相邻两行的 bbox 可能在 y 方向上有几个点的重叠。这会导致水平切割找不到干净的缝隙。解决办法是在计算缝隙时允许一定的容差,比如重叠小于 2pt 就算作无重叠。

6.2 水印误判与漏判的平衡技巧

水印误判就是把正文当水印删了,漏判就是水印没识别出来。这两个问题的处理策略是相反的,需要根据你的业务场景来权衡。

如果是做法律文书、医疗报告这类对信息完整性要求极高的场景,宁可漏判不可误判,阈值调高,规则收紧。如果是做新闻资讯、营销材料这类对噪声容忍度高的场景,可以激进一点,阈值调低,把可能的干扰项都清掉。

另外,水印的字体颜色信息在 PyMuPDF 里可以通过 span 的color字段拿到,是一个整数表示的 RGB 值。浅灰色的水印 color 值通常很大(接近 0xFFFFFF),而正文是黑色(接近 0x000000)。把这个特征加进检测规则里,准确率会提升不少。不过要注意,有些 PDF 的文字颜色信息可能不准确,不能完全依赖。

6.3 特殊版面的处理经验

有些版面用 XY-cut 处理起来特别棘手,我列几个典型的和对应的处理思路:

版面类型问题表现处理思路
文字环绕图片文本块 bbox 呈 L 形或不规则先把图片块剔除,剩余文本按正常 XY-cut 处理
斜排文字文本块 bbox 是斜的用 dir 字段识别旋转角度,单独处理
嵌套表格表格内还有表格表格整体作为一个块,内部用表格工具解析
多栏+跨栏标题标题被分到某一栏按宽度识别跨栏元素,单独插入
脚注与正文混排脚注被当成正文按字号和位置识别脚注区域,单独提取

文字环绕图片这个场景特别常见,处理的关键是先把图片块的位置拿到,然后在文本块里找那些 bbox 与图片块有重叠的,把它们标记为“环绕文本”,单独按 y 坐标排序后插入到图片位置附近。这样虽然不能完美还原环绕效果,但至少阅读顺序不会乱。

6.4 性能优化与批量处理建议

PyMuPDF 本身速度很快,但 XY-cut 的递归切割在文本块很多的时候会有性能问题。一份 100 页的 PDF,每页 50 个文本块,递归深度可能到 10 层以上,总计算量不小。

优化的思路有几个。第一,在递归之前先做一次预排序,把明显属于同一栏的块先分组,减少递归的层数。第二,切割线的查找可以用空间索引加速,比如把块的 x 范围建成区间树,查找缝隙时不用遍历所有块。第三,对于批量处理场景,可以用多进程并行,每个进程处理一部分页面,最后合并结果。PyMuPDF 的页面对象不是线程安全的,但进程间没问题。

实测下来,一份 200 页的双栏学术论文,单进程处理大概需要 15 到 20 秒,用 4 个进程并行能压到 5 秒左右。这个速度对于离线预处理来说完全够用了。

6.5 解析质量验证的实用方法

做完解析之后怎么知道效果好不好?不能光靠肉眼看几页。我通常用这几个指标来量化评估:

第一个是阅读顺序正确率。随机抽 20 个页面,人工标注正确的阅读顺序,然后和算法输出对比,算一个准确率。这个指标最直接,但标注成本高。

第二个是水印剔除的准确率和召回率。准确率是剔除的块里真正是水印的比例,召回率是真正的水印里被剔除的比例。这两个指标要一起看,单独看一个没意义。

第三个是下游检索的命中率。把解析结果接入 RAG,用一批标准问题测试,看检索到的内容是否相关。这个指标最贴近实际效果,但需要先把 RAG 链路搭起来。

我的经验是,前两个指标在调参阶段用,快速迭代。第三个指标在最终验收时用,确保整体效果达标。一般来说,阅读顺序正确率到 90% 以上,水印剔除准确率到 85% 以上,下游检索效果就不会太差。

7. 一些实操中的个人体会

这套 bbox + XY-cut + PyMuPDF 的方案,我在多个 RAG 项目里用了大半年,踩过的坑不算少,但整体稳定性是满意的。最大的感受是:不要追求一步到位的完美解析,而是要把解析做成一个可迭代、可验证的流程。先跑通基本流程,用少量样本验证,发现问题再针对性调参,比一上来就写一大堆规则要高效得多。

另外,PDF 解析这个领域没有银弹。XY-cut 能解决 80% 的常见版面问题,但剩下 20% 的长尾情况可能需要专门的规则甚至模型来处理。我的建议是先把 80% 做扎实,剩下的 20% 根据业务价值决定投入多少精力。如果某类文档特别多,那就值得为它写专门的解析逻辑;如果只是偶尔出现,人工兜底也不是不可以。

最后分享一个小技巧:在调试 XY-cut 的时候,把切割线的位置可视化出来,画在页面上,一眼就能看出哪里切错了。PyMuPDF 支持在页面上画线,用page.draw_line()就行。这个可视化工具帮我省了大量调试时间,强烈推荐你也试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 13:17:49

MFC程序用libcurl实现multipart/form-data文件上传完整指南

MFC项目里要往服务器传文件&#xff0c;第一反应就是发http post请求。可真正动手你会发现&#xff0c;实际要处理的东西比想象中多&#xff1a;请求体怎么拼&#xff0c;multipart表单格式对不对&#xff0c;服务器能不能识别&#xff0c;C回调函数为什么编译不过&#xff0c;…

作者头像 李华
网站建设 2026/10/14 13:17:48

C语言源码大全拆解:从数据结构到编译调试与二次开发

简介&#xff1a;C语言作为编程基石&#xff0c;指针、内存管理与数据结构是学习者绕不开的三大难关。理解其底层原理&#xff0c;例如链表的指针操作与排序算法的边界条件&#xff0c;能显著提升代码质量与调试效率。经典源码往往涵盖链表、栈、队列、查找排序、字符串处理及文…

作者头像 李华
网站建设 2026/10/14 13:17:47

SpringBoot+Vue构建短流量分析系统实战

1. 项目概述&#xff1a;短流量分析系统的技术架构与核心价值短流量数据分析与可视化系统是现代互联网业务中不可或缺的运维工具&#xff0c;它能够实时捕捉、存储和分析网络请求的微观行为特征。这个基于SpringBootVueMyBatisMySQL的技术栈实现的全栈系统&#xff0c;完美解决…

作者头像 李华
网站建设 2026/10/14 13:17:15

光刻工艺从原理到实战:核心参数与常见缺陷排查指南

简介&#xff1a;一份面向半导体工艺工程师、IC制造从业者及微电子专业学生的光刻技术中文讲解PPT&#xff0c;系统梳理版图设计、光刻胶涂覆、曝光、显影、蚀刻与剥离的完整工艺链路&#xff0c;并展开深紫外/极紫外光源、正负型光刻胶、套刻对准、低缺陷控制&#xff0c;以及…

作者头像 李华
网站建设 2026/10/14 13:16:42

Node.js与Vue.js构建家教平台架构设计与优化实践

1. 项目背景与核心价值大学生家教服务管理系统是连接在校大学生与家长需求的高效平台。这个基于Node.js和Vue.js构建的系统&#xff0c;解决了传统家教中介信息不透明、匹配效率低、管理混乱等痛点。我去年参与开发过类似项目&#xff0c;发现市场对这类平台的需求量每年增长超…

作者头像 李华