乐于分享
好东西不私藏

毁灭吧 word!golang 生成 word 模板替换问题排查

毁灭吧 word!golang 生成 word 模板替换问题排查

正在开发人事管理系统的入职登记表 word 导出功能,使用 golang 的 “github.com/nguyenthenguyen/docx” 包进行 word 模板替换时,遇到了一个诡异的问题。
就是大部分占位符都能正确替换,但有几个占位符却无法替换成功。折腾了一天才搞定🥲

我开始以为是代码的问题,检查了半天没有发现问题。然后,我又怀疑是这个三分库的 bug,可能出了异常,但是没有把 error 返回出来。但是,感觉也不太可能,毕竟在类似的使用场景下,大部分占位符都能正确替换,说明这个库的基本功能是正常的。最后,我才想到,可能是 word 模板本身的问题。

问了一下 DeepSeek,也印证了这种猜测。

文本被拆分

在 DOCX 的 XML 中,一个段落()可能由多个 (run)组成,每个 run 代表一段连续且格式一致的文本。例如,如果目标文本 “你好” 中的“你”是粗体,“好”是普通字体,它们会被分到两个不同的 run 中。Replace 方法通常只能在一个 run 内部进行简单字符串匹配,无法跨 run 查找。因此,即使文档中看起来有连续的文本,实际 XML 可能是:

<w:p>
  <w:r><w:t>你</w:t></w:r>
  <w:r><w:t>好</w:t></w:r>
</w:p>

此时,golang 代码就没法正确替换

doc.Replace("你好", "Hello", -1)

排查方法

怎么能看到 word 文件的 XML 结构呢?其实很简单,DOCX 文件本质上是一个 ZIP 压缩包,我们可以直接解压它来查看内部的 XML 文件。

  1. 将 .docx 文件的后缀改为 .zip。
  2. 直接右键解压缩这个 ZIP 文件。

可以看看这个文件夹的目录结构:

> tree
.
├── [Content_Types].xml
├── _rels
├── customXml
│   ├── _rels
│   │   └── item1.xml.rels
│   ├── item1.xml
│   └── itemProps1.xml
├── docProps
│   ├── app.xml
│   ├── core.xml
│   └── custom.xml
└── word
    ├── _rels
    │   ├── document.xml.rels
    │   └── header1.xml.rels
    ├── document.xml
    ├── endnotes.xml
    ├── fontTable.xml
    ├── footnotes.xml
    ├── header1.xml
    ├── media
    │   └── image1.png
    ├── settings.xml
    ├── styles.xml
    └── theme
        └── theme1.xml

8 directories, 18 files

这个 work 文件的结构真是非常值得学习,看起来就像是一个小型网站模板系统一样,图片和样式都被分离出来了,文档内容在 document.xml 中,样式在 styles.xml 中,图片在 media 文件夹中。
最后再用 zip 打包,只是后缀改成了 docx,真是天才。
Excel 也是类似的,特别是在大数据量下,用了 ZIP 压缩可以大大减少文件大小,相比 csv 文件来说,docx 和 xlsx 的文件大小可以小很多。

如果 Markdown 搞个扩展模式,例如 mdx, 也可以通过类似的方式把图片整合进去,感觉也很棒。

言归正传,我们打开 document.xml 文件,搜索一下正常的占位符文本(例如, 占位符 GT1 可以被正确的替换),会看到确实是没有分割:

<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia" w:ascii="宋体" w:hAnsi="宋体" w:cs="宋体"/>
<w:bCs/>
<w:sz w:val="21"/>
<w:szCs w:val="21"/>
</w:rPr>
<w:t>GT1</w:t>
</w:r>

而异常的占位符(例如, 占位符 GT2)被分割成了两部分:

<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
</w:rPr>
<w:t>GT</w:t>
</w:r>

<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:lang w:val="en-US" w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>2</w:t>
</w:r>

这就是为什么 GT1 能被正确替换,而 GT2 却无法替换的原因了。

解决方法

最简单直接的方法,就是删掉有问题的占位符,然后手动敲一遍。

⚠:千万不要尝试从别处复制黏贴字符串过来,然后修改某个字符。word 的复制黏贴会产生意想不到的格式变化,即便你复制黏贴过来清除格式,再应用格式也不行。一定要手动敲一遍。

我现在才理解为何发票之类的文件,默认都是采用 PDF 或者图片了。Word 的格式调整能浪费你一天时间。

Latex 会不会是更好的选择呢?

Word 虽然编辑起来方便,但是遇到类似的占位符替换问题,排查起来真的非常麻烦。

不知道 Latex 会不会是更好的选择呢?毕竟它是基于文本的,直接替换字符串就行了,不会有被拆分成多个 run 的问题。