跳过导航
返回
2410 月
2026 年星期六

Windows PDF 批量 OCR 环境配置备忘

篇幅 4,037 字 · 约 13 分钟 编辑这一则

Windows PDF 批量 OCR 环境配置备忘

目标:遍历目录,为每个 PDF 生成 原名_ocr.pdf,OCR 文本层位于原始图像之下,视觉与原版一致。 适用:Windows 10/11,扫描件、页面横竖混排、横排/竖排中文。 记录日期:2026-10-24


0. 版本记录(换机后按此对齐)

组件版本 / 路径说明
操作系统Windows 10/11 64-bit
Python3.14,D:\Program Files\Python314从 python.org 安装,非 Microsoft Store 版
pip 包ocrmypdf(v13+)v13 起已移除 --remove-background
Tesseract5.x,C:\Program Files\Tesseract-OCRUB-Mannheim 构建
Tesseract 语言chi_sim、eng、osdosd 是 rotate_pages 的依赖
Ghostscript10.x,C:\Program Files\gs\gs10.xx.x64-bit
脚本ocr.py见第 6 节

⚠️ 不要用 Microsoft Store 版 Python,与 OCRmyPDF 存在兼容性问题。 ⚠️ 不建议用 Chocolatey 安装 Tesseract/Ghostscript——实测在非管理员会话下反复失败,且残留锁文件难清理。手动安装更稳。


1. 安装 Python

  1. 从 https://www.python.org/downloads/ 下载 64-bit 安装包。
  2. 安装时勾选 “Add python.exe to PATH”。
  3. 安装到非系统盘也可(如 D:\Program Files\Python314)。
  4. 验证:
python --version
pip --version

2. 安装 Tesseract(含中文与 OSD)

2.1 下载与安装

  1. 打开 UB-Mannheim 官方页面:https://github.com/UB-Mannheim/tesseract/wiki
  2. 下载 64 位安装程序(文件名类似 tesseract-ocr-w64-setup-5.5.0.exe)。
  3. 运行安装程序,关键选项:
    • 安装路径:保持默认 C:\Program Files\Tesseract-OCR。
    • 组件选择页:展开 Additional language data (download),勾选:
      • Chinese (Simplified) → chi_sim
      • English → 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

  1. 打开官方下载页:https://ghostscript.com/releases/gsdnld.html
  2. 下载 Windows 64-bit 安装包(gs10.xx.x-win64.exe)。
  3. 安装到默认路径 C:\Program Files\gs\gs10.xx.x,安装程序通常会自动把 bin 加入 PATH。
  4. 关闭并重开 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 预设对比

预设languagePSM旋转阈值二值化适用
fastchi_sim+eng默认2.0auto快速基准
standardchi_sim+eng默认2.0auto通用默认
scanchi_sim+eng默认2.0auto干净扫描件
scan_mixedchi_sim+eng默认1.2auto扫描件页面横竖混排
oldchi_sim+eng62.0sauvola老旧脏污
mixedchi_sim+chi_sim_vert+eng默认2.0auto文字横竖混排
verticalchi_sim_vert52.0auto文字竖排古籍

任何预设都能被显式参数覆盖,例如 --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 置信度低,可能被误转或漏转。如果目录里这类页多:

8.8 双面扫描背面倒置(180°)无需额外参数

rotate_pages 底层 OSD 检测 0°/90°/180°/270° 四个方向,背面倒置会自动转正。

8.9 --mode redo 不损失原图

只删除旧的不可见文本层重做 OCR,不栅格化页面。只有 --mode force 才会栅格化。


9. 提高识别精度的优先顺序

按性价比从高到低:

  1. 换 tessdata_best 版 chi_sim.traineddata —— 收益最直接,不增加运行时间。
  2. --user-words 词典 —— 专有名词、人名、术语多的文档效果明显。
  3. --preset old(sauvola + PSM 6)—— 针对老旧、脏污、复杂版面。
  4. --no-downsample-large-images —— 高分辨率小字密集文档(地图、图纸),会显著增加内存和耗时。
  5. 调低 --rotate-threshold —— 仅针对页面朝向漏判问题。

10. 换机迁移检查清单


附:一句话总结

手动装 Python + Tesseract(勾 chi_sim、eng、osd)+ Ghostscript,pip install ocrmypdf,跑 python ocr.py "<目录>" --preset scan_mixed -r。不要用 Chocolatey,不要用 --clean / --remove-background,网络路径先拷本地。


分享到:

评论

前一则
2026年10月24日 · 给日记站接上 Artalk 评论