写数学建模论文时,我遇到了一个很典型但很麻烦的问题:论文内容已经基本完成,模型、公式、结果都整理好了,但 Word 文档里的很多数学公式并没有真正渲染,而是以普通文本形式存在。

例如论文中有很多类似这样的表达:

max Z(α) = α·G(x)/G* + (1−α)·E(x)/E*

或者:

Ĝ = 7626 / 7629.6 = 0.9995

这些内容在数学上是对的,但在 Word 里只是普通文本,不是可编辑、可排版的公式对象。对于数学建模论文来说,这会直接影响最终观感:公式显得像草稿,打印效果不好,也不符合正式论文的排版习惯。我的论文正文中确实存在大量这种普通文本公式,比如 GDP 归一化贡献、就业归一化贡献、目标函数和约束条件等。

这篇文章记录一次完整的修复思路:如何不用手动一个个改公式,而是通过 Python 直接操作 .docx 底层结构,把普通文本公式批量转换成 Word 原生公式。


一、问题本质:不是 LaTeX 没渲染,而是公式根本不是公式

刚开始容易误以为这是“Word 不支持 LaTeX”或者“公式渲染失败”。

但真正的问题是:这些公式并没有放在 Word 的公式编辑区域里。

Word 里的公式和普通文本不一样。你按 Alt + = 输入的公式,会被 Word 保存成一种叫 OMML 的结构,也就是 Office Math Markup Language。它是 Word 原生数学公式的底层格式。

而论文里这些内容虽然看起来像公式,但其实只是普通字符:

G* = max G(x)
E* = max E(x)
max Z(α) = αĜ(x) + (1−α)Ê(x)

所以 Word 不会自动把它们变成公式。它们在视觉上只是文本,在底层也只是 <w:t> 文本节点。原论文中 6.5 节的归一化目标函数就存在这种情况。


二、为什么不建议手动修?

最直接的方法当然是手动修改:

  1. 在 Word 里按 Alt + =
  2. 输入公式
  3. 调整居中
  4. 删除原公式文本
  5. 重复几十次

但问题是,数学建模论文里的公式数量通常很多。我的这份论文中,从 5.3 到 11 节都有公式,包括投资上下限、分段线性函数、GDP/就业目标函数、Pareto 前沿、理想点距离、Monte Carlo 扰动公式和预算效率模型等。

手动改的问题有三个:

第一,耗时。几十个公式逐个修改,很容易花一两个小时。

第二,容易出错。比如 s_iks_{ik}E(x)e_iG*G^*,这些符号一旦手动输入错,论文模型就可能出现逻辑问题。

第三,格式不统一。手动改出来的公式,可能有的居中,有的不居中;有的大,有的小;有的前后间距正常,有的紧贴正文。

所以更好的方案是:让脚本直接批量生成 Word 原生公式。


三、docx 的本质:它其实是一个压缩包

.docx 是 Word 文件,这没错。

但从技术上看,.docx 实际上是一个 ZIP 压缩包。你把一个 .docx 文件改名成 .zip,然后解压,会看到很多 XML 文件。其中最重要的是:

word/document.xml

正文内容、段落、表格、部分公式信息,都在这个 XML 文件里。

所以自动修复公式的核心思路就是:

读取 docx
解压并读取 word/document.xml
定位普通文本公式所在段落
删除原来的普通文本公式
插入 Word 原生 OMML 公式结构
重新打包成新的 docx

这就是这个脚本的底层逻辑。

它不是在模拟鼠标键盘操作 Word,也不是截图,更不是把 Word 转成 Markdown 后再转回来,而是直接修改 Word 文件的底层 XML 结构。


四、OMML:Word 原生公式的底层语言

Word 公式不是以 LaTeX 保存的,而是以 OMML 保存的。

例如一个下标公式:

x_i

在 OMML 中会变成类似这样的结构:

<m:sSub>
  <m:e>
    <m:r><m:t>x</m:t></m:r>
  </m:e>
  <m:sub>
    <m:r><m:t>i</m:t></m:r>
  </m:sub>
</m:sSub>

其中:

m:sSub    表示下标
m:sSup    表示上标
m:sSubSup 表示上下标
m:f       表示分式
m:rad     表示根号
m:acc     表示帽子符号
m:oMath   表示一个数学公式

脚本里专门写了一组函数来生成这些结构,例如 mr_text() 生成数学文本节点,s_sub() 生成下标,s_sup() 生成上标,frac() 生成分式,acc_hat() 生成帽子符号,rad() 生成根号,math_rpr() 统一设置数学字体和字号。上传的脚本说明中也明确列出了这些函数对应的 OMML 元素。

这意味着脚本生成的不是图片,也不是伪公式,而是真正的 Word 原生公式。


五、核心代码思路:从线性公式解析到 OMML

脚本中最关键的类是:

class FormulaParser:

它是一个轻量级公式解析器,负责把类似下面的线性公式:

\max Z(\alpha)=\alpha\frac{G(x)}{G^*}+(1-\alpha)\frac{E(x)}{E^*}

解析成 Word 能识别的 OMML 结构。

它支持论文中常用的几类语法:

x_i
G^*
\sum_{i=1}^{10}
\frac{G(x)}{G^*}
\hat{G}(x)
\sqrt{(1-\hat{G}(x))^2+(1-\hat{E}(x))^2}

解析器的工作方式可以理解成逐字符扫描:

遇到普通字符,比如 xG+=,就生成普通数学文本节点。

遇到 _,就把后面的内容作为下标。

遇到 ^,就把后面的内容作为上标。

遇到 \frac,就解析分子和分母,然后生成分式结构。

遇到 \hat,就生成带帽子的数学符号。

遇到 \sqrt,就生成根号结构。

遇到 \alpha\Delta\xi\zeta,就转换为对应希腊字母。

所以它不是一个完整的 LaTeX 编译器,而是一个“够用、可控、针对论文公式定制”的轻量解析器。这个定位很重要。


六、为什么脚本选择按段落索引替换?

脚本里有一个非常关键的列表:

INDEX_REPLACEMENTS = [
    (137, [("5.3", r"L_i=300,\quad i=1,2,\ldots,10")]),
    (138, [("5.3", r"U_i=2500,\quad i=1,2,\ldots,10")]),
    ...
]

这表示:

第 137 个段落替换成 L_i=300...
第 138 个段落替换成 U_i=2500...

也就是说,脚本不是全文搜索 G(x)E(x) 这种字符串再替换,而是直接定位到具体段落。

这样做的优点是非常明显的:不会误伤正文。因为论文里可能多次出现 G(x)E(x)α,如果用全文替换,很容易把普通解释文字也变成公式。

但是它也有一个风险:段落索引依赖当前文档版本。

如果你后来在前面加了一段话,或者删了一段内容,第 137 段就可能不再是原来的公式段落。这时脚本可能会替换错位置。

所以这个脚本最适合用于“文档结构已经稳定,只剩公式显示需要修复”的阶段。


七、为什么从后往前替换?

脚本替换段落时用了倒序:

for idx, formulas in sorted(replacements, key=lambda item: item[0], reverse=True):

这不是随便写的,而是为了防止段落索引错位。

假设你从前往后替换,第 100 段如果被删掉并拆成多个公式段落,那么第 101、102、103 段的位置就可能发生变化。

而从后往前替换,后面的段落先处理,前面段落的编号不会受影响。这个细节很关键,说明脚本不是简单粗暴地替换文本,而是考虑了 XML 文档结构变化带来的影响。


八、生成公式段落:不仅要变成公式,还要排版好看

脚本里有一个函数:

def centered_math_para(linear: str, template_p: etree._Element | None = None) -> etree._Element:

这个函数负责生成新的公式段落。

它做了几件事:

  1. 复制原段落的部分格式;
  2. 设置公式段落居中;
  3. 设置公式上下间距;
  4. 插入 m:oMathPara
  5. 把解析好的公式放进去。

也就是说,脚本不是简单把文本变成公式,而是顺手做了基本排版,让独立公式看起来更像正式论文中的公式。


九、它还会生成转换报告

脚本运行完成后,不只输出新的 Word 文件,还会生成:

formula_fix_report.md

报告中会记录:

转换前 OMML 节点数
转换后 OMML 节点数
成功转换公式数量
未转换成功公式数量
是否检测到 m:oMath / m:oMathPara
是否存在 \sum、\frac、\hat、\Delta 等原始 LaTeX 残留

上传的脚本说明中也提到,最终会输出 paper_final_formula_fixed.docxformula_fix_report.md,并建议在 Word 中进行人工检查。

这个报告很重要,因为脚本“运行成功”不等于“论文完全没问题”。只有报告显示转换数量正常、未转换公式数量为 0,并且 Word 打开后公式能正常编辑,才算真正完成。


十、这个脚本的优点

这个方案最大的优点是:避开了 Word GUI,直接操作 docx 底层结构。

相比手动修,它更快。

相比截图公式,它可编辑、可搜索、打印清晰。

相比在线工具,它不会把论文上传到第三方网站。

相比 Word 批量转换,它更可控。

相比把文档转成 Markdown 再转回 Word,它更不容易破坏表格、图片和论文格式。

脚本的使用方式也很简单。根据脚本说明,只需要安装 lxml,把原始文档放到 03_output/paper_final.docx,运行脚本后会输出 03_output/paper_final_formula_fixed.docx04_logs/formula_fix_report.md

基本命令如下:

pip install lxml
python formula_fix.py

十一、但它不是万能工具

这里必须强调:这个脚本不能被神化。

它不是通用 LaTeX 转 Word 工具,而是针对当前论文版本写的定制化修复脚本。

它的主要限制有三个。

第一,解析器只支持论文中用到的公式语法。它可以处理上下标、分式、求和、根号、帽子符号、常见希腊字母,但不适合复杂矩阵、分段函数、大括号方程组、aligned 环境等。

第二,它依赖段落索引。如果 Word 文档内容顺序发生变化,INDEX_REPLACEMENTS 就需要重新校准。脚本说明中也明确提到,如果 paper_final.docx 的段落顺序变化,就需要调整索引。

第三,它只能校验 XML 是否有效,不能完全替代人工视觉检查。脚本可以检查 docx 是否损坏、XML 是否能解析、OMML 节点是否存在,但它无法保证 Word 打开后的每一个公式视觉效果都完美。

所以最稳妥的流程是:

运行脚本
查看 formula_fix_report.md
用 Microsoft Word 打开修复后的 docx
检查公式、目录、图表、页码
再导出 PDF

十二、我认为还应该做一个安全加固

这个脚本目前最值得改进的地方,是替换前校验。

现在它主要按段落索引替换。为了防止索引错位,最好给每个替换项增加一个 expected_text 字段。

比如不要只写:

(137, [("5.3", r"L_i=300,\quad i=1,2,\ldots,10")])

而是写成:

(137, "L_i = 300", [("5.3", r"L_i=300,\quad i=1,2,\ldots,10")])

替换前先检查第 137 段里是否真的包含:

L_i = 300

如果不包含,就不要替换,而是写入 missed 报告。

这样即使文档段落变化了,也不会误删正文。

这是工程上非常重要的一步:自动化脚本越强,越要有防误伤机制。


十三、这件事真正给我的启发

这次公式修复其实不只是一个 Word 排版问题,它反映了科研写作和工程自动化之间的关系。

很多时候,我们遇到的问题不是“不会写公式”,而是“科研产物在不同工具之间流转时,格式和结构丢失了”。

纯文本公式适合生成、复制、版本控制,但不适合正式论文排版。

Word 原生公式适合展示、打印、提交,但手动维护成本高。

截图公式看起来省事,但不可编辑、不可搜索、不可维护,不适合正式论文长期迭代。

所以真正好的方案,不是选择某一个工具,而是打通它们之间的结构转换:

普通文本公式
解析成结构化数学表达
生成 Word 原生 OMML
得到可编辑、可打印、可提交的正式论文

这就是这个脚本最有价值的地方。

它解决的不是一个孤立问题,而是论文自动化生产链条中的一个关键环节。


十四、最终总结

这次修复的核心思路可以总结为一句话:

不要把公式当成普通文本去美化,而要把它转换成 Word 真正认识的数学对象。

这个脚本的价值在于,它直接操作 .docx 底层 XML,把普通文本公式批量转换成 Word 原生 OMML 公式,从而兼顾了公式准确性、排版效果和可编辑性。

但它的正确使用前提是:

  1. 文档版本稳定;
  2. 段落索引准确;
  3. 转换后必须人工检查;
  4. 不要把它当成通用 LaTeX 编译器;
  5. 最好加入替换前文本校验,避免误伤正文。

对于数学建模论文、课程论文、科研报告这类包含大量公式的 Word 文档来说,这种方法非常有价值。它比手动修公式更高效,比截图公式更规范,比在线转换工具更可控,也更符合长期迭代的科研写作流程。

最终,我对这个脚本的评价是:

它不是万能工具,但它是一个非常实用的“定制化论文公式修复工具”。在当前论文版本固定、公式范围明确的情况下,它是一个高效、可靠、值得使用的方案。