ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python调用LibreOffice实现ODT转PDF实战指南

Python调用LibreOffice实现ODT转PDF实战指南 上周有个朋友发来一份 ODT 文件说在 WPS 里转 PDF 后表格全部错位问我能不能写个 Python 脚本稳定搞定。这个问题其实特别典型ODT 转 PDF 在办公自动化里天天遇到但很多人只会改后缀名、用在线转换网站文件一多要么格式崩要么隐私泄露。我自己在几个项目里折腾过各种方案踩了不少坑今天把这套实战经验完整捋一遍。内容围绕 Python 调用 LibreOffice 命令行实现文档转换也会聊到纯 Python 库的局限性、批量并发处理的正确姿势、Web 服务里怎么用才不把进程卡死。适合正在搭建文档处理管线的 Python 开发者也适合办公自动化刚入门、想少走弯路的朋友。真正要做的是渲染而不是改名ODT 内部是一堆 XML 和资源文件打包成的 ZIPPDF 则是排版引擎把每个字符、图片、表格按固定坐标绘制出来的结果两者之间必须有一个软件真正打开文档、计算分页、导出 PDF。LibreOffice 就是干这个的最可靠选择而且它提供了无需图形界面的命令行模式非常适合被 Python 脚本调用。1. ODT 转换 PDF 的本质为什么看起来简单却问题不断1.1 改扩展名和真正导出的区别先回答一个很多人问过的蠢问题把document.odt直接重命名为document.pdf会怎样答案是不会怎样双击打开 PDF 会报错。原因在于 ODT 和 PDF 的文件结构完全不同。ODT 本质是一个 ZIP 压缩包里面装着content.xml、styles.xml、meta.xml和一堆图片资源而 PDF 至少包含对象树、内容流、交叉引用表等结构需要由渲染引擎绘制出来。可以类比成做菜ODT 是一份菜谱和一堆食材PDF 是最终端上桌的成品菜肴。重命名只是给菜谱贴了个成品的标签并没有人真正烹饪。真正的转换必须由某个软件打开 ODT解析样式、计算文本流的换行与分页、确定图片位置、处理表格宽度再调用字体渲染引擎输出页面。LibreOffice、OpenOffice、ONLYOFFICE 这类办公套件内置的就是这个能力。1.2 市面上能得到的三条路线从 Python 的角度出发想要把 ODT 转成 PDF可选的路线大致三条调用 LibreOffice/OpenOffice 命令行最成熟、保真度最高能处理复杂的页眉页脚、样式、图表。LibreOffice 提供 headless 模式无界面后台运行适合服务器。用纯 Python 库解析 ODT 并自己生成 PDF比如odfpyreportlab理论上可行但实现成本极高只适用于极简纯文本文档。调用在线转换 API上传文档然后下载结果适合不要求隐私、偶尔转换的个人场景不适合批量自动化因为网络传输慢、文件大小受限、还有数据泄露风险。我的选型结论很直接只要条件是稳定、私有化、可控就选 LibreOffice 命令行。下面所有方案都围绕这条路展开。1.3 为什么第一个推荐是 LibreOfficeLibreOffice 是自由开源的办公套件它的 Writer 组件原生支持 ODT 格式也就是说 ODT 是它自己的文件格式。用自己最熟悉格式的软件去转换错误率自然最低。它提供soffice命令可以通过--headless --convert-to pdf直接批量转换不弹窗、不需要登录、不依赖网络。GitHub 上很多文档转换项目最终都退回到这个方案道理就在这。2. 环境准备Python 环境、LibreOffice 安装和版本兼容性坑2.1 Windows 系统下安装与配置Windows 下安装 LibreOffice 很简单去官网下载最新安装包安装时一直下一步就行。安装完成后重点注意soffice.exe的路径默认在C:\Program Files\LibreOffice\program\soffice.exe有些版本也存在于C:\Program Files (x86)\LibreOffice\program\soffice.exe。建议把这个目录加到系统 PATH 环境变量里否则 Python 每次调用时都要写绝对路径脚本换一台机器就废了。添加 PATH 的方式右键此电脑 → 属性 → 高级系统设置 → 环境变量 → 在系统变量里找到 Path → 编辑 → 新建 → 粘贴安装目录 → 确定。然后在新的命令行窗口里输入soffice --version如果显示版本号说明配置成功。如果提示找不到命令多半是环境变量没生效重开终端或者直接用绝对路径。这里有个容易忽略的点LibreOffice 有 32 位和 64 位两种版本但 Python 的subprocess调用并不关心位数只要系统能运行对应程序即可。不过建议和操作系统位数一致避免某些第三方组件不兼容。2.2 Linux 系统下的安装与字体坑Linux 服务器上安装一般用 apt/yum。Ubuntu/Debian 上我通常只安装 Writer 组件减小体积sudo apt update sudo apt install -y libreoffice-writer如果嫌依赖解析麻烦也可以直接sudo apt install -y libreoffice会装全家桶但磁盘占用多几个 GB。瘦身党可以装libreoffice-core再加libreoffice-writer不过新手还是建议装完整版省得缺组件后排查半天。装完检查soffice是否符合预期which soffice通常输出/usr/bin/soffice。Linux 上最大的坑是字体。服务器不带图形界面通常也不装中文字体导致转换出来的 PDF 中文全是方框或乱码。我踩过不止一次。解决办法是安装中文字体包sudo apt install -y fonts-noto-cjk装完可以用fc-list | grep -i noto验证。实际演示时我还专门写过一段代码检查系统中文字体是否存在后面会给出。2.3 macOS 上的一行命令macOS 上用 Homebrew 最方便brew install --cask libreoffice安装后可执行文件路径不是直接soffice而是/Applications/LibreOffice.app/Contents/MacOS/soffice建议在.zshrc里加一行 aliasalias soffice/Applications/LibreOffice.app/Contents/MacOS/soffice另外 macOS 会要求首次运行白名单授权如果在服务器上以launchd方式跑注意给足够权限。3. 方案一subprocess 调用 LibreOffice 命令行的完整解读3.1 基础命令结构与参数语义LibreOffice 的命令行转换核心长这样soffice --headless --convert-to pdf --outdir /output/path /input/file.odt四个关键参数逐一解释--headless无头模式不启动图形界面。服务器上必须加否则会因为没有显示器直接报错。--convert-to pdf指定输出格式为 PDF。LibreOffice 内置导入过滤器会自动根据目标格式调用 Writer 导出。--outdir /output/path指定输出目录。值得注意的是如果不加--outdir生成的 PDF 会放在输入文件所在目录并覆盖实际测试并不会覆盖原 ODT只是在旁边生成同名 PDF。但为了保险我始终显式指定--outdir。/input/file.odt输入文件完整路径。路径中不能有不符合编码的字符否则转换会静默失败。还可以更精细地指定 PDF 过滤器参数例如soffice --headless --convert-to pdf:writer_pdf_Export:{SelectPdfVersion:{type:long,value:17}} test.odt这段的意思是使用writer_pdf_Export过滤器并设置 PDF 版本为 1.7值为 17 表示 PDF/A-1a需要注意不同版本枚举不同。日常转换不需要这种级别但当你需要控制 PDF/A 属性、压缩质量、字体嵌入策略时就需要研究过滤器选项了。一般默认参数已经够用。3.2 Python 调用代码示例正确处理路径、输出和退出码Python 里用subprocess调用时最忌讳用shellTrue拼字符串因为路径里的空格、中文、引号会让命令直接卡壳。正确做法是把命令参数写成列表由subprocess自行处理转义import subprocess import os import sys def odt_to_pdf(input_path: str, output_dir: str) - str: input_path os.path.abspath(input_path) output_dir os.path.abspath(output_dir) os.makedirs(output_dir, exist_okTrue) cmd [ soffice, --headless, --convert-to, pdf, --outdir, output_dir, input_path ] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout120) if result.returncode ! 0: raise RuntimeError(f转换失败: {result.returncode}\n{result.stderr}) base os.path.splitext(os.path.basename(input_path))[0] pdf_path os.path.join(output_dir, base .pdf) if not os.path.isfile(pdf_path): raise FileNotFoundError(fPDF 文件未生成: {pdf_path}) return pdf_path几个细节说明capture_outputTrue用于捕获 LibreOffice 的 stdout 和 stderr方便排查问题。textTrue把输出解码为字符串否则是 bytes。timeout120防止转换几十页的大文档时进程挂死。如果转换超时subprocess.run会抛出TimeoutExpired外层捕获后应该杀掉残留 soffice 进程这点在并发场景尤为重要。returncode非零时抛出异常是常规操作。但你一定会遇到returncode 为零但 PDF 没生成的情况比如输入文件损坏、字体缺失时 LibreOffice 可能只输出一条警告仍然返回 0。所以生成后检查 PDF 文件是否存在非常有必要。3.3 同名文件覆盖与特殊字符问题LibreOffice 在输出目录下生成与输入 ODT 同名的 PDF。如果目标目录已经存在同名 PDF新转换会直接覆盖旧文件不会有任何确认提示。这个行为在批量重复转换时反而省事但如果你希望保留历史版本要自己在脚本里处理冲突比如按时间戳重命名。文件名里如果带有、(,),[,],#等特殊字符subprocess 列表形式不会有问题因为没经过 shell 解析。但如果路径里包含非 UTF-8 编码的字符比如从某个旧 Windows 系统拷过来的 GBK 文件名Python 侧就可能先报编码错误这通常不是 LibreOffice 的问题而是操作系统文件名编码不统一。生产环境中最好在入库时就把文件名统一成 ASCII 或规范 UTF-8。3.4 高频典型问题用户配置文件锁导致的间歇性失败这个坑几乎每个用 LibreOffice 做并发的团队都会遇到。LibreOffice 首次启动会创建一个用户配置文件目录Windows 在%APPDATA%\LibreOffice\4\userLinux 在~/.config/libreoffice/4/user。当你连续调用好几个soffice进程时它们会争抢这个配置文件导致报错信息类似Error: source file could not be loaded或者The lock file ... already exists, another LibreOffice process is using it实际上文件本身没问题纯粹是并发冲突。解决办法是在每次调用时给 LibreOffice 一个独立的临时配置目录soffice -env:UserInstallationfile:///tmp/lo_profile_123 --headless --convert-to pdf ...这样每个进程用独立的 profile互不干扰。Python 里可以用进程 ID 或随机字符串拼一个唯一路径import tempfile profile_dir tempfile.mkdtemp(prefixlo_profile_) cmd [ soffice, f-env:UserInstallationfile://{profile_dir}, --headless, --convert-to, pdf, --outdir, output_dir, input_path ]用完最好把临时目录回收。后面讲并发时还会专门说。4. 方案二纯 Python 库方案为什么不可靠——从一个 content.xml 实验讲起4.1 ODT 内部结构长什么样为了说清楚纯 Python 方案的局限我直接解压一个 ODT 文件看看内部结构unzip -l sample.odt典型输出包含mimetype content.xml styles.xml meta.xml settings.xml Pictures/1.png manifest.rdf META-INF/manifest.xml其中content.xml是正文和大部分内容所在styles.xml保存样式定义Pictures/是嵌入的图片。我们可以用 Python 的zipfile读取content.xmlimport zipfile from lxml import etree with zipfile.ZipFile(sample.odt) as z: with z.open(content.xml) as f: root etree.fromstring(f.read()) text_parts root.findall(.//{urn:oasis:names:tc:opendocument:xmlns:text}1.0/p)如果文档只有几段普通文字你会得到清晰的text:p列表。这给了很多新手一种错觉把text:p里的文本提取出来用reportlab一行行画到 PDF 不就行了4.2 解析 XML 自己画 PDF 的五个致命伤真正动手后会发现任何非平凡文档都会让这个方案崩盘样式继承ODT 的文本样式通过text:span text:style-nameT1引用样式表中定义的字体、字号、颜色、缩进而样式表又有段落样式、字符样式、表格样式、页面样式四层。要完整复现等于重写一个排版引擎。字体度量PDF 绘制文本必须知道每个字符的宽度才能计算自动换行和对齐。你需要读取系统字体计算每个字形宽度并处理中英文混排时的基线对齐。分页计算ODT 文档流是动态分页的段落前后的分页符、表格行跨页、页眉页脚跟随页面样式变化。纯规则引擎很难处理 keep-next 这类 Word 处理习惯的段落控制属性。图片位置正文中的浮动图片、锚定段落、环绕模式全都需要模拟 Writer 的排版逻辑。表格宽度ODT 表格列宽可以使用绝对单位、相对百分比、甚至自适应内容。真实世界里的表格几乎都有复杂跨行、跨列、合并单元格纯 XML 遍历处理会写到你怀疑人生。结论很明确除非文档是你自己程序生成的、结构极度受控的纯文本 ODT否则不要用纯 Python 库转 PDF。这不是 Python 不行而是 Office 排版引擎本身就是巨大的工程。4.3 什么场景下纯库方案反而合适当然纯库方案并非一无是处。如果你处理的文档是程序自动生成的比如导出订单、发票、通知函正文只有标题、段落、一个简单表格且你已经完全了解 ODT 的结构那么用odfpy读取内容再用reportlab生成 PDF响应速度会更快也不会依赖服务器上安装 LibreOffice。我的经验是凡是一次性转换用户上传的任意 ODT 文件都不要碰纯库方案凡是你自己模板生成的 ODT数量大、格式固定纯库也许是爱。但现实里为了一个纯文本模块去维护两套渲染逻辑后期成本很高。我最终还是在项目中统一回退到 LibreOffice。5. 方案三Windows 没有 LibreOffice 时的曲线救国——ODT 转 DOCX 再转 PDF5.1 用 win32com 调 Word 直接打开 ODT 的局限性有些公司只给开发机装了 Microsoft Office不允许再装 LibreOffice。此时很多同事会想着用pywin32调 Word 的 COM 接口处理转换import win32com.client import os def convert_odt_to_pdf_with_word(input_path, output_path): word win32com.client.Dispatch(Word.Application) word.Visible False doc word.Documents.Open(input_path) doc.SaveAs2(output_path, FileFormat17) # 17 表示 wdFormatPDF doc.Close() word.Quit()这个方案确实能跑通但我要说实话Word 打开 ODT 的兼容性并不好。普通的文本、图片问题不大可一旦遇到 LibreOffice 特有的样式属性、页面设置、表格边框Word 渲染结果经常和原文件有差异。更麻烦的是 COM 调用要求机器上有完整的 Office 许可、不允许无头运行服务器上还会弹出奇怪的交互对话框。若非不得已不推荐。5.2 桥接五次不如原生一次为什么还是建议装 LibreOffice也有人推出一个桥接思路先用 LibreOffice——等等既然你都装了 LibreOffice为什么不直接转 PDF所以这个思路本质上只在一种情况下成立你手头既有 Word 又要保留可编辑的 DOCX需要先转换格式再交给 Word 做二次处理。但中间转两次格式必然会损失部分细节浪费的时间和空间也不值得。我在实际项目里的判断标准很简单如果服务器允许安装开源软件优先装 LibreOffice一步到位只有当你确定客户环境绝对不允许安装任何额外办公套件、且所有 ODT 文档都是简单格式时才考虑win32com调 Word。前者稳定可控后者看人品。5.3 其他跨平台替代工具ONLYOFFICE 和 Calligra 值得一提除了 LibreOfficeONLYOFFICE Desktop Editors 也支持 ODT 转 PDF而且它的排版引擎在网页协作场景表现不错。Calligra 是 KDE 社区的办公套件但成熟度相对一般。服务器自动化领域LibreOffice 依然是命令接口最完整、文档最多、坑最少的选择。OpenOffice 大家也常用但它的无头模式 API 更老新版本不开源支持下更新缓慢。除非公司强依赖 OpenOffice 的 UNO 扩展否则没必要绕远。6. 实战包含图片、表格、页眉页脚的复杂 ODT 转换与异常排查6.1 准备一个带料的测试文档并跑通为了验证方案的可靠性我特意用 LibreOffice Writer 创建了一个测试文档test_complex.odt里面包含一个三行两列的表格其中一格跨两列、一张嵌入图片、打印样式的页眉和页脚、标题下面的一个无序列表。然后在虚拟环境里执行python -c from converter import odt_to_pdf; print(odt_to_pdf(test_complex.odt, out/))输出/abs/path/out/test_complex.pdfPDF 打开后页眉页脚完整图片居中表格边框无溢出。这个结果说明--convert-to pdf默认导出已经足够处理大部分办公文档。真正复杂的部分往往不是转换本身而是异常处理。6.2 生产级函数超时、临时 Profile、错误号一个都不能少我整理了一份在多个项目里用过的生产级函数。它比上面的基础版多了四件事独立临时配置目录、超时后清理进程、返回码非零时打印完整 stderr、生成文件后校验大小非零。import os import shutil import subprocess import tempfile def odt_to_pdf_pro(input_path: str, output_dir: str, timeout: int 180) - str: input_path os.path.abspath(input_path) output_dir os.path.abspath(output_dir) os.makedirs(output_dir, exist_okTrue) profile_dir tempfile.mkdtemp(prefixlo_profile_) try: cmd [ soffice, f-env:UserInstallationfile://{profile_dir}, --headless, --norestore, --convert-to, pdf, --outdir, output_dir, input_path ] proc subprocess.run( cmd, capture_outputTrue, textTrue, timeouttimeout ) except subprocess.TimeoutExpired as e: # 超时后尝试清理残余进程 subprocess.run([pkill, -f, profile_dir], capture_outputTrue) raise RuntimeError(f转换超时: {input_path}) from e except FileNotFoundError as e: raise RuntimeError(找不到 soffice是否安装 LibreOffice 并配置 PATH?) from e finally: shutil.rmtree(profile_dir, ignore_errorsTrue) if proc.returncode ! 0: raise RuntimeError( fLibreOffice 返回码 {proc.returncode}: {proc.stderr} ) base os.path.splitext(os.path.basename(input_path))[0] pdf_path os.path.join(output_dir, base .pdf) if not os.path.isfile(pdf_path) or os.path.getsize(pdf_path) 0: raise RuntimeError(fPDF 未生成或大小为 0: {pdf_path}) return pdf_path几个小细节--norestore防止 LibreOffice 恢复上次未保存的文档弹窗pkill -f profile_dir用于杀残留进程最后检查文件大小非零很多静默失败能在这里被拦截。6.3 字体缺失导致整页方块的真实案例有次我在一台干净的 Debian 服务器上跑批量转换所有中文文本转出来都是□□□。逐个排查后发现服务器上没有任何中文字体。fc-list输出里只有几个 DejaVu 字体而 DejaVu 不含中文字形。装好fonts-noto-cjk后重新转换中文恢复正常。这类问题最坑的地方在于LibreOffice 不会因为缺字体而报错它只是找一个能用的字体替代替代不了就画方框。所以转换后的 PDF 必须做可视化抽查或至少抽样验证文本内容长度。如果必须自动化检查可以用pdftotext抽文本pdftotext out.pdf - | wc -l对比源文档的文本量能发现约八成缺字问题。7. 批量与并发转换把脚本从能用升级到抗打7.1 批量转换的基本框架glob 失败清单第一步先把单个转换扩展成扫目录全部转换同时保留失败清单不要因为一个坏文件中断整个任务from pathlib import Path def batch_convert(input_dir: str, output_dir: str): input_dir Path(input_dir) output_dir Path(output_dir) errors [] for odt_path in input_dir.glob(*.odt): try: pdf_path odt_to_pdf_pro(str(odt_path), str(output_dir)) print(fOK: {odt_path.name} - {Path(pdf_path).name}) except Exception as e: errors.append((odt_path.name, str(e))) print(fFAIL: {odt_path.name}: {e}) print(f完成成功 {len(list(input_dir.glob(*.odt))) - len(errors)}失败 {len(errors)} 个) return errors如果要遍历子目录把glob(*.odt)改成rglob(*.odt)。7.2 concurrent.futures 做并发时必踩的连环坑很多人写完串行版本嫌慢就上了ThreadPoolExecutor。结果发现要么崩溃要么报 profile 锁错误。核心原因前面提过LibreOffice 的默认用户配置目录只有一个多个进程同时写会冲突。解决方案有三个层级方案 A完全不并发串行跑。文件不多时最简单不会出问题。 方案 B限制并发数为 1本质还是串行但用线程池统一管理超时与异常。 方案 C真正的并发每个子进程都指定独立的-env:UserInstallation临时目录让每个进程拥有独立 profile。经过测试并发数控制在 2~4 时CPU 和内存收益最现实数量再多容易内存爆炸。代码示例from concurrent.futures import ThreadPoolExecutor, as_completed def convert_one(item): return odt_to_pdf_pro(item, output_dir) with ThreadPoolExecutor(max_workers4) as executor: futures {executor.submit(convert_one, str(p)): p for p in input_dir.glob(*.odt)} for future in as_completed(futures): try: pdf future.result() print(成功:, Path(pdf).name) except Exception as e: print(失败:, futures[future].name, e)注意真正的瓶颈通常在内存。每个 soffice 进程大约占用 200~400MB 内存4 并发就是 1.6GB服务器只有 2GB 内存时建议 max_workers2。7.3 在 Web 服务里避免同步阻塞和后端超时如果只是写脚本subprocess.run阻塞没什么问题。但放到 FastAPI 或 Flask 接口里直接同步调用会让请求线程干等一个 180 秒的转换。正确姿势是使用线程池把转换任务放到后台执行并限制并发数量。最简单的示例from fastapi import FastAPI, File, UploadFile from concurrent.futures import ThreadPoolExecutor app FastAPI() pool ThreadPoolExecutor(max_workers2) app.post(/convert/odt-to-pdf) async def convert_odt(file: UploadFile): content await file.read() # 保存临时文件调用 pool.submit(odt_to_pdf_pro, ...) # 返回一个 task_id前端轮询任务状态真正的生产项目里我一般不会直接在 Web 进程内跑 LibreOffice而是把任务丢给 Celery/Redis 队列由独立 worker 进程去转换避免 Web 服务器被打垮。如果业务量不大用线程池加信号量也够用关键是千万不要用os.system同步调用否则用户请求线程全卡在等待上。7.4 日志、重试和统计的工程化套路工程化脚本还要加日志。核心信息包括输入文件路径、输出路径、耗时、返回码、stderr 前 500 字符。出现失败时自动重试一次等待 2 秒如果仍然失败记入错误清单。import logging, time logger logging.getLogger(odt_converter) def convert_with_retry(path, output_dir, retries2): for i in range(retries): try: start time.time() pdf odt_to_pdf_pro(path, output_dir) logger.info(转成功 %s - %s 耗时 %.2fs, path, pdf, time.time()-start) return pdf except Exception as e: logger.warning(第 %d 次尝试失败 %s: %s, i1, path, e) time.sleep(2) raise RuntimeError(f重试后仍失败: {path})日志文件名建议按天滚动不然生产环境日志会巨大。8. 错误速查表与我的最后几点私货8.1 高频错误速查对照表把几年里遇到的典型错误整理成了表格方便直接查报错现象可能原因解决方案soffice: command not foundLibreOffice 未安装或 PATH 未配置正确安装并配置 PATH或调用绝对路径Error: source file could not be loaded输入文件损坏、路径编码异常、实际不是 ODT检查文件能正常用 Writer 打开统一文件名为 UTF-8return code 77缺系统基础库或字体安装字体与依赖如libreoffice-gtk3或fonts-noto-cjk报 profile lock 错误多进程并发共用用户配置目录每个调用使用独立-env:UserInstallation转换完成但 PDF 是空白的ODT 文档内容异常、或者过滤参数错误手动打开确认文档正常取消自定义过滤器参数中文全部是方框服务器缺少中文字体apt install fonts-noto-cjk或部署中文字体文件Python 报编码 UnicodeDecodeError文件或 stdout 编码不兼容 Windows使用textFalse然后按 utf-8 容错解码转换耗时很长且 CPU 100%文档含大量高清图片或复杂表格考虑限制图片导出分辨率doc 中图片压缩后再转这个表不是万能药但覆盖了 90% 的日常报错。8.2 我最后想说的几句实在话搞了几年文档转换最有价值的教训就是不要一开始就追求并发和花哨的库先把最小的串行链路跑通再不断加防护。LibreOffice 是这里面最普通却最可靠的工具它没有 Python 生态那种亮眼包装但一个soffice命令稳定得可怕。如果你在处理 ODT 转换时遇到格式错乱先别急着改代码打开 LibreOffice 人工转一次。如果人工也一样乱那问题就在源文档本身不在脚本。如果人工正常而脚本生成的 PDF 乱再检查字体和profile 锁也不迟。另外转换速度真的和机器性能关系极大。小文档 1 秒内出结果带几十张高清图的文档可能要 10 秒。批量任务上不要盲目设 120 秒超时可以按文档大小乘以 5 再加 20 秒来动态计算。我的项目中一般做成可配置参数默认 180 秒。最后分享一个小技巧如果你需要转换的 ODT 是从网上银行、政府系统导出的里面常带数字签名和只读保护。LibreOffice 默认模式会提示输入密码或跳过受保护区域。这时在命令里加一个--writer参数指定文档类型可以绕开部分保护弹窗。具体写法是soffice --headless --writer --convert-to pdf --outdir ...。遇到导入时弹密码框的文档不妨试试这个组合能免去不少人工介入。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进