Windows PDF 批量 OCR 环境配置备忘
目标:遍历目录,为每个 PDF 生成
原名_ocr.pdf,OCR 文本层位于原始图像之下,视觉与原版一致。 适用:Windows 10/11,扫描件、页面横竖混排、横排/竖排中文。 记录日期:2026-10-24
0. 版本记录(换机后按此对齐)
| 组件 | 版本 / 路径 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11 64-bit | |
| Python | 3.14,D:\Program Files\Python314 | 从 python.org 安装,非 Microsoft Store 版 |
| pip 包 | ocrmypdf(v13+) | v13 起已移除 --remove-background |
| Tesseract | 5.x,C:\Program Files\Tesseract-OCR | UB-Mannheim 构建 |
| Tesseract 语言 | chi_sim、eng、osd | osd 是 rotate_pages 的依赖 |
| Ghostscript | 10.x,C:\Program Files\gs\gs10.xx.x | 64-bit |
| 脚本 | ocr.py | 见第 6 节 |
⚠️ 不要用 Microsoft Store 版 Python,与 OCRmyPDF 存在兼容性问题。 ⚠️ 不建议用 Chocolatey 安装 Tesseract/Ghostscript——实测在非管理员会话下反复失败,且残留锁文件难清理。手动安装更稳。
1. 安装 Python
- 从 https://www.python.org/downloads/ 下载 64-bit 安装包。
- 安装时勾选 “Add python.exe to PATH”。
- 安装到非系统盘也可(如
D:\Program Files\Python314)。 - 验证:
python --version
pip --version
2. 安装 Tesseract(含中文与 OSD)
2.1 下载与安装
- 打开 UB-Mannheim 官方页面:https://github.com/UB-Mannheim/tesseract/wiki
- 下载 64 位安装程序(文件名类似
tesseract-ocr-w64-setup-5.5.0.exe)。 - 运行安装程序,关键选项:
- 安装路径:保持默认
C:\Program Files\Tesseract-OCR。 - 组件选择页:展开
Additional language data (download),勾选:Chinese (Simplified)→chi_simEnglish→eng(默认已选)Orientation and script detection→osd(rotate_pages必需)
- Add to PATH:保持默认勾选。
- 安装路径:保持默认
2.2 验证
关闭并重开 PowerShell:
tesseract --version
tesseract --list-langs
--list-langs 输出应包含:
chi_sim
eng
osd
2.3 缺失语言包的手动补救
如果 chi_sim 或 osd 不在列表里,不要重装,直接下载对应文件放入 C:\Program Files\Tesseract-OCR\tessdata\:
# 在管理员 PowerShell 中
Invoke-WebRequest -Uri "https://github.com/tesseract-ocr/tessdata/raw/main/chi_sim.traineddata" -OutFile "C:\Program Files\Tesseract-OCR\tessdata\chi_sim.traineddata"
Invoke-WebRequest -Uri "https://github.com/tesseract-ocr/tessdata/raw/main/osd.traineddata" -OutFile "C:\Program Files\Tesseract-OCR\tessdata\osd.traineddata"
精度要求高时,可换成
tessdata_best版chi_sim.traineddata,中文识别率通常更好。
3. 安装 Ghostscript
- 打开官方下载页:https://ghostscript.com/releases/gsdnld.html
- 下载 Windows 64-bit 安装包(
gs10.xx.x-win64.exe)。 - 安装到默认路径
C:\Program Files\gs\gs10.xx.x,安装程序通常会自动把bin加入 PATH。 - 关闭并重开 PowerShell,验证:
gswin64c --version
若提示找不到命令,手动将 C:\Program Files\gs\gs10.xx.x\bin 加入系统 PATH。
4. 安装 OCRmyPDF
pip install ocrmypdf
验证:
ocrmypdf --version
能输出版本号,说明 Tesseract、Ghostscript、Python 三者已正确串联。
5. 环境自检清单
在新机器上配置完后,逐项运行,全部通过再开始批量处理:
python --version # Python 3.10+
pip show ocrmypdf # 显示版本
tesseract --version # Tesseract 5.x
tesseract --list-langs # 包含 chi_sim / eng / osd
gswin64c --version # Ghostscript 10.x
ocrmypdf --version # OCRmyPDF v13+
6. 批量 OCR 脚本 ocr.py
保存到 C:\Users\<用户名>\Desktop\ocr.py 或任意工作目录。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量 OCR PDF 脚本(Windows 友好)
核心行为:
- 遍历目录,为每个 PDF 在同目录生成 {原名}_ocr.pdf
- OCR 文本层位于原始页面图像之下(OCRmyPDF 默认),视觉与原版一致
- 自动纠正页面横竖混排(rotate_pages)和轻微倾斜(deskew)
- 支持横排/竖排中文、旧文本层重做、用户词典等精度调优
- 预设参数组合可被显式命令行参数覆盖
"""
from __future__ import annotations
import argparse
import logging
import sys
from pathlib import Path
import ocrmypdf
from ocrmypdf import exceptions as ocr_exc
# ---------------------------------------------------------------------------
# 预设参数
# ---------------------------------------------------------------------------
PRESETS = {
# 最快、最保守:只做旋转与倾斜矫正
"fast": {
"language": "chi_sim+eng",
"deskew": True, "rotate_pages": True, "rotate_threshold": 2.0,
"oversample": None,
"thresholding": "auto", "psm": None, "oem": None,
"timeout": 180, "downsample_large": True,
},
# 通用默认:加 oversample 兜底低 DPI
"standard": {
"language": "chi_sim+eng",
"deskew": True, "rotate_pages": True, "rotate_threshold": 2.0,
"oversample": 300,
"thresholding": "auto", "psm": None, "oem": None,
"timeout": 180, "downsample_large": True,
},
# 扫描件:重采样兜底 + 略保守的旋转
"scan": {
"language": "chi_sim+eng",
"deskew": True, "rotate_pages": True, "rotate_threshold": 2.0,
"oversample": 300,
"thresholding": "auto", "psm": None, "oem": None,
"timeout": 300, "downsample_large": True,
},
# 扫描件 + 页面横竖混排:积极旋转
# 注意:这里的“横竖混排”指纸张朝向不同,文字仍是正常横排;
# 不是古籍文字竖排,所以 language 不用 chi_sim_vert,PSM 也不用 5。
"scan_mixed": {
"language": "chi_sim+eng",
"deskew": True, "rotate_pages": True, "rotate_threshold": 1.2,
"oversample": 300,
"thresholding": "auto", "psm": None, "oem": None,
"timeout": 300, "downsample_large": True,
},
# 老旧/脏污文档:局部自适应二值化 + 复杂版面 PSM 6
"old": {
"language": "chi_sim+eng",
"deskew": True, "rotate_pages": True, "rotate_threshold": 2.0,
"oversample": 300,
"thresholding": "sauvola", "psm": 6, "oem": None,
"timeout": 300, "downsample_large": True,
},
# 横竖文字混排(不是页面方向):多语言组合,慢,先小范围验证
"mixed": {
"language": "chi_sim+chi_sim_vert+eng",
"deskew": True, "rotate_pages": True, "rotate_threshold": 2.0,
"oversample": 300,
"thresholding": "auto", "psm": None, "oem": None,
"timeout": 300, "downsample_large": True,
},
# 竖排古籍:竖排语言 + 竖排文本块 PSM 5
"vertical": {
"language": "chi_sim_vert",
"deskew": True, "rotate_pages": True, "rotate_threshold": 2.0,
"oversample": 300,
"thresholding": "auto", "psm": 5, "oem": None,
"timeout": 300, "downsample_large": True,
},
}
def build_parser() -> argparse.ArgumentParser:
epilog = """\
使用示例:
扫描件 + 页面横竖混排 python ocr.py "D:\\扫描文档" --preset scan_mixed -r
干净扫描件 python ocr.py "D:\\扫描文档" --preset scan -r
通用横排中文 python ocr.py "D:\\文档" -r
老旧/脏污文档 python ocr.py "D:\\旧档案" --preset old -r
竖排古籍(文字竖排) python ocr.py "D:\\古籍" --preset vertical -r
重做已有旧文本层 python ocr.py "D:\\文档" --mode redo -r
带专有名词词典 python ocr.py "D:\\医学" --user-words terms.txt -r
先看会处理哪些文件 python ocr.py "D:\\扫描文档" --dry-run -r
"""
p = argparse.ArgumentParser(
description="批量 OCR PDF,生成 _ocr 后缀文件;OCR 文本层位于原图之下。",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=epilog,
)
p.add_argument("input_dir", type=Path, help="要处理的目录")
p.add_argument("-r", "--recursive", action="store_true", help="递归处理子目录")
p.add_argument("--preset", choices=tuple(PRESETS), default="standard",
help="参数预设(默认 standard;扫描件横竖混排用 scan_mixed)")
p.add_argument("--dry-run", action="store_true", help="只列出待处理文件,不实际执行")
p.add_argument("--mode", choices=("skip", "redo", "force"), default=None,
help="文本层处理:skip=已有文本层跳过(默认);"
"redo=删除旧文本层后重做;force=强制栅格化后重做")
p.add_argument("-l", "--language", default=None,
help="Tesseract 语言,如 chi_sim+eng / chi_sim_vert / "
"chi_sim+chi_sim_vert+eng")
p.add_argument("--psm", type=int, default=None,
help="Tesseract 页面分割模式 0-13(文字竖排用 5)")
p.add_argument("--oem", type=int, default=None, choices=(0, 1, 2, 3),
help="Tesseract 引擎模式 0-3(默认 LSTM)")
p.add_argument("--timeout", type=float, default=None,
help="单页 OCR 超时秒数(复杂页面可调大)")
p.add_argument("--deskew", dest="deskew", action="store_true", default=None,
help="启用倾斜矫正")
p.add_argument("--no-deskew", dest="deskew", action="store_false",
help="禁用倾斜矫正")
p.add_argument("--rotate", dest="rotate_pages", action="store_true", default=None,
help="启用自动旋转(纠正页面横竖混排)")
p.add_argument("--no-rotate", dest="rotate_pages", action="store_false",
help="禁用自动旋转")
p.add_argument("--rotate-threshold", type=float, default=None,
help="旋转判定阈值,越低越积极(默认 2.0;扫描件混排可试 1.0~1.5)")
p.add_argument("--oversample", type=int, default=None,
help="重采样到指定 DPI(如 300),对低 DPI 源有帮助")
p.add_argument("--thresholding",
choices=("auto", "otsu", "adaptive-otsu", "sauvola"),
default=None, help="二值化方法;背景脏/光照不均试用 sauvola")
p.add_argument("--no-downsample-large-images", dest="downsample_large",
action="store_false", default=None,
help="关闭大图降采样,保留小字细节(更耗内存/时间)")
p.add_argument("--downsample-large-images", dest="downsample_large",
action="store_true")
p.add_argument("--user-words", type=Path, default=None,
help="Tesseract 用户词典文件(每行一个词),适合专有名词多的文档")
p.add_argument("--jobs", type=int, default=None,
help="并行进程数(默认用 OCRmyPDF 自动值)")
p.add_argument("--overwrite", action="store_true",
help="覆盖已存在的 _ocr 输出")
p.add_argument("--log", type=Path, default=Path("batch_ocr.log"),
help="日志文件(相对输入目录或绝对路径)")
return p
def merge_preset(args: argparse.Namespace) -> None:
"""把 None 表示的'未指定'项,用 preset 的值填上。"""
preset = PRESETS[args.preset]
if args.mode is None:
args.mode = "skip"
for key in ("language", "psm", "oem", "timeout",
"deskew", "rotate_pages", "rotate_threshold", "oversample",
"thresholding", "downsample_large"):
if getattr(args, key) is None:
setattr(args, key, preset[key])
def validate(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
if args.psm is not None and not (0 <= args.psm <= 13):
parser.error(f"--psm 必须在 0-13,当前 {args.psm}")
if args.oversample is not None and args.oversample < 0:
parser.error("--oversample 必须是非负数")
if args.rotate_threshold is not None and args.rotate_threshold <= 0:
parser.error("--rotate-threshold 必须大于 0")
if args.user_words is not None and not args.user_words.is_file():
parser.error(f"--user-words 文件不存在: {args.user_words}")
if args.jobs is not None and args.jobs < 1:
parser.error("--jobs 必须 >= 1")
def setup_logger(log_path: Path) -> logging.Logger:
logger = logging.getLogger("batch_ocr")
logger.setLevel(logging.INFO)
logger.handlers.clear()
logger.propagate = False
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s",
"%Y-%m-%d %H:%M:%S")
try:
log_path.parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(log_path, encoding="utf-8")
fh.setFormatter(fmt)
logger.addHandler(fh)
except OSError as e:
print(f"警告:无法创建日志文件 {log_path}: {e}", file=sys.stderr)
ch = logging.StreamHandler(sys.stdout)
ch.setFormatter(fmt)
logger.addHandler(ch)
return logger
def collect_pdfs(root: Path, recursive: bool) -> list[Path]:
pattern = "**/*.pdf" if recursive else "*.pdf"
files = [
p for p in root.glob(pattern)
if p.is_file() and not p.stem.endswith("_ocr")
]
return sorted(files)
def process_one(input_path: Path, output_path: Path,
logger: logging.Logger, args: argparse.Namespace) -> str:
if output_path.exists():
if args.overwrite:
try:
output_path.unlink()
logger.info(f" 覆盖已存在输出: {output_path.name}")
except OSError as e:
logger.error(f" 无法删除 {output_path.name}: {e}")
return "error"
else:
logger.info(f" 输出已存在,跳过: {output_path.name}")
return "skipped"
logger.info(f"处理: {input_path}")
try:
ocrmypdf.ocr(
str(input_path),
str(output_path),
# 文本层模式(三选一)
skip_text=(args.mode == "skip"),
redo_ocr=(args.mode == "redo"),
force_ocr=(args.mode == "force"),
# 图像预处理
deskew=args.deskew,
rotate_pages=args.rotate_pages,
rotate_pages_threshold=args.rotate_threshold,
oversample=args.oversample,
# Tesseract 调优
language=args.language,
tesseract_thresholding=args.thresholding,
tesseract_pagesegmode=args.psm,
tesseract_oem=args.oem,
tesseract_timeout=args.timeout,
tesseract_downsample_large_images=args.downsample_large,
tesseract_user_words=str(args.user_words) if args.user_words else None,
# 运行参数
jobs=args.jobs,
optimize=1,
progress_bar=False,
)
logger.info(f" -> 完成: {output_path.name}")
return "success"
except ocr_exc.PriorOcrFoundError:
logger.warning(f" 已有文本层,跳过: {input_path.name}")
return "has_text"
except ocr_exc.EncryptedPdfError:
logger.error(f" 加密 PDF,无法处理: {input_path.name}")
return "encrypted"
except ocr_exc.MissingDependencyError as e:
logger.error(f" 缺少外部依赖: {e}")
return "error"
except ocr_exc.DigitalSignatureError:
logger.error(f" 含数字签名,OCR 会破坏签名,已跳过: {input_path.name}")
return "other"
except ocr_exc.TaggedPDFError:
logger.error(f" 含 accessibility tags,OCR 会破坏 tags,已跳过: {input_path.name}")
return "other"
except (ocr_exc.InputFileError, ocr_exc.OutputFileAccessError) as e:
logger.error(f" 文件访问错误: {input_path.name} — {e}")
return "error"
except ocr_exc.SubprocessOutputError as e:
logger.error(f" 子进程错误: {input_path.name} — {e}")
return "error"
except Exception as e:
logger.error(f" 失败: {input_path.name} — {e}", exc_info=True)
return "error"
def main() -> None:
parser = build_parser()
args = parser.parse_args()
merge_preset(args)
validate(args, parser)
root = args.input_dir.resolve()
if not root.is_dir():
parser.error(f"输入目录不存在: {root}")
log_path = args.log if args.log.is_absolute() else root / args.log
logger = setup_logger(log_path)
logger.info("=" * 60)
logger.info(f"输入目录: {root}")
logger.info(f"预设: {args.preset} | 模式: {args.mode} | "
f"递归: {args.recursive} | 覆盖: {args.overwrite}")
logger.info(
f"language={args.language} psm={args.psm} oem={args.oem} "
f"timeout={args.timeout} | "
f"deskew={args.deskew} rotate={args.rotate_pages}"
f"(th={args.rotate_threshold}) oversample={args.oversample} | "
f"thresholding={args.thresholding} downsample={args.downsample_large} | "
f"user_words={args.user_words} jobs={args.jobs}"
)
pdfs = collect_pdfs(root, args.recursive)
logger.info(f"共找到 {len(pdfs)} 个 PDF")
if args.dry_run:
for p in pdfs:
logger.info(f" [DRY] {p}")
logger.info("dry-run 结束,未执行任何 OCR。")
return
stats = {"success": 0, "skipped": 0, "has_text": 0,
"encrypted": 0, "other": 0, "error": 0}
for i, pdf in enumerate(pdfs, 1):
logger.info(f"[{i}/{len(pdfs)}]")
output_path = pdf.with_name(f"{pdf.stem}_ocr{pdf.suffix}")
try:
result = process_one(pdf, output_path, logger, args)
except KeyboardInterrupt:
logger.warning("用户中断,停止批量处理")
break
stats[result] += 1
logger.info("=" * 60)
logger.info(
f"完成:成功 {stats['success']} | 输出已存在跳过 {stats['skipped']} | "
f"已有文本层跳过 {stats['has_text']} | 加密 {stats['encrypted']} | "
f"其他跳过 {stats['other']} | 失败 {stats['error']}"
)
logger.info(f"日志: {log_path}")
if __name__ == "__main__":
main()
7. 使用方法
7.1 你的主场景:扫描件 + 页面横竖混排
python ocr.py "\\192.168.110.190\scan" --preset scan_mixed -r
该预设做四件事:rotate_pages 逐页纠正纸张朝向、rotate_threshold=1.2 让判定更积极、deskew 矫正轻微倾斜、oversample=300 兜底低 DPI。
重要:“页面横竖混排”(纸张朝向不同)≠“文字竖排”(古籍从右到左竖写)。前者用
rotate_pages+chi_sim+eng;后者才用chi_sim_vert+ PSM 5。混用会拖慢速度、降低横排精度。
7.2 先小范围验证
# 1. 挑 5-10 个有代表性的文件放到 D:\样本
# 2. 确认会处理哪些文件
python ocr.py "D:\样本" --preset scan_mixed --dry-run
# 3. 正式跑样本
python ocr.py "D:\样本" --preset scan_mixed
# 4. 打开输出的 _ocr.pdf 检查:
# - 横向页是否被转正
# - 纵向页是否被误转
# - 封面/纯图页是否被乱转
7.3 常用命令速查
| 场景 | 命令 |
|---|---|
| 扫描件 + 页面横竖混排 | python ocr.py "D:\扫描文档" --preset scan_mixed -r |
| 干净扫描件 | python ocr.py "D:\扫描" --preset scan -r |
| 通用横排中文 | python ocr.py "D:\文档" -r |
| 老旧/脏污文档 | python ocr.py "D:\旧档案" --preset old -r |
| 文字竖排古籍 | python ocr.py "D:\古籍" --preset vertical -r |
| 横竖文字混排 | python ocr.py "D:\混排" --preset mixed -r |
| 重做旧文本层 | python ocr.py "D:\文档" --mode redo -r |
| 带专有名词词典 | python ocr.py "D:\医学" --user-words terms.txt -r |
| 覆盖已有输出 | python ocr.py "D:\扫描" --preset scan_mixed --overwrite -r |
| 先看会处理哪些文件 | python ocr.py "D:\扫描" --dry-run -r |
| 旋转还不够积极 | ... --preset scan_mixed --rotate-threshold 1.0 -r |
| 不该转的被转了 | ... --preset scan_mixed --rotate-threshold 1.8 -r |
| 背景脏/光照不均 | ... --preset scan_mixed --thresholding sauvola -r |
7.4 预设对比
| 预设 | language | PSM | 旋转阈值 | 二值化 | 适用 |
|---|---|---|---|---|---|
fast | chi_sim+eng | 默认 | 2.0 | auto | 快速基准 |
standard | chi_sim+eng | 默认 | 2.0 | auto | 通用默认 |
scan | chi_sim+eng | 默认 | 2.0 | auto | 干净扫描件 |
scan_mixed | chi_sim+eng | 默认 | 1.2 | auto | 扫描件页面横竖混排 |
old | chi_sim+eng | 6 | 2.0 | sauvola | 老旧脏污 |
mixed | chi_sim+chi_sim_vert+eng | 默认 | 2.0 | auto | 文字横竖混排 |
vertical | chi_sim_vert | 5 | 2.0 | auto | 文字竖排古籍 |
任何预设都能被显式参数覆盖,例如 --preset scan_mixed --rotate-threshold 1.5 --thresholding sauvola。
8. 踩过的坑与结论(换机时重点看)
8.1 Chocolatey 不要用
在非管理员会话下执行 choco install 反复失败,留下 C:\ProgramData\chocolatey 和 .chocolatey 锁文件,清理麻烦。结论:Tesseract 和 Ghostscript 一律手动安装官方安装包。
8.2 --remove-background 已废弃
OCRmyPDF v13 起移除了 --remove-background,调用会直接抛 NotImplementedError: --remove-background is temporarily not implemented。当前脚本已彻底移除该参数。
8.3 --clean / --clean-final 依赖 unpaper
这两个参数需要外部程序 unpaper,Windows 上没有官方安装程序。未安装时使用会报 Could not find program 'unpaper' on the PATH。当前脚本已彻底移除这两个参数。需要清理图像时,用 --thresholding sauvola 替代,或外部预处理。
8.4 argparse 的 input_dir 必须声明 type=Path
否则 args.input_dir 是字符串,调用 .resolve() 报 AttributeError: 'str' object has no attribute 'resolve'。当前脚本已修正。
8.5 Windows 下 if __name__ == "__main__": 不能省
OCRmyPDF 在 Windows 上依赖它正确启动子进程,删掉会挂起或重复执行。
8.6 网络路径(SMB)性能与稳定性
对 \\192.168.x.x\share 直接跑 OCR,读写中间文件走网络会显著拖慢速度,长任务中途网络抖动还可能导致部分文件写入失败。建议先复制到本地磁盘处理,完成后再同步回网络位置。
8.7 纯图页/封面/空白页容易误旋转
文字太少,OSD 置信度低,可能被误转或漏转。如果目录里这类页多:
- 调高
--rotate-threshold(如1.8)让判定更保守; - 或单独挑出来手动确认方向。
8.8 双面扫描背面倒置(180°)无需额外参数
rotate_pages 底层 OSD 检测 0°/90°/180°/270° 四个方向,背面倒置会自动转正。
8.9 --mode redo 不损失原图
只删除旧的不可见文本层重做 OCR,不栅格化页面。只有 --mode force 才会栅格化。
9. 提高识别精度的优先顺序
按性价比从高到低:
- 换
tessdata_best版chi_sim.traineddata—— 收益最直接,不增加运行时间。 --user-words词典 —— 专有名词、人名、术语多的文档效果明显。--preset old(sauvola+ PSM 6)—— 针对老旧、脏污、复杂版面。--no-downsample-large-images—— 高分辨率小字密集文档(地图、图纸),会显著增加内存和耗时。- 调低
--rotate-threshold—— 仅针对页面朝向漏判问题。
10. 换机迁移检查清单
- Python 3.10+ 已装,非 Microsoft Store 版,已加入 PATH
-
pip install ocrmypdf完成,ocrmypdf --version正常 - Tesseract 已装,
tessdata下含chi_sim、eng、osd -
tesseract --list-langs输出三项齐全 - Ghostscript 已装,
gswin64c --version正常 -
ocr.py已拷贝到工作目录 - 用
--dry-run在小样本目录验证文件列表 - 小样本正式跑一次,打开
_ocr.pdf检查旋转、文本层、视觉是否与原版一致 - 确认无误后再对全量目录执行
附:一句话总结
手动装 Python + Tesseract(勾 chi_sim、eng、osd)+ Ghostscript,
pip install ocrmypdf,跑python ocr.py "<目录>" --preset scan_mixed -r。不要用 Chocolatey,不要用--clean/--remove-background,网络路径先拷本地。
评论