Python批量doc转PDF:win32com与LibreOffice双方案实战

发布时间:2026/10/6 9:54:06
Python批量doc转PDF:win32com与LibreOffice双方案实战 前阵子财务部扔过来一个文件夹里面躺着几十份doc格式的通知和台账要求下午下班前全部转成PDF归档文件名还要按新编号重排。我盯着屏幕想了三秒决定不手动挨个“另存为”直接写个doc2pdf小工具搞定它。今天把这套思路和坑都整理出来从技术选型、两种主流实现、批量处理、踩坑实录到如何封装给同事用一条线讲下来。先说个可能颠覆很多初学者的认知把doc转PDF难度不在代码而在保真。Word文档里的分节符、文本框、批注、修订、公式、页眉页脚这些东西想靠解析XML再画到PDF里复杂度堪比做一整套排版软件。真正靠谱的思路是让现成的文档程序去干活然后用Python控制它也就是脚本化“打开-另存为PDF”这个过程。后面所有的方案本质上都是找渲染引擎不是自己渲染。1. 先把方案盘明白doc2pdf 不是只有一条路能走很多人搜“python doc2pdf”会看到一堆库名拿回来却发现有的只支持docx不支持doc有的在Windows上正常、换Linux就歇菜。我建议动手前先问自己三个问题答案直接决定选型。1.1 三个问题决定技术路线第一个问题目标机器装了什么办公软件。公司内网且正版Office全覆盖win32com是首选服务器、Docker或者个人电脑上不想为转换专门装Office就考虑LibreOffice路线。第二个问题要转的是.doc还是.docx。.doc是老二进制格式格式本身就有很多历史兼容问题.docx本质是zip包XML结构相对规范。如果用LibreOffice去转很旧的.doc容易出现排版错位这时候Word反而更有优势。第三个问题对PDF保真度要求多高。合同、标书、财务表单这种一字都不能差的场景直接选完整渲染引擎只是发给领导手机上看个大概那轻量方案就能凑合。三个维度拉成一张表看起来更清楚特征win32com WordLibreOffice headlesspython-docx reportlab操作系统WindowsWindows/macOS/Linux跨平台依赖本机安装Microsoft Office安装LibreOfficepip安装即可格式支持doc/docxdoc/docx/xls/ppt等仅简单docx保真度高和Word打开基本一致中高复杂版式可能轻微偏移低复杂样式直接翻车批量性能单进程顺序偏慢单进程较快可并行快但基本不可用维护成本低但被Office绑定中注意参数和字体高需要自己处理排版1.2 为什么不建议“纯Python解析”路线有段时间被“python实现某某转换”这类标题带偏试图用python-docx把段落读出来再用reportlab画成PDF觉得这样才是纯正方案。结果拿一份带文本框、图片环绕、页眉页脚的文档测试输出的PDF像被砸扁的纸箱子元素堆在一起。原因很简单docx的XML模型和PDF的绘制模型之间差了一个排版引擎。要把每个段落的缩进、分页符、浮动图片、域代码更新结果全部复刻工作量足够做一个商业软件。所以我在这里明确一点doc2pdf工具的核心是调用成熟渲染引擎不是自己解析文档。这也是为什么后文只讲win32com和LibreOffice两条主流路线而不是给你一堆看着高级、实际只适合玩具场景的“纯Python转换”示例。2. win32com 路线让 Word 自己在后台“另存为”如果Windows上已经装了Microsoft Office这条路最稳。思路也最直白用win32com控制Word应用程序打开目标文档再让它导出PDF。本质和你鼠标操作一模一样。2.1 最小可用的转换函数代码核心就十几行先看最朴素的版本import win32com.client import os def doc2pdf(input_path: str, output_path: str | None None) - str | None: word win32com.client.Dispatch(Word.Application) word.Visible False word.DisplayAlerts 0 input_path os.path.abspath(input_path) output_path os.path.abspath(output_path) if output_path else os.path.splitext(input_path)[0] .pdf try: doc word.Documents.Open(input_path, ReadOnlyTrue) doc.SaveAs(output_path, FileFormat17) # 17 wdFormatPDF doc.Close() return output_path except Exception as e: print(f转换失败: {e}) return None finally: word.Quit()word.Visible False表示Word后台运行不在桌面弹出窗口。word.DisplayAlerts 0是关掉大部分提醒弹窗。FileFormat17对应wdFormatPDF这行是关键中的关键。这里有个细节第一次跑被COM控制的Word时有些机器上窗口还是会闪一下。可以配合word.WindowState 0让主窗口最小化减少视觉干扰但不用纠结因为闪一下就过去了对转换结果没影响。2.2 为什么推荐 ExportAsFixedFormat 而不是 SaveAs上面的代码能跑通但实际业务里我更推荐用ExportAsFixedFormat它对参数的控制精细得多doc.ExportAsFixedFormat( OutputFileNameoutput_path, ExportFormat17, # wdExportFormatPDF OpenAfterExportFalse, OptimizeFor0, # wdExportOptimizeForPrint Range0, # wdExportAllDocument From1, To1, Item0, # wdExportDocumentContent IncludeDocPropsTrue, KeepIRMTrue, CreateBookmarks1, # wdExportCreateHeadingBookmarks DocStructureTagsTrue, BitmapMissingFontsTrue, UseISO19005_1False )它比SaveAs多出来的价值在于可以只导出部分页面通过CreateBookmarks生成目录书签通过OptimizeFor指定按打印优化还是按屏幕优化还能把缺失字体直接转成位图而不是悄悄跳过。标书、合同这类对格式要求极高的场景这些参数非常管用。如果只是简单转PDF用SaveAs省事但既然写工具了我建议一上来就用ExportAsFixedFormat避免后面需要加功能时再改结构。2.3 隐藏弹窗、禁用宏和残留进程清理用COM最头疼的是弹窗。最常见的情况是文档打开时出现“是否恢复”“只读建议”“是否启用宏”等对话框一个弹窗就能让转换脚本卡在当场既不报错也不继续。处理优先级如下先禁用Word的自动提示word.Options.DoNotPromptForConvert True word.AutomationSecurity 3 # msoAutomationSecurityForceDisable禁用宏再在Documents.Open时显式传参doc word.Documents.Open( input_path, ConfirmConversionsFalse, ReadOnlyTrue, AddToRecentFilesFalse, RevertFalse )ConfirmConversionsFalse阻止格式转换确认框ReadOnlyTrue保证源文件不被意外改动AddToRecentFilesFalse不会把每个文件都塞进“最近使用的文档”列表。即便如此偶尔还会有Word进程卡在后台占用文件。我的习惯是每次批量转换结束后执行一次word.Quit()并用try/finally保证一定执行。如果实在出现僵尸进程可以用taskkill /f /im WINWORD.EXE清理但清理前要确认没有别的Word文档正开着否则会把同事正在写的文档也一起杀掉。3. LibreOffice 路线没有 Office 也能转但暗坑不少换到Linux服务器、或者目标机器不想装Office时LibreOffice是主选。它自带命令行转换模式不需要写任何COM代码。3.1 soffice 一行命令打通多格式转换命令其实很简单soffice --headless --convert-to pdf --outdir /output/dir /input/dir/xxx.docx在Python里调用时不要用os.system拼字符串用subprocess.run传参数列表避免路径里的空格和特殊字符被shell误解import subprocess def doc2pdf_libreoffice(input_path: str, output_dir: str) - bool: output_dir os.path.abspath(output_dir) os.makedirs(output_dir, exist_okTrue) cmd [ soffice, --headless, --convert-to, pdf, --outdir, output_dir, os.path.abspath(input_path) ] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: print(result.stderr) return False return True注意--outdir必须是已存在的目录否则LibreOffice会直接报“Output directory does not exist”。代码里用os.makedirs(exist_okTrue)把这层兜住。另一个跨平台细节Linux上命令可能是libreoffice而不是soffice程序里最好先用shutil.which(soffice)探测一次找不到再试libreoffice。3.2 中文字体、profile 锁、outdir 三个重灾区LibreOffice第一大坑是中文字体。如果系统里没有中文字体转换出来的PDF会变成方框或者缺字。Windows上一般不用管系统字体在那里Linux服务器上必须装字体。常见做法是把Windows字体目录里的simsun.ttc、msyh.ttc等文件拷到/usr/share/fonts下然后执行fc-cache -f刷新字体缓存。转换前可以用fc-list :langzh确认系统里有中文字体。第二个坑是profile锁。LibreOffice每次运行都要读取用户配置目录如果前一次异常退出留下了锁定文件后续转换会报错。解决办法是在命令里加独立profilesoffice --headless -env:UserInstallationfile:///tmp/lo_profile --convert-to pdf --outdir out in.docx批量并行转换时每个子进程必须用不同的profile目录否则它们会互相抢锁甚至全部失败。Windows下路径要写成file:///C:/temp/lo_profile这样的形式这个细节经常被忽略。第三个坑就是前面说的--outdir必须存在。三个坑总结成一句话LibreOffice能干活但环境和参数要伺候到位。3.3 LibreOffice 转 PDF 和 Word 转 PDF 的版面差异做完对比测试后建议大家对保真度要有心理预期。同一份带表格和页眉页脚的文档Word转PDF基本一比一还原LibreOffice大体能看但如果原文档用了很个性化的字体、复杂的文本框分层会出现轻微偏移或字体替换。项目Word COMLibreOffice页眉页脚还原度高大都能还原复杂表格还原度高多数正常个别单元格错位字体嵌入自动嵌入依赖系统字体缺失时替换书签链接ExportAsFixedFormat可生成默认保留旧版.doc兼容和已装Word版本一致对特别老的.doc兼容一般如果你的业务对版式要求极高我建议在同一台机器上先用两种方式各转一份让最终使用者肉眼对比一次再定正式路线。别等到批量转完发现页码标记不对再回头找原因。3.4 顺手救活 xlsx 和 ppt从 doc2pdf 到通用转换LibreOffice的另一个好处是顺带处理Excel和PowerPoint。把命令里的--convert-to pdf保留输入文件换成对应扩展名即可。比如财务要的月度报表其实是xlsx我就把工具扩了一个--engine libreoffice参数一个通用函数就把doc/docx/xls/xlsx/ppt/pptx全转PDF了。这一步在“工具化”章节里会体现得更清楚。4. 批量目录扫描与并发提速的取舍单人单文件场景不需要批量处理但现实需求往往是“给我把整个文件夹都转了”。4.1 pathlib 递归遍历自动跳过已转出的PDF批量转换的第一步是拿到所有待转换文件的清单。我用pathlib而不是os.walk代码更短也更易读from pathlib import Path def collect_docs(source_dir: str, skip_existing: bool True) - list[Path]: exts {.doc, .docx, .xls, .xlsx, .ppt, .pptx} files [] for p in Path(source_dir).rglob(*): if p.suffix.lower() in exts and not p.name.startswith(~$): if skip_existing and p.with_suffix(.pdf).exists(): continue files.append(p) return filesrglob(*)递归遍历目录~$开头的文件是Office临时文件必须过滤skip_existing这个开关很重要批量任务中断后重新跑一遍时会自动跳过已经成功的文件节省大量时间。文件后缀大小写也要处理.doc和.DOC都要认。4.2 并发提速要分引擎来看能不能用ThreadPoolExecutor把转换速度提上去实测下来win32com路线最好别并行。原因是Word COM是单实例、有状态的多个线程同时控制同一个Word进程轻则互相干扰重则让文档打开出错、进程崩溃。稳妥做法是单线程循环配合每处理N个文档重启一次Word进程释放内存保持稳定。LibreOffice则可以试并行但每个进程必须指定不同的UserInstallation目录否则profile锁会让它们互相等待效果反而更差。我做过一次20个文档的测试单进程LibreOffice约40秒4进程并行约15秒提升确实明显。并行粒度建议按文件分配不要尝试在单个文件上多进程转换LibreOffice自己也会对单文件做多线程处理抢不到太多收益。5. 实测踩坑清单卡死、乱码、路径和校验这段内容是我最想写的。网上教程大多只给“成功路径”但实际跑起来坑基本都在环境交互上。5.1 Word 弹窗让批处理停摆的完整排查链路批量转第二次的时候脚本在第三个文件卡住既没有报错也没有输出。我第一反应是已经设置了VisibleFalse应该没弹窗但仔细一想弹窗藏不住不代表它不存在。排查链路是这样的先在每份文档转换前后打印时间戳发现前两份正常第三份进入转换后没有结束日志。然后把Documents.Open的参数逐一调整发现是“是否恢复文档”提示在处理一份以前异常关闭的docx时出现。最终处理办法是三条word.Options.DoNotPromptForConvert True word.AutomationSecurity 3 doc word.Documents.Open(path, ConfirmConversionsFalse, ReadOnlyTrue, AddToRecentFilesFalse)配置完再跑所有弹窗都消失了。这里最值得记住的是问题不一定是代码逻辑错了而是Word替你做决定但被隐藏了。排查COM类问题建议在流程里打印异常和文档名让卡住的位置暴露出来。5.2 中文路径、特殊字符与乱码日志的处理还有一次在Windows服务器上跑路径里带中文转换直接抛UnicodeDecodeError。根子不在Word转换本身而在subprocess或控制台编码。解决方法是传递路径时用绝对路径对象不要用字符串拼接避免Python的str和bytes隐式转换。subprocess.run用列表传参会天然规避shell排除特殊字符的问题我建议一直坚持这个习惯。如果脚本输出的日志中文乱码一般不是转换坏了而是控制台编码不是UTF-8。Windows下可以在脚本开头加import sys sys.stdout.reconfigure(encodingutf-8)或者在调用Python时设置环境变量PYTHONIOENCODINGutf-8。这不影响最终PDF但能让你在排查时看清到底报了什么错。5.3 用 pypdf 校验页数把问题拦在交付之前转换完不等于万事大吉。我养成了习惯每转完一个文件立刻用pypdf读一次页数同时确认文件头是%PDF并把页数记录到日志from pypdf import PdfReader def verify_pdf(path: Path) - int: try: reader PdfReader(path) return len(reader.pages) except Exception: return -1页数为-1说明文件损坏或根本没生成成功页数为0也要警惕可能内容异常。再配合人工抽查两三份源文档和PDF对比基本能把批量异常在交付前拦下来。给财务的数据一旦交付返工成本可比检查成本高太多。6. 把工具做到可以交付给同事用一个人用的脚本可以很随意但“给财务部用”就得有个像样的命令行工具。6.1 argparse 命令行参数设计我用argparse做了以下参数import argparse def main(): parser argparse.ArgumentParser(description批量doc/docx/xls/ppt转PDF) parser.add_argument(-i, --input, requiredTrue, help输入文件或目录) parser.add_argument(-o, --output, defaultoutput, help输出目录默认./output) parser.add_argument(-e, --engine, choices[word, libreoffice], defaultword, help转换引擎) parser.add_argument(--skip-existing, actionstore_true, help跳过已生成的PDF) parser.add_argument(--parallel, typeint, default1, helpLibreOffice并行进程数) args parser.parse_args() run_batch(args)默认--engine word因为办公场景WindowsOffice最常见Linux服务器上用--engine libreoffice指定内部自动探测soffice或libreoffice命令。--skip-existing支持中断后续跑--parallel只对LibreOffice生效程序会为每个并行进程自动加独立profile。6.2 日志、失败继续和汇总报告批量任务最怕“一个文件失败全部白做”。所以循环里每个文件单独try/except失败记录原因并继续。日志用logging同时输出到控制台和文件logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(convert.log, encodingutf-8), logging.StreamHandler() ] )最后把所有失败列表集中打印同时生成一份summary.txt写清楚成功数量、失败文件及原因。这份汇总在交付时非常有用同事不用打开日志就能找到问题文件你也能快速定位是环境问题还是个别文件损坏。6.3 打包 exe 的注意点以及我的交付习惯要不要把Python脚本打包成exe我的建议是如果只在会装Python的人手里跑脚本就够了如果给不懂技术的同事用打包成exe更省心。PyInstaller命令大致如下pip install pyinstaller pyinstaller -F --console doc2pdf.py--console保留控制台这样参数错误时用户能看到提示。引擎选win32com时exe本身不包含Office运行机器上仍要装OfficeLibreOffice同理只是不需要装Python了。PyInstaller打包win32com程序偶尔会遇到运行时找不到pywintypes加上这两个隐藏导入一般能解决pyinstaller -F --console --hidden-import pywintypes --hidden-import pythoncom doc2pdf.py我目前的交付习惯是代码仓库里放一份源码和requirements.txt再把打包好的exe放内网共享目录。同事直接用exe你排查问题直接用源码两边都舒服。整套工具做下来我最大的体会是先把环境问题解决掉再谈写代码。装好Office、装好字体、试通一条命令、转一份真实文档肉眼对比远比调了一整天参数却因为在Linux上没装中文字体而翻车强。最后分享一个小习惯在批量任务里加一个--dry-run参数先扫描目录把所有待转换文件打印出来不真正调用Word或LibreOffice。这样能提前发现哪些文件命名有问题、哪些已经转换过避免辛辛苦苦跑完才发现选错了目录。专业工具不需要多花哨的界面把几个朴实的流程安排顺就能把重复劳动变成一条可靠的流水线。