AI Agent学习日记 Day2
今天继续实现word翻译功能,上次的代码翻译完后会丢失图片等元素,让deepseek改了好几版代码都还是有问题,我决定先搞懂根本原因再改代码。
经调查,Word 的文档结构(通过 COM 对象模型)如下:
Document
└─ Paragraphs
└─ Range
├─ Runs (文本块)
├─ InlineShapes (内联图片、图表等)
└─ Fields (域代码,如目录)
其中,Paragraphs(段落)的定义是:一个段落是用户按下回车键结束的一段内容。一个 Paragraph 对象包含一个 Range,Range 覆盖该段落的所有内容(包括文本、图片、表格、形状等)。段落是文档流的基本块,每个段落可以包含多种元素(文本、内联图片、域代码等)。
Range(范围)是文档中连续的任意区域,可以是一个字符、一个段落、整个文档。Range 可以包含多个 Paragraph 和多个 InlineShape(内联对象),并且可以通过 Range.Runs 访问该范围内的所有文本块(Runs)。通过 Range 可以操作文本、格式、以及插入或删除内容。
Runs(文本块)是 Range 中具有相同格式的连续文本序列。Word 在内部将文本按格式变化自动分割为多个 Run。例如,同一段落中字体加粗、颜色变化、或插入图片都会导致 Run 的拆分。
-
每个 Run 是纯文本或包含内嵌对象(如
InlineShape、Field等)。 -
一个 Run 中如果有图片,则这个 Run 的
InlineShapes.Count > 0,且Run.Text通常为空或只包含占位符字符。 -
图片本身不是一个 Run,而是附着在某个 Run 上的对象。
之前的代码中,我们通过操作Range,提取出Range中的所有文本,然后翻译完再替换掉。这就会导致原本存在于Range中的图片等元素也被覆盖掉,所以会丢失元素。
deepseek给出的解决方案是逐 Run 处理,不再整个替换掉Range。但是我想到,既然Run会按照格式变化划分,那”这是一句完整的话”这样的句子就会被划分为“这是一句”“完整”“的话”三个Run,这样喂给AI翻译效果肯定不行,于是我放弃了这个方案。
既然这条路不通,那就回过头去重新开始。实际上python有专门的库python-docx来操作word,但是这个库处理不了复杂的元素,如插入的图形,文本框等,所以我一开始才会采用win32com。在我实际的业务中,word文件里面文本框等复杂元素有不少,所以这个问题必须要解决。
随着调查的深入,我发现 Word 文档(.docx)实际上是一个 ZIP 压缩包,包含多个 XML 文件。python-docx 库主要封装了 document.xml 中主流元素(段落、表格、图片)的操作,但文本框(TextBox)在 XML 中属于绘图对象(DrawingML),结构复杂且多变,官方库未提供直接 API。但是我可以通过直接解析 document.xml,查找 <w:txbxContent> 标签(文本框内容的容器),提取并修改其中的文本。
root = doc.part.element
# 查找所有 w:txbxContent 元素(忽略命名空间)
txbx_contents = root.xpath('.//*[local-name()="txbxContent"]')
for txbx in txbx_contents:
# 遍历文本框内的所有段落
for para_elem in txbx.xpath('.//*[local-name()="p"]'):
# 提取所有文本节点内容
original = ''.join(para_elem.xpath('.//*[local-name()="t"]/text()')).strip()
if original and len(original) > 1:
translated = translate_text(original, target_lang, delay, context=context)
if translated:
# 替换所有 w:t 节点的文本
for t_elem in para_elem.xpath('.//*[local-name()="t"]'):
t_elem.text = translated
完美解决!
下一步就是多线程并行翻译了,Excel和PPT都有,word也一定要有!
我的PPT是按页,Excel是按sheet并行翻译,但是word是流式文档,不存在天然可拆分的单位,即使word有页码,但每一页也并不是独立的,甚至一句话都能跨两页。所以只能先把要翻译的元素全部提取出来,再多线程并行翻译,最后单线程顺序写回。这既能大幅提升性能(API 调用成为瓶颈),又能避免并发修改文档导致的数据损坏。
-
收集阶段(单线程):遍历文档,收集所有需要翻译的文本单元(段落、表格单元格内的段落、文本框内的段落)。每个单元包含:唯一标识(如索引)、原始文本、对象引用(用于写回)。
-
并行翻译阶段(多线程):使用线程池,将文本单元分配给多个线程,每个线程调用
translate_text进行翻译,返回(单元索引, 译文)。 -
写回阶段(单线程):按索引顺序将译文写回对应的文档对象。
我本来想着PPT都能多线程写回去,为什么word不行?原因如下:
-
python-pptx(PPT):每个Slide对象在内部对应 XML 树的不同分支,且python-pptx在加载整个文档后,各个幻灯片之间的 XML 节点树是相对独立的。多线程同时修改不同幻灯片的形状(shape.text = ...),实际上是在操作不同子树,只要没有同时修改同一个对象(例如同一张幻灯片),通常不会引发冲突。python-pptx也没有在内部维护复杂的全局缓存(如 xpath 缓存),所以实践中并行是安全的。 -
python-docx(Word):文档的段落、表格、文本框等元素都位于同一棵 XML 树中,且python-docx内部会维护一些缓存和索引(例如段落编号)。多个线程同时修改不同位置的文本,虽然理论上可能不会直接冲突,但lxml底层在修改节点时可能会触发父节点重新序列化或缓存失效,存在潜在风险。更稳妥的做法是只并行翻译,不并行写回。
但是我看了设计思路后觉得还有问题,如果只是把所有的翻译文本直接分给多个线程翻译,那么每个线程拿到的文本块都是零散的,为了保证翻译效果,我在把文档喂给AI翻译时是传递了上下文的,所以只有上下文连贯地发给AI翻译才能取得最好的翻译效果。于是我将翻译文本全部提取出来后,按线程数将翻译文本分组,每一组拿到的文本都是连贯的,最后再多线程分组翻译。
嗯,效果拔群,完美!
下次继续实现自动写脚本的Agent吧。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)