OCRmyPDF 批量处理实战:GNU Parallel 批处理、watcher.py 热文件夹监控与 Docker 服务部署
本文围绕 OCRmyPDF 的批量处理方案展开,覆盖用 GNU Parallel 和 find 对多文件并行执行 OCR、用 Docker 流式处理整棵目录树,以及如何把 watcher.py 热文件夹服务跑在本机或 Docker 容器中实现"文件落地即自动 OCR"。读完本文,你可以按自己的硬件与部署形态(裸机、NAS、Docker、macOS 自动化)选择并落地的批量 OCR 流水线,并理解 watcher 的环境变量配置背后的源码实现。
1. 用 GNU Parallel 执行批量作业
对多个文件一次性应用 OCRmyPDF,推荐借助 GNU Parallel。关键点是:parallel 和 ocrmypdf 各自都会尝试用满所有可用处理器,为避免进程把系统压垮,建议用 -j 2 限制 parallel 同时只跑两个作业。
以下命令对当前目录中所有 *.pdf 文件执行 ocrmypdf,结果写入事先创建好的 output/ 目录,且不会搜索子目录:
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
其中 --tag 让 parallel 在打印任何消息时都带上文件名前缀,这样日志中的任何错误都能追溯到产生它的那个文件。另外需要说明:OCRmyPDF 在解析 PDF、收集页面信息之前会自动尝试修复(repair)损坏的 PDF,因此一批文件里混入少量格式不规范的扫描件也不会让整个批量作业失败。
2. 目录树的批量处理
2.1 用 find 原地处理
遍历目录树并对所有文件"原地"执行 OCR(输入与输出指向同一文件),每处理完一个文件打印一次文件名:
find . -name '*.pdf' -printf '%p\n' -exec ocrmypdf '{}' '{}' \;
注意这个写法同一时间只运行一个 ocrmypdf 进程。若要并行化,可以改用 find 生成文件列表、parallel 负责并行执行,仍然是原地更新:
find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}'
2.2 Windows 批处理
在 Windows 的 batch 文件中使用递归 for 循环即可:
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
2.3 Docker 容器:标准输入/输出流式处理
如果通过 Docker 容器处理目录树,则必须经由标准输入/输出流式传输文件,因为容器内部无法直接看到宿主机路径:
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
pdfout=$(mktemp)
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
done
该模式与仓库中 misc/synology.py 的做法一致:Synology NAS 脚本正是通过 docker run --rm -i jbarlow83/ocrmypdf --deskew - - 把本地 PDF 从 stdin 喂给容器、从 stdout 取回 OCR 结果,再写回宿主机。
3. 仓库内置的批处理示例脚本
3.1 misc/batch.py:递归目录批处理模板
misc/batch.py 是一个用户贡献的批处理示例,演示了如何以库的方式调用 ocrmypdf 对整棵目录树做递归 OCR 并记录日志。它的核心逻辑:
- 用
start_dir.glob("**/*.pdf")递归收集所有 PDF(start_dir由命令行第一个参数给出,默认为当前目录); - 先调用
ocrmypdf.pdfa.file_claims_pdfa(filename)检查文档是否已含文本层,若已符合 PDF/A(说明已有文本)则跳过; - 需要时先把原件复制归档到
archive_dir(脚本中默认/pdfbak,可按需修改); - 再执行
ocrmypdf.ocr(filename, filename, deskew=True)原地 OCR; - 通过捕获
EncryptedPdfError(加密文档)、PriorOcrFoundError(已含文本)、DigitalSignatureError(含数字签名)、TaggedPDFError(已是 Tagged PDF)等异常分别记录"跳过原因",而不是让整个批次崩溃。
这个脚本的注释明确提示"应当按自己的需求修改脚本",可作为自建批量工具的最短起步模板。
3.2 misc/synology.py:针对 Synology DiskStation 的适配脚本
Synology DiskStation(NAS)在安装 Docker 套件后同样可以运行 OCRmyPDF 镜像。misc/synology.py 是为这类设备编写的示例脚本,要点:
- 用
os.walk遍历起始目录,对每个.pdf文件生成带时间戳的输出名(%Y-%m-%d-%H%M_OCR_前缀); - 以 root 身份经
cron调度执行,通过subprocess.run启动docker run --rm -i jbarlow83/ocrmypdf --deskew - -,把文件内容经 stdin 输入、stdout 输出; - 处理完成后对 OCR 结果和原件分别
chmod 0o664(适配 NAS 的共享目录权限约定),再把 OCR 产物移入归档目录、原件移入no_ocr子目录。
文档同时提醒:该脚本编写时仅在 x86 架构的 Synology 产品上验证过,在 ARM 产品上能否运行未知,且针对 Synology 相对有限的 CPU 与内存可能还需要进一步调参。
对于有数千个文件量级的超大批量任务,原作者在文档中建议直接联系作者,相关咨询工作也是该开源项目的重要资金来源。
4. 热文件夹(Hot/Watched Folder)
4.1 watcher.py:随源码分发的目录监视器
OCRmyPDF 自带一个文件夹监视器 misc/watcher.py,它包含在源码分发包中,但不是主程序的一部分。它可以本机原生运行,也可以放在 Docker 容器里运行,原生实例通常性能更好,且 watcher.py 支持所有平台。使用者需要按自己的需求对脚本做定制。
安装依赖。 监视器依赖三个额外包,在 pyproject.toml 中定义为可选依赖 watcher:
watcher = ["watchdog>=1.0.2", "cyclopts>=3", "python-dotenv"]
因此安装方式为:
# 使用 uv(推荐)
uv sync --extra watcher
# 或使用 pip
pip3 install ocrmypdf[watcher]
运行示例。 以下配置监视 /mnt/input-pdfs,结果按年月写入 /mnt/output-pdfs:
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
python3 watcher.py
watcher.py 环境变量说明(文档官方表格):
| 环境变量 | 说明 |
|---|---|
OCR_INPUT_DIRECTORY |
设置要监视的输入目录(递归监视) |
OCR_OUTPUT_DIRECTORY |
设置输出目录(不应位于输入目录下) |
OCR_ARCHIVE_DIRECTORY |
设置处理完原件的归档目录(不应位于输入目录下,且要求设置了 OCR_ON_SUCCESS_ARCHIVE) |
OCR_ON_SUCCESS_DELETE |
当退出码为 0(OK)时,把已处理的原件移动到 OCR_ARCHIVE_DIRECTORY;注意该选项优先级高于归档选项,即两者都设置时输入文件会被删除 |
OCR_OUTPUT_DIRECTORY_YEAR_MONTH |
设置后输出文件按 {output}/{year}/{month}/{filename} 存放 |
OCR_DESKEW |
对倾斜的输入 PDF 应用去歪斜(deskew) |
OCR_JSON_SETTINGS |
一个 JSON 字符串,指定传给 ocrmypdf.ocr 的其他参数,如 'OCR_JSON_SETTINGS={"rotate_pages": true, "optimize": "3"}' |
OCR_POLL_NEW_FILE_SECONDS |
轮询间隔(秒) |
OCR_LOGLEVEL |
日志级别 |
源码层面的补充。 从 misc/watcher.py 的参数定义(main() 函数,L158-L248)可以看到,除了文档表格列出的变量外,源码还暴露了更多可通过环境变量(或同名命令行参数,由 cyclopts 解析)控制的配置:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
OCR_INPUT_DIRECTORY |
/input |
输入目录 |
OCR_OUTPUT_DIRECTORY |
/output |
输出目录 |
OCR_ARCHIVE_DIRECTORY |
/processed |
归档目录 |
OCR_USE_POLLING |
False |
使用轮询(PollingObserver)替代文件系统事件,适合不支持 inotify 类事件的挂载 |
OCR_RETRIES_LOADING_FILE |
5 |
文件就绪前的最大重试次数 |
OCR_PATTERNS |
*.pdf,*.PDF |
要监视的文件模式(逗号分隔) |
OCR_LOGLEVEL |
INFO |
取值为 DEBUG/INFO/WARNING/ERROR/CRITICAL |
几个值得注意的实现细节:
- 文件就绪等待。
wait_for_file_ready()(misc/watcher.py L63-L88)会循环尝试用pikepdf.Pdf.open打开新文件,失败则按OCR_POLL_NEW_FILE_SECONDS间隔重试。注释说明这是为了应对 Docker 场景:容器有时会在文件真正落盘完成之前发布 watchdog 事件,直接读取会导致解析失败。 - OCR 调用与退出码。
execute_ocrmypdf()(misc/watcher.py L91-L133)通过ocrmypdf.ocr(ocrmypdf.OcrOptions(input_file=..., output_file=..., **ocrmypdf_kwargs))以库方式执行 OCR(对应 src/ocrmypdf/api.py 中的 API 入口,OCR_JSON_SETTINGS的内容会被展开为OcrOptions关键字参数)。仅当返回退出码为 0 时才会执行删除或归档动作;失败时原件保留在输入目录中,便于排查。 - 年月归档路径。
get_output_path()(misc/watcher.py L48-L60)在开启OCR_OUTPUT_DIRECTORY_YEAR_MONTH时创建{year}/{两位月}两级子目录并把输出固定为.pdf后缀。 - 安全约束。 若
OCR_JSON_SETTINGS中出现input_file或output_file键,脚本会直接报错退出——这两个键必须由监视器自身控制,不允许被 JSON 设置覆盖。
仓库自带的 tests/test_watcher.py 以子进程方式启动 misc/watcher.py,向输入目录拷贝测试 PDF 后断言输出文件出现在预期位置(含年月子目录两种情形),可作为行为验证的参考。
一个典型的落地形态是把网络扫描仪或扫描用电脑配置为把文件丢进被监视的文件夹,后续 OCR 完全自动化。
4.2 用 Docker 运行 watcher 服务
watcher 服务已包含在 OCRmyPDF Docker 镜像中,启动命令如下:
docker run \
--volume <path to files to convert>:/input \
--volume <path to store results>:/output \
--volume <path to store processed originals>:/processed \
--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
--env OCR_ON_SUCCESS_ARCHIVE=1 \
--env OCR_DESKEW=1 \
--env PYTHONUNBUFFERED=1 \
--interactive --tty --entrypoint python3 \
jbarlow83/ocrmypdf \
/app/watcher.py
watcher Docker 参数说明:
| 参数 | 说明 |
|---|---|
--volume <待转换文件路径>:/input |
放入此位置的文件将被 OCR |
--volume <结果存储路径>:/output |
OCR 结果文件的存放位置 |
--volume <原件归档路径>:/processed |
已处理原件的归档位置 |
--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 |
输出文件按 {output}/{year}/{month}/{filename} 存放 |
--env OCR_ON_SUCCESS_ARCHIVE=1 |
把已处理原件移入归档目录 |
--env OCR_DESKEW=1 |
对倾斜输入 PDF 应用去歪斜 |
--env PYTHONUNBUFFERED=1 |
强制 STDOUT 无缓冲,便于在 docker logs 中实时看到消息 |
--env OCR_LOGLEVEL='DEBUG' |
日志级别 |
--env OCR_JSON_SETTINGS={"language":"deu+eng", "rotate_pages": true} |
一个 JSON 字符串,指定传给 ocrmypdf.ocr 的其他参数 |
权限注意事项: 镜像默认以非 root 的 app 用户(uid 1000)运行,因此除非追加 --user 参数,容器可能没有权限写入 /output 与 /processed 卷。具体取值取决于你使用 rootful Docker、rootless Docker 还是 Podman,可参阅 docs/docker.md 中 "Bind-mounted volumes" 一节的详细说明。
轮询机制与可用性保障。 该服务依赖轮询来检查文件系统变化,因此未必适合所有环境(如慢速网络共享文件系统——此时也可在原生 watcher 中用 OCR_USE_POLLING 明确切换为 PollingObserver)。可以使用 Docker Compose 这类配置管理工具保证服务常驻,仓库中的示例 misc/docker-compose.example.yml 展示了典型配置:
services:
ocrmypdf:
restart: always
container_name: ocrmypdf
image: jbarlow83/ocrmypdf-alpine
volumes:
- "/media/scan:/input"
- "/mnt/scan:/output"
environment:
- OCR_OUTPUT_DIRECTORY_YEAR_MONTH=0
user: "<SET TO YOUR USER ID>:<SET TO YOUR GROUP ID>"
entrypoint: python3
command: /app/watcher.py
其中 user 一项的注释解释了取值逻辑:rootful Docker 填宿主机 uid:gid;rootless Docker 填 0:0(容器 root 映射到宿主机用户);Podman 则填宿主机 uid:gid 并附加 userns_mode: "keep-id"。
4.3 注意事项(Caveats)
watchmedo在网络文件系统上可能无法正常工作,具体取决于文件客户端与服务端的能力;- 这个简单方案不过滤文件系统事件类型,因此文件复制、删除、移动以及目录操作都会被送入 ocrmypdf,多种情况下会产生错误。如果对监视文件夹做任何"复制文件进去"以外的操作,应先禁用该文件夹的监视;
- 如果源目录与目标目录相同,watchmedo 可能造成死循环(输出文件再次触发事件);
- 在 BSD、FreeBSD 和较旧版本的 macOS 上,可能需要用
ulimit -n 1024提高文件描述符上限,才能监视多达 1024 个文件的文件夹。
4.4 替代方案
- 在 Linux 上,可以配置 systemd 用户服务,对一批文件自动执行 OCR;
- Watchman 是
watchmedo的一个功能更强的替代品。
5. macOS Automator 工作流
在 macOS 上可以用 Automator 应用创建 Workflow 或 Quick Action,在流程中使用 Run Shell Script 动作。需要注意:在 Automator 上下文中,PATH 可能与终端中的不同,可能必须显式设置 PATH 以包含 ocrmypdf。以下截图可作为起步参考(可自定义传给 ocrmypdf 的命令):
6. 小结:按场景选择批量方案
| 场景 | 推荐方案 |
|---|---|
| 一次性处理当前目录/目录树 | parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf 或 find 变体 |
| Windows 环境 | for /r %%f in (*.pdf) do ocrmypdf %%f %%f |
| 只装了 Docker、无原生依赖 | stdin/stdout 流式 docker run(见 2.3 节) |
| 长期"文件落地即处理" | 原生 watcher.py(性能更好)或 Docker 版 watcher + Compose 常驻 |
| Synology NAS | misc/synology.py + cron |
| macOS 桌面 | Automator Quick Action |
所有脚本类方案都强调一点:仓库提供的示例脚本(batch.py、synology.py、watcher.py)均是可定制的模板,上线前应按自身的目录结构、权限和硬件资源做相应修改。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
