文章目录
- 【Python实战】手把手教你基于Smartcat与Python制作完美格式的双语工单
-
- 一、 背景与痛点:为什么AI搞不定 Word 排版?
- 二、 解决方案架构
-
- 1. 为什么选择 Smartcat?
- 三、 Python 实现逻辑:V6 版本核心功能
- 四、 完整代码实现
-
- Python 脚本 (`merge_v6.py`)
- 五、 代码关键部分详解
-
- 1. 为什么不用 `cell.text += …` ?
- 2. 软回车技巧 (`\\n`)
- 3. XML 指纹去重
- 六、 总结
【Python实战】手把手教你基于Smartcat与Python制作完美格式的双语工单
摘要:部门面临国际化需求,急需将复杂的工单(Word格式)转换为中英文对照版本。尝试了市面上各种AI产品,发现它们无法完美保留Word中复杂的嵌套表格、红色警告等排版。本文介绍了一种“硬核”方案:利用 Smartcat 保证翻译与文档结构的稳定性,再利用 Python 进行像素级的双语合并,实现“中文原格式保留 + 英文Times New Roman字体 + 智能字号适配”。
一、 背景与痛点:为什么AI搞不定 Word 排版?
最近部门正在推进业务国际化,领导要求将现有的 航空维修工单(通常是格式极度严格的 Word 文档,包含大量嵌套表格、红色警告字体、签字栏等)全部升级为 中英文双语对照版。
起初我尝试了各种 AI 文档工具,结果令人崩溃:
结论:截止目前,还没有一款 AI 产品能实现“完美读取 Word 复杂格式 -> 翻译 -> 在原单元格内输出双语 -> 导出 Word”。
于是,我设计了一套“专业分工”的自动化工作流。
二、 解决方案架构
既然无法一步到位,我们将任务拆解为两步:
#mermaid-svg-E6EbDBP2i9OWnxFm{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E6EbDBP2i9OWnxFm .error-icon{fill:#552222;}#mermaid-svg-E6EbDBP2i9OWnxFm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E6EbDBP2i9OWnxFm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E6EbDBP2i9OWnxFm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E6EbDBP2i9OWnxFm .marker.cross{stroke:#333333;}#mermaid-svg-E6EbDBP2i9OWnxFm svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E6EbDBP2i9OWnxFm p{margin:0;}#mermaid-svg-E6EbDBP2i9OWnxFm .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm .cluster-label text{fill:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm .cluster-label span{color:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm .cluster-label span p{background-color:transparent;}#mermaid-svg-E6EbDBP2i9OWnxFm .label text,#mermaid-svg-E6EbDBP2i9OWnxFm span{fill:#333;color:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm .node rect,#mermaid-svg-E6EbDBP2i9OWnxFm .node circle,#mermaid-svg-E6EbDBP2i9OWnxFm .node ellipse,#mermaid-svg-E6EbDBP2i9OWnxFm .node polygon,#mermaid-svg-E6EbDBP2i9OWnxFm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-E6EbDBP2i9OWnxFm .rough-node .label text,#mermaid-svg-E6EbDBP2i9OWnxFm .node .label text,#mermaid-svg-E6EbDBP2i9OWnxFm .image-shape .label,#mermaid-svg-E6EbDBP2i9OWnxFm .icon-shape .label{text-anchor:middle;}#mermaid-svg-E6EbDBP2i9OWnxFm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-E6EbDBP2i9OWnxFm .rough-node .label,#mermaid-svg-E6EbDBP2i9OWnxFm .node .label,#mermaid-svg-E6EbDBP2i9OWnxFm .image-shape .label,#mermaid-svg-E6EbDBP2i9OWnxFm .icon-shape .label{text-align:center;}#mermaid-svg-E6EbDBP2i9OWnxFm .node.clickable{cursor:pointer;}#mermaid-svg-E6EbDBP2i9OWnxFm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-E6EbDBP2i9OWnxFm .arrowheadPath{fill:#333333;}#mermaid-svg-E6EbDBP2i9OWnxFm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-E6EbDBP2i9OWnxFm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-E6EbDBP2i9OWnxFm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E6EbDBP2i9OWnxFm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-E6EbDBP2i9OWnxFm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E6EbDBP2i9OWnxFm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-E6EbDBP2i9OWnxFm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-E6EbDBP2i9OWnxFm .cluster text{fill:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm .cluster span{color:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-E6EbDBP2i9OWnxFm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-E6EbDBP2i9OWnxFm rect.text{fill:none;stroke-width:0;}#mermaid-svg-E6EbDBP2i9OWnxFm .icon-shape,#mermaid-svg-E6EbDBP2i9OWnxFm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E6EbDBP2i9OWnxFm .icon-shape p,#mermaid-svg-E6EbDBP2i9OWnxFm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-E6EbDBP2i9OWnxFm .icon-shape rect,#mermaid-svg-E6EbDBP2i9OWnxFm .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E6EbDBP2i9OWnxFm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-E6EbDBP2i9OWnxFm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-E6EbDBP2i9OWnxFm :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
上传
机器翻译+人工校对
结构保持一致
提供格式基准
样式侦测与注入
中文原稿 cn.docx
Smartcat 翻译平台
导出英文版 en.docx
Python 自动化脚本
双语对照版_V6.docx
1. 为什么选择 Smartcat?
Smartcat 是一款强大的云端翻译工具。它的核心优势在于对 Office 文档的解析能力:
- 结构锁定:它能提取文档中的纯文本进行翻译,同时在后台严格锁定文档的 XML 结构标签。
- 格式一致:从 Smartcat 导出的“英文版”文档,其表格数量、行数、单元格顺序与“中文原版”是完全一致的。
- 术语库:航空术语(如 Check vs Inspect)容不得半点差错,Smartcat 可以挂载术语库保证准确性。
操作流程:
三、 Python 实现逻辑:V6 版本核心功能
有了结构一致的“中文原版”和“英文译版”,接下来的任务就是把它们合并。为了达到出版级的效果,我编写了 V6 版本的脚本,实现了以下核心功能:
- 脚本在处理每一个中文段落时,会像侦探一样分析:它是红色的吗?是加粗的吗?字号是多少?
- 字体统一:英文部分强制使用标准西文字体 Times New Roman。
- 字号跟随:如果中文是大标题,英文自动变大;如果是小注脚,英文自动变小。
- 颜色警示:如果中文是红色警告,英文也自动标红加粗;普通文字则显示为黑色。
- 支持表格套表格(嵌套表格),确保无死角覆盖。
- 不动原有的中文,而是利用“软回车”,将英文优雅地追加在中文下方。
四、 完整代码实现
请确保本地已安装 python-docx 库:
pip install python-docx
Python 脚本 (merge_v6.py)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from docx import Document
from docx.shared import Pt, RGBColor
from docx.oxml.ns import qn
# ================= 配置区 =================
# 确保这两个文件在脚本同级目录下,且表格结构一致
FILE_CN = 'cn.docx' # 中文原版 (Smartcat 上传前的原件)
FILE_EN = 'en.docx' # 英文译版 (Smartcat 导出的纯译文件)
FILE_OUT = '双语对照版_V6_TNR字体.docx' # 输出文件名
# 字体设置
ENGLISH_FONT_NAME = 'Times New Roman' # 国际通用英文衬线字体
WARNING_COLOR = RGBColor(255, 0, 0) # 红色 (用于警告信息)
NORMAL_COLOR = RGBColor(0, 0, 0) # 黑色 (用于普通正文)
# =========================================
def analyze_style(paragraph):
"""
【核心功能 1:样式侦探】
扫描中文段落,提取关键样式特征。
参数: paragraph (docx段落对象)
返回: (is_bold, is_red, ref_size)
"""
is_bold = False
is_red = False
ref_size = None # 默认大小
# 遍历段落里的所有 Run (文字块)
for run in paragraph.runs:
# 1. 检测加粗
if run.bold:
is_bold = True
# 2. 检测红色 (RGB: 255,0,0)
# 航空文档中红色通常代表警告,翻译后必须保留此特征
if run.font.color and run.font.color.rgb == RGBColor(255, 0, 0):
is_red = True
# 3. 捕捉字号 (如果中文手动设置了字号,就抓取它)
if run.font.size is not None:
ref_size = run.font.size
return is_bold, is_red, ref_size
def set_smart_style(run, is_bold_context, is_red_context, ref_size):
"""
【核心功能 2:样式应用师】
根据侦探传回的线索,给英文设置正确的样式。
规则:Times New Roman + 黑色(或红) + 字号跟随中文
"""
# 1. 强制设置字体为 Times New Roman
run.font.name = ENGLISH_FONT_NAME
r = run._element
# 这一步是为了确保 Word 将其识别为西文字体(w:eastAsia),防止回退到宋体
r.rPr.rFonts.set(qn('w:eastAsia'), ENGLISH_FONT_NAME)
# 2. 设置字号 (动态跟随中文)
if ref_size is not None:
run.font.size = ref_size
else:
# 如果中文没有独立字号(也就是跟随段落默认),英文也不设,让它自动继承
pass
# 3. 颜色与加粗逻辑
if is_red_context:
# 场景:警告/禁止事项 -> 英文也要醒目(红+粗)
run.font.color.rgb = WARNING_COLOR
run.bold = True
else:
# 场景:普通文本 -> 英文保持黑色,加粗状态跟随中文标题
run.font.color.rgb = NORMAL_COLOR
run.bold = is_bold_context
def merge_text_lines(paras_cn, text_en_raw):
"""
【核心功能 3:对齐合并】
将英文文本按行拆分,并插空填入中文段落后。
"""
# 将英文按换行符拆分,过滤空行
lines_en = [line.strip() for line in text_en_raw.split('\\n') if line.strip()]
if not lines_en:
return 0
count = 0
# 按照段落数量进行循环 (防止索引越界)
limit = min(len(paras_cn), len(lines_en))
for i in range(limit):
p_cn = paras_cn[i]
line_en = lines_en[i]
# 简单去重:如果中文格子里已经有了这段英文,就不重复添加
if line_en in p_cn.text:
continue
# === A. 侦测中文样式 (侦探出动) ===
is_bold, is_red, ref_size = analyze_style(p_cn)
# === B. 写入英文 ===
# 如果中文段落本身有字,则先换行(\\n);如果是空行,直接写
# 使用 add_run 而不是 add_paragraph,是为了保持在同一个段落里,行距更紧凑
prefix = '\\n' if p_cn.text.strip() else ''
run = p_cn.add_run(prefix + line_en)
# === C. 应用样式 (设计师出动) ===
set_smart_style(run, is_bold, is_red, ref_size)
count += 1
# 处理溢出的行 (例如中文只有1行,英文翻译成了3行)
# 将多余的英文全部追加到中文的最后一段后面
if len(lines_en) > len(paras_cn) and len(paras_cn) > 0:
last_p = paras_cn[–1]
is_bold, is_red, ref_size = analyze_style(last_p)
for extra_line in lines_en[limit:]:
if extra_line not in last_p.text:
run = last_p.add_run('\\n' + extra_line)
set_smart_style(run, is_bold, is_red, ref_size)
count += 1
return count
def process_table_pair(tbl_cn, tbl_en, processed_cells):
"""
【核心功能 4:递归表格处理器】
支持处理表格嵌套(表格里的格子里还有表格)
"""
# 确保不越界,取行数较小值
row_limit = min(len(tbl_cn.rows), len(tbl_en.rows))
for r in range(row_limit):
row_cn = tbl_cn.rows[r]
row_en = tbl_en.rows[r]
# 确保列不越界
cell_limit = min(len(row_cn.cells), len(row_en.cells))
for c in range(cell_limit):
cell_cn = row_cn.cells[c]
cell_en = row_en.cells[c]
# 使用 XML 元素 ID 进行去重,防止合并单元格被多次处理
# 这是一个非常关键的技巧,否则大单元格会被重复写入多次
if cell_cn._element in processed_cells: continue
processed_cells.add(cell_cn._element)
txt_cn = cell_cn.text.strip()
txt_en = cell_en.text.strip()
# 只有当英文有内容,且不等于中文时才合并
if txt_en and txt_en != txt_cn:
# 确保有段落对象
if len(cell_cn.paragraphs) == 0: cell_cn.add_paragraph()
# 执行合并
merge_text_lines(cell_cn.paragraphs, txt_en)
# 递归检查:如果单元格里还套着表格
if len(cell_cn.tables) > 0 and len(cell_en.tables) > 0:
nested_limit = min(len(cell_cn.tables), len(cell_en.tables))
for n_i in range(nested_limit):
process_table_pair(cell_cn.tables[n_i], cell_en.tables[n_i], processed_cells)
def run_fix_v6():
print(f"🚀 启动 V6.0 (TNR字体 + 动态字号)…\\n中文源: {FILE_CN}\\n英文源: {FILE_EN}")
try:
doc_cn = Document(FILE_CN)
doc_en = Document(FILE_EN)
except Exception as e:
print(f"❌ 错误: {e}")
return
# 1. 处理正文 (表格之外的段落)
paras_cn = doc_cn.paragraphs
paras_en = doc_en.paragraphs
p_cn_valid = [p for p in paras_cn if p.text.strip()]
p_en_valid = [p for p in paras_en if p.text.strip()]
limit_p = min(len(p_cn_valid), len(p_en_valid))
for i in range(limit_p):
merge_text_lines([p_cn_valid[i]], p_en_valid[i].text)
# 2. 处理表格 (递归处理)
tables_cn = doc_cn.tables
tables_en = doc_en.tables
processed_cells = set() # 记录处理过的单元格
min_tables = min(len(tables_cn), len(tables_en))
print(f"正在处理 {min_tables} 个表格…")
for i in range(min_tables):
process_table_pair(tables_cn[i], tables_en[i], processed_cells)
print("-" * 30)
print(f"✅ V6 处理完成!字体已统一为 Times New Roman,字号自适应。")
doc_cn.save(FILE_OUT)
print(f"📁 文件已保存为:{FILE_OUT}")
if __name__ == '__main__':
run_fix_v6()
五、 代码关键部分详解
1. 为什么不用 cell.text += … ?
很多新手在操作 Word 时喜欢用 cell.text = cell.text + english。这是一种破坏性操作,它会清空单元格内原有的所有样式(加粗、颜色),导致排版崩坏。 **本代码采用 add_run**:它是在现有内容的屁股后面“追加”一个新的小尾巴。这样原有的中文格式完全不受影响。
2. 软回车技巧 (\\n)
我们在插入英文前,会判断 prefix = '\\n'。这相当于在 Word 里按下了 Shift+Enter(软回车)。
- 好处:英文和中文属于同一个段落。
- 效果:如果中文设置了“居中对齐”,追加的英文会自动继承居中;如果中文设置了“首行缩进”,英文也会缩进。这比创建新段落(add_paragraph)要紧凑、美观得多。
3. XML 指纹去重
在处理 Word 表格时,合并单元格是一个大坑。一个跨3行的单元格,在代码循环中会出现3次。如果不做处理,英文会被写入3次。 代码中使用了 if cell_cn._element in processed_cells: continue。 _element 是单元格在内存中的唯一 XML 地址。不管它跨了几行,只要地址一样,我们就只处理一次。
六、 总结
通过 Smartcat 解决翻译准确性与文档结构一致性的问题,再通过 Python 解决排版样式的自动化合并问题。这套方案不仅成本低(Smartcat 有免费版,Python 开源),而且产出的文档质量完全符合航空级标准。
如果你也在做文档国际化,或者需要批量处理复杂的 Word 排版,不妨试试这个思路!



