阅读文本 / Reading text
“翻译准确,但是格式全没了”是文档交付里很常见的返工。一个段落在 Word 里看起来只有一句话,在 DOCX 中却可能包含多个 run:普通文本、强调文本、字号变化,以及不属于普通 run 列表的超链接等结构。
使用 python-docx 时,直接给
paragraph.text
赋值,会用新的文本替换段落现有内容。段落级样式可以保留,但原有 run 级格式不会因此自动保留。这个行为在
python-docx 文本 API
中有明确说明。
这张图展示本地合成 DOCX 的测试记录,不是排版引擎截图。本文解决的是“受约束替换不丢格式”,不是自动解决跨语言句法重排,也不是保证翻译后分页不变。
先限定可安全修改的范围
这份最小实现只接受由普通文字 run 组成的段落。段落包含超链接、图片、域、换行、制表符或其他复杂 XML 节点时,明确拒绝,让调用方转入专门流程。代码不会悄悄把这些元素扁平化。
翻译输入也不是一个随意的字符串列表:每个结果必须带原始 run 的序号,数量、顺序、类型全部匹配后才开始修改。验证放在写入之前,避免更新一半才发现后面的字段有问题。
一个可以重复跑的最小测试
测试环境为
python-docx==1.2.0
。这里的英文来自手写固定对照,用于验证结构,不调用机器翻译。保存后重新打开
translated-runs.docx
再验收,避免仅检查内存里的对象。
from docx import Document
from docx.oxml.ns import qn
def replace_plain_runs(paragraph, translated):
if any(child.tag not in {qn("w:pPr"), qn("w:r")}
for child in paragraph._p):
raise ValueError("paragraph contains unsupported structures")
runs = paragraph.runs
for run in runs:
if any(child.tag not in {qn("w:rPr"), qn("w:t")}
for child in run._r):
raise ValueError("run contains a field, image or control")
if len(translated) != len(runs):
raise ValueError("segment count mismatch")
for index, item in enumerate(translated):
if (not isinstance(item, dict) or item.get("id") != index
or not isinstance(item.get("text"), str)
or any(ch in item["text"] for ch in "\t\r\n")):
raise ValueError("segment identity or text mismatch")
for run, item in zip(runs, translated):
run.text = item["text"]
def signature(paragraph):
return [(r.text, r.bold, r.italic) for r in paragraph.runs]
doc = Document()
p = doc.add_paragraph()
p.add_run("请在 ")
p.add_run("周五之前").bold = True
p.add_run(" 提交报告。")
before = signature(p)
try:
replace_plain_runs(p, [{"id": 0, "text": "Incomplete"}])
raise AssertionError("bad segments accepted")
except ValueError:
assert signature(p) == before
replace_plain_runs(p, [
{"id": 0, "text": "Submit the report "},
{"id": 1, "text": "before Friday"},
{"id": 2, "text": "."},
])
doc.save("translated-runs.docx")
restored = Document("translated-runs.docx").paragraphs[0]
assert restored.text == "Submit the report before Friday."
assert restored.runs[1].bold is True
assert len(restored.runs) == 3
complex_p = doc.add_paragraph("keep")
complex_p.runs[0].add_break()
try:
replace_plain_runs(complex_p, [{"id": 0, "text": "reject"}])
raise AssertionError("line break accepted")
except ValueError:
assert complex_p.text == "keep\n"
print("PASS count mismatch: no partial write")
print("PASS save/reopen: 3 runs, emphasis preserved")
print("PASS unsupported control: rejected")
print(signature(restored))
为什么不把每个 run 单独交给翻译器
run 是格式边界,不是语义边界。Word 的编辑历史也可能把一个词切成多个 run。逐个孤立翻译会丢上下文;不同语言的语序还可能把强调部分移到句首或句尾。
实际的译文生成应以完整句段提供上下文,再返回经过校验的结构化分段。返回结果和原来不等长时,不要硬塞到现有 run 中:需要一个明确的重新排版策略或人工复核。本文简单例子刚好可以保持三个片段,不能把它推广成所有语言都能一一映射。
把验收拆成三层
层次
检查内容
本文是否覆盖
数据结构
run 数量、顺序、加粗签名与保存后恢复
覆盖这个受限样例
语义内容
术语、否定词、日期、金额、遗漏
不覆盖;需要独立校对
视觉版式
字体回退、断行、页码、表格溢出
不覆盖;需要渲染再检查
_p
和
_r
是这里为了检查 XML 使用的内部属性。升级 python-docx 时要固定版本并重新跑测试,不能假设内部结构永远不变。需要处理表格、页眉、页脚、文本框和修订痕迹时,也要分别枚举内容范围;
document.paragraphs
不能代表文档中的所有文字。
交付前的停止条件
不支持的结构、分段不匹配、字体缺字、重要字段改变,任何一个都应该阻止自动交付。把原文件只读保存,译文写到新文件,再附上语言、处理版本和人工验收记录,这比“保存成功”更能说明质量。
如果你需要从上传文档进入翻译和审阅流程,可以查看
OmniDoc 产品入口
。本文的代码示例是独立的开源库教学样例,不代表 OmniDoc 当前导入器或排版引擎的实现。核心原则只有一个:把格式当成要验收的数据,而不是翻译后的附赠品。
- 1 -
更新于 2026-10-09