ARTICLE · 1333673
用 Python 写一个 PDF 转 DOC 小工具(支持扫描版 PDF)
用 Python 写一个 PDF 转 DOC 小工具(支持扫描版 PDF)

1. 需求分析:PDF 转 DOC 到底难在哪
PDF 和 DOC 是两种完全不同的文档模型:PDF 描述的是「在某个坐标画某个字形」,DOC 描述的是「段落、样式、表格」这样的结构化内容。所以转换的核心不是格式翻译,而是版面还原。
按 PDF 的来源,可以分成两类,处理方式完全不同:
很多工具只能处理第一类,遇到扫描件就输出一堆空白。本文的小工具会同时覆盖两类,并自动判断走哪条路径。
2. 技术选型
PyMuPDF | ||
PyMuPDF | ||
PaddleOCR | ||
Pillow | ||
python-docx | .docx,兼容 Word/WPS |
安装依赖:
pip install pymupdf paddleocr paddlepaddle pillow python-docx说明:
PaddleOCR首次运行会自动下载模型,需要联网。如果只想处理文本型 PDF,可以不装 OCR 相关依赖。
3. 整体流程设计

关键点在于按坐标重建段落:OCR 和文本抽取拿到的都是「一行行带坐标的文字」,需要根据纵坐标把同一行的文字合并,再根据行间距判断是否换段。
4. 核心代码实现
4.1 判断是否为扫描版 PDF
import fitz # PyMuPDFdef is_scanned_pdf(pdf_path: str, sample_pages: int = 5, threshold: int = 50) -> bool:"""抽样判断 PDF 是否为扫描版:可提取字符数过少即认为是扫描件"""doc = fitz.open(pdf_path)pages = min(sample_pages, doc.page_count)total_chars = 0for i in range(pages):text = doc[i].get_text("text").strip()total_chars += len(text)doc.close()# 平均每页字符数低于阈值,判定为扫描版return total_chars / max(pages, 1) < threshold
4.2 文本型 PDF:抽取文本块
def extract_text_blocks(pdf_path: str):"""返回 [(page_no, [(x0, y0, x1, y1, text), ...]), ...]"""doc = fitz.open(pdf_path)result = []for page_no, page in enumerate(doc):blocks = []for b in page.get_text("blocks"):x0, y0, x1, y1, text = b[0], b[1], b[2], b[3], b[4]text = text.strip()if text:blocks.append((x0, y0, x1, y1, text))result.append((page_no, blocks))doc.close()return result
4.3 扫描型 PDF:渲染 + OCR
import ioimport numpy as npfrom PIL import Imagefrom paddleocr import PaddleOCR# 首次初始化会下载模型;use_angle_cls 用于纠正倾斜文字ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False)def ocr_page(page, dpi: int = 200):"""把一页 PDF 渲染成图片后做 OCR,返回 [(x0, y0, x1, y1, text), ...]"""pix = page.get_pixmap(dpi=dpi)img = Image.open(io.BytesIO(pix.tobytes("png"))).convert("RGB")arr = np.array(img)result = ocr.ocr(arr, cls=True)blocks = []if not result or not result[0]:return blocksfor line in result[0]:box, (text, score) = lineif score < 0.5: # 置信度过低直接丢弃continuexs = [p[0] for p in box]ys = [p[1] for p in box]blocks.append((min(xs), min(ys), max(xs), max(ys), text))return blocks
4.4 按坐标重建段落
这是整个工具里最影响效果的一步。思路是:先按纵坐标把文字块聚成「行」,再按行间距把「行」聚成「段」。
def group_into_paragraphs(blocks, line_tol: float = 8.0, para_gap: float = 1.8):"""把带坐标的文字块聚合成段落文本列表"""if not blocks:return []# 1. 按 y 坐标排序,聚成行blocks = sorted(blocks, key=lambda b: (b[1], b[0]))lines = []for b in blocks:placed = Falsefor line in lines:# 与已有行的纵向中心接近,视为同一行if abs((b[1] + b[3]) / 2 - (line["y"] + line["h"] / 2)) < line_tol:line["items"].append(b)placed = Truebreakif not placed:lines.append({"y": b[1], "h": b[3] - b[1], "items": [b]})# 2. 行内按 x 排序拼接for line in lines:line["items"].sort(key=lambda b: b[0])line["text"] = "".join(i[4] for i in line["items"])lines.sort(key=lambda l: l["y"])# 3. 按行间距聚成段paragraphs, current = [], [lines[0]["text"]]for prev, cur in zip(lines, lines[1:]):gap = cur["y"] - (prev["y"] + prev["h"])if gap > prev["h"] * para_gap:paragraphs.append("".join(current))current = [cur["text"]]else:current.append(cur["text"])paragraphs.append("".join(current))return [p for p in paragraphs if p.strip()]
4.5 写入 DOCX
from docx import Documentfrom docx.shared import Ptdef write_docx(paragraphs, out_path: str, font_name: str = "宋体", font_size: int = 11):doc = Document()style = doc.styles["Normal"]style.font.name = font_namestyle.font.size = Pt(font_size)# 中文字体需要额外设置 eastAsiastyle.element.rPr.rFonts.set("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia", font_name)for p in paragraphs:doc.add_paragraph(p)doc.save(out_path)
4.6 主流程串联
def pdf_to_docx(pdf_path: str, out_path: str, force_ocr: bool = False):scanned = force_ocr or is_scanned_pdf(pdf_path)print(f"[信息] 判定为{'扫描版'if scanned else'文本型'} PDF")all_paragraphs = []if scanned:doc = fitz.open(pdf_path)for page_no, page in enumerate(doc):print(f"[OCR] 正在识别第 {page_no + 1}/{doc.page_count} 页")blocks = ocr_page(page)all_paragraphs.extend(group_into_paragraphs(blocks))doc.close()else:for _, blocks in extract_text_blocks(pdf_path):all_paragraphs.extend(group_into_paragraphs(blocks))write_docx(all_paragraphs, out_path)print(f"[完成] 已输出:{out_path}")if __name__ == "__main__":import sysif len(sys.argv) < 3:print("用法: python pdf2docx.py input.pdf output.docx [--ocr]")sys.exit(1)pdf_to_docx(sys.argv[1], sys.argv[2], force_ocr="--ocr" in sys.argv)
5. 效果优化建议
- 倾斜校正
:扫描件常有轻微倾斜,OCR 前可用 PaddleOCR的use_angle_cls或 OpenCV 做透视校正。 - 表格还原
:OCR 结果里表格会被拆成散乱文字,可结合 PaddleOCR的版面分析模型(PP-Structure)单独识别表格并转成 docx 表格。 - 图片保留
:文本型 PDF 可用 page.get_images()提取内嵌图片,按坐标插入到 docx 对应位置。 - 多栏排版
:双栏文档需要先按 x 坐标切分左右栏,再分别聚合,否则会把两栏文字串在一起。 - 性能
:OCR 是瓶颈,200 DPI 已足够,再高收益有限;大文件建议按页并行处理。
6. 小结
这个工具的核心只有三件事:判断 PDF 类型、拿到带坐标的文字、按坐标重建段落。文本型 PDF 走快速通道,扫描型 PDF 走 OCR 通道,最终统一输出为 docx。
需要提醒的是,PDF 转 DOC 不可能做到 100% 无损还原,尤其是复杂排版、公式和表格。实际使用时,把它当作「先转出可编辑的初稿,再人工微调」的工具,效率提升会非常明显。














































































