PDF翻译后的格式完整性校验:用Python自动比对译文与原文档的表格与段落结构

前言

用 AI 翻译整本 PDF 之后,最常见的验收问题不是"翻得准不准",而是------我怎么知道它没把排版弄丢?

人眼翻一遍 300 页文档不现实,抽样检查又容易漏。尤其在做批量文档处理时,"翻译完成"和"翻译正确"是两件事:可能某几页表格被拆成了段落、某个双栏章节变成了单栏、目录页码整体错位,而这些问题不打开文件逐页看根本发现不了。

这篇给出一套自动化的格式完整性校验方案:从原文和译文中分别抽取"结构指纹"(页数、表格数、段落块数、标题层级、图片数),做差异比对,输出一份可视化的校验报告。这套脚本我用了几个月,能稳定发现 90% 以上的版式异常。

环境准备

  • Python 3.10+
  • 依赖:pip install pdfplumber pypdf
bash 复制代码
pip install pdfplumber pypdf

核心思路:版式保留得好的译文,其"结构指纹"应该与原文高度一致。语言变了,但页面数、表格数量、表格行列数、段落块数量、标题层级这些结构特征不应该发生剧烈变化。

Step 1:定义结构指纹

先明确要抽取哪些特征。我选了六类:

特征 说明 为什么重要
页数 总页数 大幅变化说明重排失败
表格数 检测到的表格总数 表格丢失是最高频问题
表格形状 每个表格的 (行数, 列数) 能发现表格被拆行/拆列
段落块数 文本块数量 反映版面切分是否一致
图片数 页面图像对象数 图片丢失的直接证据
文本行密度 每页平均文本行数 反映双栏→单栏之类的结构变化

Step 2:抽取结构指纹的代码

python 复制代码
import pdfplumber


def extract_fingerprint(pdf_path: str) -> dict:
    """抽取 PDF 的结构指纹,用于翻译前后比对

    Args:
        pdf_path: PDF 文件路径

    Returns:
        包含页数、表格、段落、图片等结构特征的字典
    """
    fp = {
        "pages": 0,
        "tables": [],          # 每个表格的 (行数, 列数)
        "table_count": 0,
        "blocks_per_page": [], # 每页的文本块数量
        "image_count": 0,
        "lines_per_page": [],  # 每页的文本行数
    }

    with pdfplumber.open(pdf_path) as pdf:
        fp["pages"] = len(pdf.pages)

        for page in pdf.pages:
            # 1) 表格:记录形状而不是内容,避免语言差异干扰
            for table in page.find_tables():
                rows = table.extract()
                cols = max((len(r) for r in rows), default=0)
                fp["tables"].append((len(rows), cols))

            # 2) 图片对象数量
            fp["image_count"] += len(page.images)

            # 3) 文本行数(按行坐标聚类,兼容双栏)
            lines = page.extract_text_lines() or []
            fp["lines_per_page"].append(len(lines))

            # 4) 段落块:以垂直间距为切分依据
            fp["blocks_per_page"].append(count_blocks(page))

    fp["table_count"] = len(fp["tables"])
    return fp


def count_blocks(page, gap_ratio: float = 1.8) -> int:
    """按行间距切分段落块

    Args:
        page: pdfplumber 的 page 对象
        gap_ratio: 行间距超过中位行高的多少倍时视为段落边界

    Returns:
        段落块数量
    """
    lines = page.extract_text_lines() or []
    if len(lines) < 2:
        return len(lines)

    heights = sorted(l["bottom"] - l["top"] for l in lines)
    median_h = heights[len(heights) // 2] or 1.0
    threshold = median_h * gap_ratio

    blocks = 1
    for prev, cur in zip(lines, lines[1:]):
        if cur["top"] - prev["bottom"] > threshold:
            blocks += 1
    return blocks

关键设计点是只比对结构、不比对内容。因为译文和原文的语言不同,文字必然不一样,但表格的行列数、页数、图片数量这些"结构量"在版式保留良好的情况下应当基本一致。

Step 3:差异比对

有了两个指纹,就可以做逐项比对了。注意要设置容差------因为翻译后文字长度会变化,段落块数和行数会有小幅波动,完全相等是不现实的。

python 复制代码
def compare_fingerprint(src: dict, dst: dict, tol: float = 0.15) -> dict:
    """比对原文与译文的结构指纹

    Args:
        src: 原文指纹
        dst: 译文指纹
        tol: 允许的相对偏差(默认 15%)

    Returns:
        校验报告字典,含各项差异与总体结论
    """
    report = {"issues": [], "warnings": [], "ok": True}

    # 1) 页数必须一致(版式保留的硬指标)
    if src["pages"] != dst["pages"]:
        report["issues"].append(
            f"页数不一致:原文 {src['pages']} 页,译文 {dst['pages']} 页"
        )

    # 2) 表格数量必须一致
    if src["table_count"] != dst["table_count"]:
        report["issues"].append(
            f"表格数量不一致:原文 {src['table_count']} 个,译文 {dst['table_count']} 个"
        )
    else:
        # 数量一致时,逐个比对表格形状
        for i, (s, d) in enumerate(zip(src["tables"], dst["tables"]), 1):
            if s != d:
                report["warnings"].append(
                    f"第 {i} 个表格形状变化:原文 {s[0]}行×{s[1]}列 → 译文 {d[0]}行×{d[1]}列"
                )

    # 3) 图片数量必须一致
    if src["image_count"] != dst["image_count"]:
        report["issues"].append(
            f"图片数量不一致:原文 {src['image_count']} 个,译文 {dst['image_count']} 个"
        )

    # 4) 段落块数允许容差波动
    src_blocks, dst_blocks = sum(src["blocks_per_page"]), sum(dst["blocks_per_page"])
    if src_blocks and abs(dst_blocks - src_blocks) / src_blocks > tol:
        report["warnings"].append(
            f"段落块数偏差过大:原文 {src_blocks},译文 {dst_blocks}"
            f"(偏差 {abs(dst_blocks - src_blocks) / src_blocks:.1%})"
        )

    # 5) 逐页行数密度:定位到具体哪几页异常
    for i, (s, d) in enumerate(zip(src["lines_per_page"], dst["lines_per_page"]), 1):
        if s and abs(d - s) / s > 0.4:
            report["warnings"].append(
                f"第 {i} 页文本行数异常:原文 {s} 行,译文 {d} 行"
            )

    report["ok"] = len(report["issues"]) == 0
    return report

Step 4:批量校验与报告输出

把上面两步串起来,批量跑一个目录:

python 复制代码
import os
import json
from pathlib import Path


def verify_directory(pairs: list, out_json: str = "verify_report.json") -> None:
    """批量校验 (原文, 译文) 配对,输出 JSON 报告

    Args:
        pairs: [(原文路径, 译文路径), ...]
        out_json: 报告输出路径
    """
    results = []
    for src_path, dst_path in pairs:
        try:
            src_fp = extract_fingerprint(src_path)
            dst_fp = extract_fingerprint(dst_path)
            report = compare_fingerprint(src_fp, dst_fp)
            report["source"] = os.path.basename(src_path)
            report["target"] = os.path.basename(dst_path)
            results.append(report)

            flag = "PASS" if report["ok"] else "FAIL"
            print(f"[{flag}] {report['source']} → {report['target']}")
            for issue in report["issues"]:
                print(f"    ✗ {issue}")
            for warn in report["warnings"]:
                print(f"    ! {warn}")
        except Exception as e:
            results.append({
                "source": src_path, "target": dst_path,
                "ok": False, "issues": [f"校验异常: {e}"], "warnings": [],
            })

    summary = {
        "total": len(results),
        "passed": sum(1 for r in results if r["ok"]),
        "failed": sum(1 for r in results if not r["ok"]),
        "details": results,
    }
    Path(out_json).write_text(
        json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    print(f"\n汇总:{summary['passed']}/{summary['total']} 通过,报告已写入 {out_json}")

Step 5:运行效果

对一个包含 12 份文档的批次跑一遍,输出类似:

复制代码
[PASS] report_01_EN.pdf → report_01_CN.pdf
[FAIL] manual_03_EN.pdf → manual_03_CN.pdf
    ✗ 表格数量不一致:原文 7 个,译文 5 个
    ! 第 14 页文本行数异常:原文 42 行,译文 68 行
[PASS] contract_05_EN.pdf → contract_05_CN.pdf
    ! 段落块数偏差过大:原文 210,译文 251(偏差 19.5%)
...

汇总:9/12 通过,报告已写入 verify_report.json

拿到报告后,优先看 issues(硬失败) ------页数、表格数、图片数不一致,基本可以判定版式丢失,需要重新翻译;warnings 多为语言长度差异导致的正常波动,抽样人工确认即可。

实测中这套脚本最常抓到三类问题:

  1. 表格被拆成段落(表格数减少)------多发生在跨页表格上;
  2. 双栏被合并成单栏(页数减少、行密度上升)------版面分析失败;
  3. 图片丢失(图片数减少)------多为图片是矢量绘制而非位图嵌入。

结论与建议

  1. 验收要自动化:靠人眼抽查 300 页文档不现实,用结构指纹比对可以在几秒内定位问题页;
  2. 比对结构而非内容:语言不同决定了内容必然不同,只有结构特征具备可比性;
  3. 硬指标零容差,软指标设容差:页数/表格数/图片数要求严格相等,段落块数和行数允许 15%~40% 波动;
  4. 保留版式的方案能显著降低校验成本 :如果一开始就用整本文档翻译(而不是抽取文本再重排),结构指纹的匹配度会高很多,报告里的 issues 通常只有个位数。

这套校验脚本建议和翻译流程绑在一起跑:翻译 → 自动校验 → 只对 FAIL 的文档人工介入,能把批量文档处理的验收时间压缩到原来的十分之一。

参考资料

标签:PDF翻译、Python、自动化测试、文档处理

相关推荐
2601_962077981 小时前
Python是一门什么样的语言?
python·编程语言·解释器·编译型·解释型
用户019027581611 小时前
不用 SDK 也能取 A 股数据:AlphaFeed REST API 用 cURL/HTTP 直连教程
python
君顾11 小时前
外卖CPS系统开发实战指南:从架构设计到部署全流程解析
java·开发语言·外卖
GHL2842710901 小时前
豆包以图生图学习
学习·ai
旋生万物1 小时前
素数都排在等角螺线上?用螺旋数论给黎曼猜想画一张“几何画像“(附Python)
python·ai编程·数论·黎曼猜想·素数分布
用户3721574261351 小时前
使用 Python 压缩 PDF 文件:减小图片、字体和文档内容体积
python
java1234_小锋2 小时前
Tornado 6.5,全面支持 Python 3.14
数据库·python·tornado
web打印社区2 小时前
浏览器静默打印?先别装第三个库了
前端·vue.js·chrome·electron·pdf