golang 生成 word 文档,模板替换问题排查

文章目录

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

    我开始以为是代码的问题,检查了半天没有发现问题。
    然后,我又怀疑是这个三分库的 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)被分割成了两部分:

    word xml 分割

    <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 的问题。

    关于作者 🌱

    我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式