如何从无固定模式的PDF文件中提取目标文本片段
纯文本提取丢失PDF原生排版、位置信息,是导致行顺序错乱、锚点失效的核心原因,按以下思路实现可覆盖所有提到的问题场景:
放弃直接导出纯文本的方案,提取阶段保留完整布局元数据
不要直接调用pdfminer.six的extract_text()方法输出无结构txt,改用extract_pages()接口遍历页面元素,配合调整后的LAParams参数做布局分析,为每个文本块记录x/y坐标、字号、字体、字重属性。提取完成后对所有文本块按视觉顺序重排:以y坐标降序排序(PDF坐标原点在页面左下角,y值越大位置越靠上),同一y轴区间内的块按x坐标升序排序,从根源解决第二个文件里文本顺序错乱的问题——纯文本提取默认按PDF内部对象存储顺序输出,本身就不保证和人眼看到的阅读顺序一致。靠视觉特征锚定章节标题,不依赖固定行号、硬正则
人眼能识别章节标题,本质是标题和正文存在明确视觉差异:通常比正文字号大2pt以上、字体加粗、和前后段落的间距大于普通正文行间距。遍历重排后的文本块时,先筛选符合标题视觉特征的块,再匹配块内文本是否为目标章节标题即可。如果遇到标题和其他内容混排在同一提取行的情况,可以进一步拆分到单个字符维度,靠字体差异、字符间的异常大间距把标题从混排内容里拆分出来,不会漏匹配。锚定标题后按排版边界提取内容,不依赖固定行偏移
定位到标题块的坐标后,从标题下方第一个符合正文特征的文本块开始收集内容,直到遇到下一个同等级/更高等级的标题(同样靠字号、加粗、间距特征判断)就停止提取。结合目标内容左右边距固定的特征,加一层过滤规则:只保留x坐标范围落在目标内容边距区间内的文本块,自动排除页眉、页脚、侧边注释、页码等干扰内容,不需要关心标题前有多少未知段落。增加模糊匹配兜底逻辑适配异常文件
如果遇到少数标题格式和正文完全一致的特殊PDF,可以加一层轻量的文本相似度校验(比如编辑距离匹配),结合位置特征排除误匹配,不需要写复杂的通用正则。
最小可运行参考代码
from pdfminer.high_level import extract_pages from pdfminer.layout import LTTextLineHorizontal, LAParams # 布局分析参数,可根据PDF排版微调 laparams = LAParams( line_overlap=0.5, char_margin=2.0, line_margin=0.5, word_margin=0.1, detect_vertical=False ) # 配置项,根据实际PDF参数校准一次即可,同模板文件通用 TARGET_SECTION_TITLE = "Important section title" CONTENT_LEFT_MARGIN = 72 # 目标内容左边距 CONTENT_RIGHT_MARGIN = 540 # 目标内容右边距 BODY_FONT_SIZE = 12 # 正文字号 def extract_target_section(pdf_path): text_blocks = [] # 逐页提取带元数据的文本块 for page in extract_pages(pdf_path, laparams=laparams): for elem in page: if isinstance(elem, LTTextLineHorizontal): # 提取当前行的核心属性 font_sizes = [] is_bold = False for char in elem: if hasattr(char, "size"): font_sizes.append(char.size) if hasattr(char, "fontname") and "Bold" in char.fontname: is_bold = True text_blocks.append({ "y_top": elem.y1, "x0": elem.x0, "x1": elem.x1, "content": elem.get_text().strip(), "font_size": max(font_sizes) if font_sizes else BODY_FONT_SIZE, "is_bold": is_bold }) # 按人眼阅读顺序重排所有文本块 text_blocks.sort(key=lambda x: (-x["y_top"], x["x0"])) # 定位目标标题位置 title_pos = None for idx, block in enumerate(text_blocks): if TARGET_SECTION_TITLE in block["content"] and (block["is_bold"] or block["font_size"] > BODY_FONT_SIZE): title_pos = idx break if title_pos is None: return "" # 提取标题到下一个标题之间的目标内容 result = [] for block in text_blocks[title_pos+1:]: # 遇到下一个标题则终止提取 if block["is_bold"] or block["font_size"] > BODY_FONT_SIZE: break # 过滤边距不符合的非目标内容 if block["x0"] >= CONTENT_LEFT_MARGIN and block["x1"] <= CONTENT_RIGHT_MARGIN: if block["content"]: result.append(block["content"]) return "\n".join(result)
上述代码里的边距、字号参数只需要校准一次,同模板生成的所有PDF都可以通用,不需要针对单个文件调整。
内容的提问来源于stack exchange,提问作者Hung Vu

