PySide6 + LibreOffice 实现 Word 批量转 PDF 桌面工具

发布时间:2026/10/18 21:29:22
PySide6 + LibreOffice 实现 Word 批量转 PDF 桌面工具 简介这份资源面向需要批量处理文档格式转换的办公人员与Python初学者提供一套基于PySide6与docx2pdf库实现的Word转PDF桌面工具。它通过图形化界面替代命令行操作用户只需选择待转换的Word文件并点击触发即可在指定位置生成PDF省去手动另存为的重复劳动适合日常办公中频繁处理文档格式转换的场景。压缩包为rar格式内含1个py脚本文件体积约1KB轻量易部署无需复杂环境配置即可运行。目前已有167人学习下载说明该方案在同类需求中具备一定参考价值。读者可获得完整的转换逻辑源码理解PySide6界面搭建与docx2pdf调用的结合方式并在此基础上扩展批量处理、路径选择或结果提示等功能快速落地属于自己的文档转换小工具。1. 用 PySide6 把 Word 转 PDF桌面端文档转换工具的最小可用路径手里有一批 Word 文档要批量转成 PDF又不想依赖在线服务、不想装 Office、更不想让文件离开本机——这是很多做商业资料整理、合同归档、课件分发的开发者都会碰到的场景。PySide6 做界面、底层调转换引擎把「选文件、设参数、看进度、拿结果」这条链路封成一个能双击运行的桌面工具是我认为目前 Python 桌面端最省心的组合之一。它适合两类人一类是手头有大量 docx 需要离线批量处理的从业者另一类是想拿一个完整 GUI 项目练手、顺便把文档处理流程跑通的 Python 开发者。下面这份资源就是围绕这个目标拆出来的我会把选型理由、核心代码、参数边界和踩过的坑一次讲清。2. 转换引擎选型为什么 PySide6 只做壳脏活交给别的库PySide6 本身是 Qt 的 Python 绑定负责窗口、按钮、进度条、文件对话框这些交互层它不负责解析 Word 也不负责生成 PDF。真正干转换的活儿必须另找引擎。这一步选错后面全是返工。2.1 三条主流路线的取舍我一般会把可选方案归成三类按「是否依赖 Office」和「保真度」两个维度来筛方案依赖保真度适用场景docx2pdf必须装 Microsoft Word高本机有 Office 的 Windows/macOSLibreOffice 无头模式装 LibreOffice较高跨平台、可离线批量python-docx reportlab纯 Python低只提取文字、不要求排版docx2pdf 的原理是调用本机 Word 的 COM 接口Windows或 AppleScriptmacOS转换质量几乎和手动另存为一致但前提是机器上得有 Word而且它没法在没装 Office 的服务器上跑。LibreOffice 的soffice --headless --convert-to pdf是另一条路跨平台、可脚本化排版还原度对绝大多数公文和报告够用缺点是首次启动慢、复杂图表偶尔错位。纯 Python 那条路我不推荐用于「文档转换」这个需求因为它基本等于重新排版字体、页眉页脚、表格边框全得自己画。提示如果你的目标用户机器上大概率装了 Office优先 docx2pdf如果是分发给不确定环境的同事LibreOffice 方案更稳。2.2 用 subprocess 调 LibreOffice 的最小封装选 LibreOffice 路线的话核心就是拼一条命令行然后用 subprocess 拉起来。下面这段是我常用的封装做了路径引号和超时处理import subprocess import os from pathlib import Path def convert_with_libreoffice(docx_path: str, out_dir: str, timeout: int 120) - bool: 调用 LibreOffice 无头模式把 docx 转成 pdf docx_path: 输入的 Word 文件绝对路径 out_dir: PDF 输出目录 timeout: 单文件超时秒数防止卡死 soffice rC:\Program Files\LibreOffice\program\soffice.exe # 按实际安装路径改 if not Path(soffice).exists(): raise FileNotFoundError(找不到 soffice请检查 LibreOffice 安装路径) cmd [ soffice, --headless, # 不弹窗口 --norestore, # 不恢复上次会话 --convert-to, pdf, # 目标格式 --outdir, out_dir, # 输出目录 docx_path ] try: result subprocess.run( cmd, capture_outputTrue, timeouttimeout, checkFalse ) # LibreOffice 成功时 returncode 通常为 0但偶发非 0 也生成了文件 pdf_name Path(docx_path).stem .pdf return (Path(out_dir) / pdf_name).exists() except subprocess.TimeoutExpired: return False这段代码的关键点有三个。第一--headless必须加否则会弹出 LibreOffice 界面批量处理时窗口会刷屏。第二--outdir指定的目录必须提前存在LibreOffice 不会自动创建传一个不存在的路径它会静默失败。第三timeout是保命参数我遇到过损坏的 docx 让 soffice 卡死十几分钟的情况没有超时整个批处理就挂住了。返回值我没有直接信returncode而是去检查目标 PDF 是否真的生成因为 LibreOffice 在某些版本下转换成功也返回非零码这是个玄学点。2.3 PySide6 界面层的最小骨架界面部分不用做复杂一个文件选择、一个输出目录选择、一个开始按钮、一个进度条、一个日志框就够了。下面是把上面转换函数接进 Qt 事件循环的核心结构from PySide6.QtCore import QThread, Signal from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QFileDialog, QProgressBar, QTextEdit, QLabel) import sys class ConvertWorker(QThread): 后台线程执行转换避免阻塞 UI progress Signal(int) log Signal(str) def __init__(self, files, out_dir): super().__init__() self.files files self.out_dir out_dir def run(self): total len(self.files) for i, f in enumerate(self.files, 1): ok convert_with_libreoffice(f, self.out_dir) self.log.emit(f{成功 if ok else 失败}: {f}) self.progress.emit(int(i / total * 100)) class MainWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(Word 转 PDF 工具) self.files [] layout QVBoxLayout(self) self.btn_pick QPushButton(选择 Word 文件) self.btn_pick.clicked.connect(self.pick_files) layout.addWidget(self.btn_pick) self.btn_out QPushButton(选择输出目录) self.btn_out.clicked.connect(self.pick_out) layout.addWidget(self.btn_out) self.bar QProgressBar() layout.addWidget(self.bar) self.log_box QTextEdit() self.log_box.setReadOnly(True) layout.addWidget(self.log_box) self.btn_run QPushButton(开始转换) self.btn_run.clicked.connect(self.start) layout.addWidget(self.btn_run) self.out_dir def pick_files(self): files, _ QFileDialog.getOpenFileNames(self, 选文件, , Word (*.docx *.doc)) self.files files def pick_out(self): self.out_dir QFileDialog.getExistingDirectory(self, 选输出目录) def start(self): if not self.files or not self.out_dir: self.log_box.append(请先选文件和输出目录) return self.worker ConvertWorker(self.files, self.out_dir) self.worker.progress.connect(self.bar.setValue) self.worker.log.connect(self.log_box.append) self.worker.start() if __name__ __main__: app QApplication(sys.argv) w MainWindow() w.show() sys.exit(app.exec())这里最容易被新手忽略的是QThread。转换是耗时操作如果直接在按钮的槽函数里跑循环界面会整个卡死进度条也不刷新用户以为程序崩了。把转换逻辑丢进QThread的run里通过Signal回传进度和日志UI 线程才能保持响应。progress信号传 int 给进度条log信号传字符串给文本框这是 Qt 里跨线程通信的标准做法不要在线程里直接操作控件。3. 批量处理与参数调优把单文件脚本变成能用的工具单文件能转只是起点真正干活时面对的是几十上百个文件还有各种命名、格式、异常情况。这一章讲怎么把上面的骨架补成能扛住批量任务的版本。3.1 文件遍历与输出命名策略批量场景下输入可能是一个目录里面还嵌套着子目录。我一般用Path.rglob递归拿所有 docx然后按「保持相对目录结构」的方式输出避免不同子目录里的同名文件互相覆盖from pathlib import Path def collect_docx(root: str): 递归收集所有 docx返回 (绝对路径, 相对路径) 列表 root_path Path(root) result [] for p in root_path.rglob(*.docx): if p.name.startswith(~$): # 跳过 Word 临时文件 continue result.append((str(p), str(p.relative_to(root_path)))) return result def build_out_path(out_dir: str, rel_path: str) - str: 根据相对路径生成输出 PDF 路径并创建父目录 rel Path(rel_path).with_suffix(.pdf) target Path(out_dir) / rel target.parent.mkdir(parentsTrue, exist_okTrue) return str(target)~$开头的是 Word 打开文档时生成的临时锁文件体积很小但也会被rglob匹配到不排除的话转换必然失败日志里会刷一堆报错。relative_to保留子目录层级with_suffix把扩展名换成 pdfmkdir(parentsTrue)确保输出目录结构跟输入一致。这套组合下来一个几百文件的目录树也能原样映射到输出侧。3.2 并发数怎么定别一上来就开满LibreOffice 的soffice进程本身不是为高并发设计的同时拉起太多实例会互相抢资源甚至出现「一个实例锁住用户配置目录导致其他实例启动失败」的问题。我的经验是并发数控制在 2 到 4 之间具体看机器核数和文件大小。用concurrent.futures可以这样写from concurrent.futures import ThreadPoolExecutor, as_completed def batch_convert(files, out_dir, max_workers3): files: [(abs_path, rel_path), ...] max_workers: 并发数建议 2-4 results {ok: 0, fail: 0} with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {} for abs_p, rel_p in files: out_p build_out_path(out_dir, rel_p) fut pool.submit(convert_with_libreoffice, abs_p, str(Path(out_p).parent)) future_map[fut] abs_p for fut in as_completed(future_map): src future_map[fut] try: if fut.result(): results[ok] 1 else: results[fail] 1 except Exception as e: results[fail] 1 print(f异常 {src}: {e}) return resultsmax_workers是核心参数。设成 1 就是串行最稳但最慢设成 8 以上你会看到大量「soffice 启动失败」或者转换出来的 PDF 是 0 字节。我一般默认 3遇到大文件多的批次降到 2。as_completed让结果按完成顺序返回配合计数就能实时更新进度。注意这里每个任务传的输出目录是Path(out_p).parent因为convert_with_libreoffice内部是按源文件名生成 PDF 的输出目录对了文件名自然对。3.3 转换质量相关的参数LibreOffice 转换 PDF 时字体嵌入和图片压缩是两个影响观感的点。默认配置下如果文档用了系统没装的字体PDF 里会替换成默认字体排版就歪了。解决办法是在目标机器上装齐文档用到的字体或者在转换前用脚本检查字体。另一个是图片LibreOffice 默认会按原分辨率嵌入扫描件多的文档转出来 PDF 可能几百 MB需要压缩的话得走--convert-to pdf:writer_pdf_Export加过滤器参数但配置项比较绕我一般先用默认体积实在离谱再单独处理。注意--convert-to pdf和--convert-to pdf:writer_pdf_Export是两种写法后者才能带导出过滤器选项但参数格式在不同 LibreOffice 版本间有差异升级版本后要重新验证。4. 避坑与排查那些让转换静默失败的细节这一章是我踩过的坑里挑出来最有代表性的五条每条都按「现象 → 原因 → 解决」写遇到问题可以对着查。4.1 转换成功但 PDF 是 0 字节现象日志显示成功进度条走完打开输出目录发现 PDF 文件存在但大小为 0。原因通常是 LibreOffice 的用户配置目录被占用或损坏多实例并发时尤其常见。解决给每个转换进程指定独立的用户配置目录加参数-env:UserInstallationfile:///临时路径让实例之间不抢配置。或者干脆把并发降到 1 先验证。4.2 中文文件名转出来乱码或找不到文件现象源文件叫「合同模板.docx」转换后输出目录里找不到对应 PDF或者出现一个乱码名的文件。原因是 subprocess 传参时编码不一致Windows 下默认可能是 GBK。解决确保 Python 脚本文件本身用 UTF-8 保存subprocess 调用时如果遇到编码问题显式传encodingutf-8并且路径统一用str(Path(...))而不是手工拼字符串。4.3 界面卡死、进度条不动现象点了开始按钮窗口变灰进度条一直 0几秒后系统提示程序未响应。原因就是转换逻辑跑在主线程里。解决所有耗时操作必须放进QThread或QThreadPool通过信号回传状态。上面第 2 章的ConvertWorker就是干这个的别图省事直接在槽函数里写循环。4.4 部分 docx 转换报错但其他正常现象一批文件里总有那么几个失败单独拿出来手动转又是好的。原因可能是文件本身有损坏、或者用了 LibreOffice 不支持的复杂域代码比如某些自动编号、嵌入对象。解决在转换前做一次预检用python-docx尝试打开文件打不开的直接标记为「源文件异常」跳过不要让它拖垮整批任务。4.5 打包成 exe 后找不到 soffice现象开发环境跑得好好的用 PyInstaller 打包成 exe 发给同事一运行就报找不到 soffice。原因是打包时没把 LibreOffice 的路径逻辑考虑进去或者目标机器根本没装 LibreOffice。解决要么在程序启动时检测 soffice 是否存在不存在就弹提示引导安装要么把 LibreOffice 便携版一起打包启动时用相对路径定位。我一般选前者让用户自己装省得包体积爆炸。5. 进阶技巧把转换工具做成可复用的命令行 GUI 双模式做到这里GUI 能用了但每次改参数都要点界面批量脚本化的时候不方便。我的习惯是给同一个转换核心再套一层命令行入口用argparse解析参数这样既能双击运行也能在批处理脚本里调用。import argparse def cli_main(): parser argparse.ArgumentParser(descriptionWord 转 PDF 批量工具) parser.add_argument(input, help输入目录或单个 docx 文件) parser.add_argument(-o, --output, requiredTrue, help输出目录) parser.add_argument(-w, --workers, typeint, default3, help并发数默认 3) args parser.parse_args() src Path(args.input) if src.is_file(): files [(str(src), src.name)] else: files collect_docx(str(src)) result batch_convert(files, args.output, max_workersargs.workers) print(f完成成功 {result[ok]}失败 {result[fail]}) if __name__ __main__: cli_main()这样python convert.py ./docs -o ./pdfs -w 2就能在终端里跑批量GUI 入口单独保留。两个入口共用batch_convert和convert_with_libreoffice逻辑只有一份改一处两边都生效。验证转换结果我一般做两件事一是用pdfminer或PyPDF2读一下生成的 PDF 页数跟源 docx 的页数预期对一下差太多说明排版出问题了二是随机抽几个文件打开肉眼看一下表格和页眉。自动化只能保证「文件生成了」保真度还得靠抽查。从那以后我每次做这类工具都会先把「单文件转换 → 批量并发 → 异常隔离 → 双入口」这四步走一遍再考虑加花哨功能。希望帮到你。本文还有配套的精品资源点击获取