- 文档
- 图像处理
【免费下载链接】pdf2docx
Open source Python library for converting PDF to DOCX.
pdf2docx是 Python 实现的 PDF 转 Word 库,其技术文档(docs/techdoc.rst)概括了整套转换思路:用 PyMuPDF 提取页面的文本、图片、形状及其位置,基于元素间的相对位置关系按规则解析出布局(例如"横纵线条围绕着文本"解析为表格、"文本下方的一条横线"解析为下划线),再借助 python-docx 将解析结果重建为 docx 文档。本文以该技术文档的四大主题——提取页面数据、解析页面布局、解析表格、解析段落——为骨架,结合开源仓库中的源码实现,完整讲清每一步"从哪里取数、按什么规则解析、最终如何落到 docx 对象"。
一、总体管线:提取、解析、重建三步走
技术文档中描述的三段式流程,在 Converter 类中被组织为一条可逐步调用的流水线。类文档字符串明确写出了各步职责:
- 用 PyMuPDF 逐页读取原始布局数据(文本、图片、绘图及其 bbox、字体、尺寸等属性);
- 在文档级别分析布局,如页面页眉、页脚与页边距;
- 将页面布局解析为 docx 结构,如段落(缩进、行距、对齐方式)与表格(边框、底纹、合并单元格);
- 用 python-docx 生成 docx 文件。
对应的调用链体现在 parse() 方法 中:
def parse(self, start:int=0, end:int=None, pages:list=None, **kwargs): return self.load_pages(start, end, pages) \ .parse_document(**kwargs) \ .parse_pages(**kwargs)即load_pages(第 1 步,打开文档并标记待解析页)→parse_document(第 2 步,文档级分析)→parse_pages(第 3 步,逐页解析段落/图片/表格)→make_docx(第 4 步,创建 docx)。Converter还要求 PyMuPDF 版本满足1.19.0 <= v <= 1.23.8 或 v >= 1.23.16(见 版本检查),这是运行本库的前提条件。
整个解析过程构建的对象树在 Page 模块文档中有明确定义:
Page └─ Section(版式节,可含两栏) └─ Column(栏) ├─ Blocks │ ├─ TextBlock → Line → TextSpan/ImageSpan → Char │ └─ TableBlock → Row → Cell →(递归的 Blocks 与 Shapes) └─ Shapes ├─ Stroke(描边:表格边框/下划线/删除线) ├─ Fill(填充:单元格底纹/文本高亮) └─ Hyperlink(超链接热区)这一层级结构就是后文四个主题所围绕的核心:提取阶段产出blocks与shapes原始数据,解析阶段将其组织为上述对象树,重建阶段再把对象树逐个映射为 python-docx 的段落、表格和节。
二、提取文本、图片和形状
技术文档的第一主题是"提取 PDF 页面数据"。仓库中的实现集中在 RawPageFitz 类——它对fitz.Page做封装,extract_raw_dict()一次性产出该页的原始数据字典:
def extract_raw_dict(self, **settings): raw_dict = {} # 实际页面尺寸 *_, w, h = self.page_engine.rect raw_dict.update({ 'width': w, 'height': h }) text_blocks = self._preprocess_text(**settings) # 文本块 raw_dict['blocks'] = text_blocks image_blocks = self._preprocess_images(**settings) # 图片块 raw_dict['blocks'].extend(image_blocks) shapes, images = self._preprocess_shapes(**settings) # 形状 + 矢量图 raw_dict['shapes'] = shapes raw_dict['blocks'].extend(images) hyperlinks = self._preprocess_hyperlinks() # 超链接 raw_dict['shapes'].extend(hyperlinks) ...四类元素各有一处专门的提取逻辑:
文本提取与隐藏文本过滤
_preprocess_text 通过page.get_text('rawdict')拿到所有文本块,再用page.get_texttrace()得到的字符级轨迹来过滤"隐藏文本"(OCR 生成的不可见文字层)。配置项ocr的三态语义在这里直接体现:ocr=0时过滤掉隐藏文本(span['type']==3的字符),ocr=2时反过来只保留隐藏文本(适用于已 OCR 的扫描件),ocr=1(期望 OCR)尚未实现会直接报错。判定规则是 bbox 相交面积超过一半且字体名一致时,整个文本块被标记为隐藏。
图片提取
_preprocess_images 委托给 ImagesExtractor。由于get_text('rawdict')得到的图片块不含 alpha 通道数据,实现上先用Page.get_images()按 xref 取出每张图(自动去重),再按其在页面上的位置恢复,需要时通过clip_page_to_pixmap()按clip_image_res_ratio(默认 4.0,即相对 72dpi 的分辨率倍数)裁剪渲染成位图。
形状提取:从 Path 到 Stroke/Fill
形状提取分两步。第一步 _init_paths 用page.get_cdrawings()取出全部绘图路径(该 API 自 PyMuPDF 1.18.0 提供,同时覆盖 PDF 原始路径与 Line/Square/Highlight 等注释),封装为Paths集合并丢弃超出页面范围的路径。第二步 Paths.to_shapes_and_images() 做语义分流:
- 若全部路径都是轴对齐(iso-oriented)的线段,直接转为
Stroke(水平/垂直线)或Fill(矩形填充); - 若存在非轴对齐路径,则用 OpenCV 轮廓检测找出矢量图(SVG 类)区域——由
min_svg_gap_dx(默认 15pt)、min_svg_gap_dy(默认 2pt)、min_svg_w/min_svg_h(默认 2pt)控制合并与忽略规则——这些区域被裁剪为位图插入文档; - 其余轴对齐线段保留为形状,进入后续的表格/文本样式解析。
这个分流正是技术文档所说"基于元素间相对位置关系解析内容"的数据基础。
超链接
_preprocess_hyperlinks 从page.get_links()中只保留互联网链接(kind==2),把"URI + 热区矩形"抽象为Hyperlink形状。PyMuPDF 并不显式给出链接作用的文字,因此复用文本样式形状的"按位置关联文本"流程来处理。
形状的语义类型判定
技术文档中"文本下方的一条横线"解析为"文本下划线"这一规则,实现在 Shape 模块中。每个形状先有一个"潜在类型集合":Stroke的默认类型是BORDER | UNDERLINE | STRIKE的组合,Fill是SHADING | HIGHLIGHT的组合。随后parse_semantic_type(blocks)逐条检查形状与文本行的位置关系来收敛类型。以Stroke为例,判定逻辑(Stroke._semantic_type)是:
- 形状与文本行主方向不一致(横线对竖排文字)→ 保持为边框候选;
- 形状在主方向上不超过文本行两端(允许 1pt 容差)→ 可能是下划线或删除线;
- 否则(线条明显比文字长,如"横纵线条围绕着文本"的边框线段)→ 判定为表格边框
BORDER。
Fill同理:主方向宽度不超过约文本行宽度的填充判定为高亮HIGHLIGHT,更大范围的判定为单元格底纹SHADING。
提取完成后,RawPage.clean_up() 在页面级做清理:把文本块拆解到行(line)级别(因为 PyMuPDF 的 block 划分未必合理)、删除出界行与重叠行(阈值line_overlap_threshold)、检测浮动图片(阈值float_image_ignorable_gap),并用max_border_width、shape_min_dimension过滤无效形状。
三、解析页面布局
技术文档第二主题"解析页面布局"对应 Pages.parse() 与 RawPage 中的文档级/页面级分析。这一步的目标是把"原始 blocks + shapes"组织为Page → Section → Column的流式结构,共四个子步骤:
1. 字体度量
Pages.parse 先用 fonttools 从fitz_doc提取字体属性(重点是行高比例),随后process_font()把标准行高写入每个TextSpan(span.line_height = font.line_height * span.size)。这个数据将决定后文段落行距采用"精确行距"还是"相对行距"。
2. 逐页提取与清理
对每个待解析页,RawPageFactory.create(backend='PyMuPDF')生成RawPageFitz,执行restore()(提取原始数据)→clean_up()(清理),并把页面宽高、浮动图片收集到Page对象上。若整份文档提取不到任何文字,会提示"可能是扫描件,暂不支持"(Pages.py#L58-L60)。
3. 文档级结构
Pages._parse_document()预留了页眉/页脚提升为文档级 header/footer 的位置(当前实现返回空,属于 TODO),保证页边距和分节计算基于干净的内容块进行。
4. 页边距与分节
页边距由 calculate_margin() 计算:取全部 blocks 与 shapes(排除超链接)的外接矩形与页面矩形的差值,左右边距上限为ITP(标准页边距),上下边距再乘以page_margin_factor_top/page_margin_factor_bottom(默认 0.5,即"把计算出的上下边距缩小一半以保留一定余量")。
分节由 parse_section() 完成,用于识别多栏页面。其规则从源码结构看是:
- 把页面内的行(line)与形状按行分组,逐行统计列数(
group_by_columns()); - 仅考虑单栏和两栏:一行分出的列数超过 2 时按单栏处理;
- 两栏要求两列宽度比例在 1/2 到 2 倍之间,且每列内容宽度占列宽 33% 以上,否则降级为单栏;
- 相邻两栏的交界位置发生漂移(如前一行两栏、当前行单栏但内容落在某一列内)时,通过"虚拟空栏"判断决定归入 2 栏还是关闭当前节;
- 单栏节最小高度由
min_section_height(默认 20pt)控制,低于它的两栏段被合并回单栏节。
识别出的每个Section记录bbox、num_cols、栏间距space与上一节之间的before_space(垂直定位),结构定义见 Section 模块。栏数最多支持 2 栏(源码注释中明确 "support at most two columns")。
四、解析表格
技术文档中"横纵线条围绕着文本"解析为"表格",对应 TablesConstructor 中的两类表格检测,调用入口在 Layout._parse_table():先检测显式表格(lattice),再检测无边框表格(stream),并把表内内容块移入对应单元格。
显式表格(lattice table)
lattice_tables() 处理"有横纵线条"的表格,流程与参数含义:
group_by_connectivity(dx=connected_border_tolerance, dy=...):把端点间距小于connected_border_tolerance(默认 0.5pt)的描边分组,每组即一个候选表格轮廓;remove_overlap():若一组线条完全包含在另一组内(嵌套表格),外层在此层处理、内层留待单元格子布局递归处理,只保留每组中面积最大的一个;TableStructure(strokes, min_border_clearance, max_border_width).parse(group_fills):基于线条交点求解行列结构,min_border_clearance(默认 2pt)是两条边框间的最小可分辨间距,max_border_width(默认 6pt)用于过滤过粗的"伪边框";group_fills为该表区域内的填充形状,作为单元格底纹候选;to_table_block()生成TableBlock并标记为 lattice 类型,随后assign_to_tables()把落在表内的行和形状分派到具体单元格。
无边框表格(stream table)
stream_tables() 针对没有线条、仅靠文本对齐形成的表格(如财务数据表)。核心思想是把"列间空白"推断为虚拟边框(Border对象是"在某范围内自适应的虚拟边框",确定后转为真实描边参与结构检测):
collect_stream_lines()(Blocks.py#L179-L255)按行扫描文本:某行内的多个块不在同一流式布局上(水平间距超过line_separate_threshold,默认 5pt),或块被非白色填充(底纹)包含时,视为潜在表格行;- 对每组候选行,
_outer_borders()依据表格上下相邻块的位置确定表格的外边界(HT/HB/VR/VL 四条虚拟边框),_inner_borders()递归地在列与列之间、行与行之间生成 VI(垂直内边框)与 HI(水平内边框);其中垂直边框总是构成表格结构,而水平边框在两栏以内的排版中"仅供参考"(reference=True),递归深入时才升级为结构边框——源码注释解释了这一设计的矛盾性:重建两栏版式只需要 1×N 的便捷表格,而解析真正的无边框表格则需要 M×N 的完整结构; _is_simple_structure()排除单列、以及两列且逐行对齐的"简单结构"(那其实是普通段落排版);list_not_table(默认 True)避免把项目符号列表误判为表格;单元格内检测到的 1×1 且无底纹子表格被丢弃,防止无限递归。
表格解析完成后,单元格Cell本身又是一个Layout,会递归执行"解析表格 → 解析段落",实现嵌套表格。表格内容的独立提取则通过 Converter.extract_tables() 提供:先解析页面,再汇总各页TableBlock.text,返回list[list[list[str]]];extract_stream_table=False(默认)时只提取有显式边框的 lattice 表格。命令行下可用pdf2docx table子命令输出,示例见 docs/quickstart.table.rst。
五、解析段落
技术文档第四主题"解析段落"实现在 Layout.parse() 中,顺序固定为:先_parse_table()(表格优先),再_parse_paragraph(),最后递归解析表格单元格子布局。
1. 行合并为文本块
Blocks.parse_block() 分两阶段。第一阶段 _join_lines_vertically() 按阅读顺序遍历行,把垂直方向属性相近的相邻行合并进同一个TextBlock:
- 先统计相邻行间距的众数
common_vertical_spacing()作为"常规行距"参照; - 若当前行与上一行在同一行(in_same_row),或垂直距离不超过"众数间距 + 1pt"且该间距不超过
max_line_spacing_ratio(默认 1.5)倍的行高,则并入当前块; - 图片行强制断开;遇到
TableBlock也强制断开。
第二阶段_split_text_block_vertically()再按new_paragraph_free_space_ratio(默认 0.85,空白高度超过行高该比例即视为段间空行)和line_break_free_space_ratio(默认 0.1)把大块拆成独立段落。
2. 文本样式与连字符
parse_text_format()(Blocks.py#L274-L286)把已判定为下划线/删除线/高亮的形状(shapes.text_style_shapes)施加到对应文本块上,并按delete_end_line_hyphen选项调整行尾单词(默认不删除行尾连字符)。这解释了为什么输出 docx 中部分连字符会被去掉:行尾短连字符常被误认为单词的一部分,可选开启行尾连字符清理。
3. 间距与对齐
parse_spacing() 分三小步,把"位置关系"翻译成 docx 的段落属性:
- 水平间距/对齐:
_parse_block_horizontal_spacing()以父布局working_bbox为参考,比较相邻行的左边界(lines_left_aligned_threshold,默认 1pt)、右边界(lines_right_aligned_threshold,默认 1pt)与中心(lines_center_aligned_threshold,默认 2pt)偏差,判定左对齐/右对齐/居中,并把块到左右边界的水平距离折算为段落左/右缩进; - 段前段后:
_parse_block_vertical_spacing()计算每个块相对前一块(首块相对页边距)的垂直距离:当前块是段落就设为before_space,前一块是段落则设为after_space,表格前无法直接设置段前距时,用"空段落 + 行距"的技巧实现(block.before_space); - 行距:
_parse_line_spacing()依据字体度量选择模式——若块内存在无法解析标准行高的 span(is_valid_line_height为 False),采用"精确行距"(line_space_type = 0,parse_exact_line_spacing()),否则按相对行高比例设置(parse_relative_line_spacing()),避免 Word 重排时行距漂移。
六、重建 docx
解析完成的结构树如何映射为 Word 文档?对应各对象的make_docx()方法:
- 页面级:Page.make_docx() 为非首页插入
WD_SECTION.NEW_PAGE分页节,并把page_width/page_height与四边页边距(第 3 步解析出的 margin)写入节属性,然后调用sections.make_docx(); - 节/栏级:Sections.make_docx() 用
WD_SECTION.CONTINUOUS连续节区分页面内的不同版式区域(如"上文下图"),用上一段落的space_after精确还原节的垂直起始位置before_space;两栏节通过 set_columns() 设置栏宽与栏间距,并用WD_SECTION.NEW_COLUMN分栏;浮动图片统一挂到当前页首个段落; - 块级:Blocks.make_docx() 按顺序遍历,文本块
doc.add_paragraph()后由TextBlock.make_docx()写入带样式(字体、下划线、高亮、对齐、缩进、行距)的 run;表格doc.add_table(rows, cols)后关闭 autofit,并处理两个 Word 渲染细节:相邻表格自动合并需插入最小空段落分隔、表格位于页尾时追加一个小行距空段防止意外分页; - 文档级:Converter.make_docx() 创建
Document()后逐页调用page.make_docx(),逐页捕获异常(ignore_page_error默认为 True 时跳过失败页继续转换),最后doc.save()。
值得注意的是store()/restore()序列化机制:Page、Section、Column、Blocks、Shapes全部可存为 dict 并经store()/serialize()写入 JSON(converter.py#L246-L278)。这既支撑了多进程转换(多进程实现 中每个子进程把解析结果写入pages-{i}.json,主进程deserialize()后统一生成 docx),也让调试变得可行。
七、配置参数与实战用法
Converter的所有解析行为由 default_settings 中的参数控制,可经convert(**kwargs)覆盖。完整参数表如下(默认值均取自源码注释):
| 参数 | 默认值 | 作用 |
|---|---|---|
debug | False | 为 True 时把解析布局绘制到调试 PDF |
ocr | 0 | 0-不做 OCR;1-需要 OCR(未实现);2-已 OCR 的 PDF,只提取隐藏文本 |
ignore_page_error | True | 单页失败时不中断整体转换 |
multi_processing | False | 是否多进程解析页面(仅限start/end连续页码) |
cpu_count | 0 | 多进程使用的 CPU 数,0 表示全部可用核心 |
min_section_height | 20.0 | 有效节的最小高度(pt),用于多栏分节判断 |
connected_border_tolerance | 0.5 | 两边框端点间距小于该值视为相连(pt) |
max_border_width | 6.0 | 最大边框宽度(pt),超过视为填充 |
min_border_clearance | 2.0 | 两条边框间最小可分辨间距(pt) |
float_image_ignorable_gap | 5.0 | 图片与文本行的可忽略间隙,超过则视为浮动图片(pt) |
page_margin_factor_top/bottom | 0.5 | 上下页边距缩放系数,[0,1] |
shape_min_dimension | 2.0 | 宽、高均小于该值的形状被忽略(pt) |
max_line_spacing_ratio | 1.5 | 最大行距比:行距/行高,用于行合并为块 |
line_overlap_threshold | 0.9 | 与它行重叠度超过该值的行被删除,[0,1] |
line_break_width_ratio | 0.5 | 行宽相对版式宽度低于该比例时视为换行 |
line_break_free_space_ratio | 0.1 | 行内空白占比超过该比例时断行 |
line_separate_threshold | 5.0 | 两元素水平间距超过该值视为分属不同列(pt) |
new_paragraph_free_space_ratio | 0.85 | 行间距超过行高该比例时视为新段落 |
lines_left_aligned_threshold | 1.0 | 两行左端偏差低于该值判定左对齐(pt) |
lines_right_aligned_threshold | 1.0 | 两行右端偏差低于该值判定右对齐(pt) |
lines_center_aligned_threshold | 2.0 | 两行中心偏差低于该值判定居中(pt) |
clip_image_res_ratio | 4.0 | 裁剪页面生成位图时的分辨率倍率(相对 72dpi) |
min_svg_gap_dx | 15.0 | 水平间隙小于该值时合并相邻矢量图形(pt) |
min_svg_gap_dy | 2.0 | 垂直间隙小于该值时合并相邻矢量图形(pt) |
min_svg_w/min_svg_h | 2.0 | 宽/高低于该值的矢量图形被忽略(pt) |
extract_stream_table | False | extract_tables是否包含无边框表格 |
parse_lattice_table | True | 是否解析显式边框表格,关闭可能破坏版式 |
parse_stream_table | True | 是否解析无边框表格,关闭可能破坏版式 |
delete_end_line_hyphen | False | 删除行尾连字符 |
raw_exceptions | False | 为 True 时不再吞掉异常,直接抛出 |
list_not_table | True | 避免把项目符号列表误判为表格 |
API 调用示例
完整转换(docs/quickstart.convert.rst 中的示例,页码默认从 0 起):
from pdf2docx import Converter pdf_file = '/path/to/sample.pdf' docx_file = 'path/to/sample.docx' cv = Converter(pdf_file) # 加密文档:Converter(pdf_file, password) cv.convert(docx_file) # 默认转换全部页面 cv.close()指定页面与多进程:
cv.convert(docx_file, start=1) # 从第 2 页到末页 cv.convert(docx_file, end=3) # 从第 1 页到第 3 页(end 不含) cv.convert(docx_file, pages=[0, 2, 4]) # 页码列表优先级高于 start/end cv.convert(docx_file, multi_processing=True) # 默认 CPU 数的多进程 cv.convert(docx_file, multi_processing=True, cpu_count=4)提取表格内容(返回list[list[list[str]]]):
cv = Converter(pdf_file) tables = cv.extract_tables(start=0, end=1) cv.close() for table in tables: print(table)命令行入口由 main.py 提供(setup.py中注册为pdf2docx脚本),支持convert、debug、table三个子命令,--zero_based_index=False可切换为从 1 开始的页码(docs/quickstart.cli.rst):
pdf2docx convert test.pdf test.docx pdf2docx convert test.pdf test.docx --start=1 --end=3 pdf2docx convert test.pdf test.docx --pages=0,2,4 pdf2docx convert test.pdf test.docx --multi_processing=True --cpu_count=4调试:debug 模式
排查"为什么这一页转换结果不对"是布局解析类工具的日常需求。Converter.debug_page() 提供单页调试:以debug=True解析指定页,一方面用 PyMuPDF 把各层布局(源文本块、清理后的形状、最终布局等,见@debug_plot装饰器标记的方法)绘制到debug_前缀的 PDF,另一方面把完整解析结果序列化为layout.json供离线检查;命令行对应pdf2docx debug test.pdf --page=0。
八、转换质量的验证方式
仓库的 test/test.py 说明了官方的质量验证思路,也给出了理解"解析-重建"闭环的参考:将foo.pdf转换为foo.pdf.docx后,再借助 Word(Windows,经 docx2pdf)或 LibreOffice(其他平台,命令行)把 docx 反向渲染为foo.pdf.docx.pdf,最后用 OpenCV 的 MS-SSIM(结构相似指数,get_mssim())计算两版 PDF 逐页像素级的相似度。这一"PDF → DOCX → PDF → SSIM"的回归链路,本质上是把第 5、6 节所述的 docx 重建能力当作被测对象:页边距、分栏、表格边框底纹、段落行距等解析结果的准确性,最终都体现在再渲染页面与原 PDF 的结构相似度上。
小结
回扣技术文档的主线:pdf2docx的全部工作可以概括为一条数据流——PyMuPDF 原始提取(文本 rawdict、图片 xref、get_cdrawings 路径、超链接热区,见 RawPageFitz)→规则化布局解析(按 bbox 相对位置识别语义:线条围绕文本即表格、线下文字下缘即下划线;组织出 Page/Section/Column/TableBlock/TextBlock 对象树)→python-docx 重建(节、栏、段落、表格属性逐项落盘)。四大主题在源码中的入口分别对应 Pages.parse()(提取与文档级布局)、RawPage.parse_section()(页面分节)、TablesConstructor(两类表格)与 Blocks.parse_block()/parse_spacing()(段落与间距)。理解这条链路后,上表的 30 个default_settings参数各自作用于哪个环节便一目了然,也为针对特定文档类型微调解析行为提供了明确抓手。需要再次说明的适用边界:该库基于"版式规则"而非版面模型,当前版本不支持 OCR(ocr=1未实现)、多栏最多两栏,扫描件或复杂版式 PDF 的转换质量需以实际渲染结果为准。
- 文档
- 图像处理
【免费下载链接】pdf2docx
Open source Python library for converting PDF to DOCX.
相关推荐
一键生成可启动的 OpenCore EFI:OpCore Simplify 新手上手指南
一键生成可启动的 OpenCore EFI:OpCore Simplify 新手上手指南 OpCore Simplify 是一个免费开源 采用 BSD 3 Cl
开发工具CLI零代码搞定PDF/DOCX解析:Perplexica本地文件提取技术详解
零代码搞定PDF/DOCX解析:Perplexica本地文件提取技术详解 Perplexica是一款AI驱动的搜索引擎,作为Perplexity AI的开源替代
人工智能大模型AI 应用本地部署后端前端终极指南:Rig如何实现PDF文本提取:OCR与布局分析技术全解析
终极指南:Rig如何实现PDF文本提取:OCR与布局分析技术全解析 Rig是一个构建可移植、模块化和轻量级全栈代理的框架,其强大的PDF文本提取功能结合了OCR
AI AgentAgent 框架RAG后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考