写数学建模论文时,我遇到了一个很典型但很麻烦的问题:论文内容已经基本完成,模型、公式、结果都整理好了,但 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 节的归一化目标函数就存在这种情况。
二、为什么不建议手动修?
最直接的方法当然是手动修改:
- 在 Word 里按
Alt + = - 输入公式
- 调整居中
- 删除原公式文本
- 重复几十次
但问题是,数学建模论文里的公式数量通常很多。我的这份论文中,从 5.3 到 11 节都有公式,包括投资上下限、分段线性函数、GDP/就业目标函数、Pareto 前沿、理想点距离、Monte Carlo 扰动公式和预算效率模型等。
手动改的问题有三个:
第一,耗时。几十个公式逐个修改,很容易花一两个小时。
第二,容易出错。比如 s_ik 和 s_{ik},E(x) 和 e_i,G* 和 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}
解析器的工作方式可以理解成逐字符扫描:
遇到普通字符,比如 x、G、+、=,就生成普通数学文本节点。
遇到 _,就把后面的内容作为下标。
遇到 ^,就把后面的内容作为上标。
遇到 \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:
这个函数负责生成新的公式段落。
它做了几件事:
- 复制原段落的部分格式;
- 设置公式段落居中;
- 设置公式上下间距;
- 插入
m:oMathPara; - 把解析好的公式放进去。
也就是说,脚本不是简单把文本变成公式,而是顺手做了基本排版,让独立公式看起来更像正式论文中的公式。
九、它还会生成转换报告
脚本运行完成后,不只输出新的 Word 文件,还会生成:
formula_fix_report.md
报告中会记录:
转换前 OMML 节点数
转换后 OMML 节点数
成功转换公式数量
未转换成功公式数量
是否检测到 m:oMath / m:oMathPara
是否存在 \sum、\frac、\hat、\Delta 等原始 LaTeX 残留
上传的脚本说明中也提到,最终会输出 paper_final_formula_fixed.docx 和 formula_fix_report.md,并建议在 Word 中进行人工检查。
这个报告很重要,因为脚本“运行成功”不等于“论文完全没问题”。只有报告显示转换数量正常、未转换公式数量为 0,并且 Word 打开后公式能正常编辑,才算真正完成。
十、这个脚本的优点
这个方案最大的优点是:避开了 Word GUI,直接操作 docx 底层结构。
相比手动修,它更快。
相比截图公式,它可编辑、可搜索、打印清晰。
相比在线工具,它不会把论文上传到第三方网站。
相比 Word 批量转换,它更可控。
相比把文档转成 Markdown 再转回 Word,它更不容易破坏表格、图片和论文格式。
脚本的使用方式也很简单。根据脚本说明,只需要安装 lxml,把原始文档放到 03_output/paper_final.docx,运行脚本后会输出 03_output/paper_final_formula_fixed.docx 和 04_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 公式,从而兼顾了公式准确性、排版效果和可编辑性。
但它的正确使用前提是:
- 文档版本稳定;
- 段落索引准确;
- 转换后必须人工检查;
- 不要把它当成通用 LaTeX 编译器;
- 最好加入替换前文本校验,避免误伤正文。
对于数学建模论文、课程论文、科研报告这类包含大量公式的 Word 文档来说,这种方法非常有价值。它比手动修公式更高效,比截图公式更规范,比在线转换工具更可控,也更符合长期迭代的科研写作流程。
最终,我对这个脚本的评价是:
它不是万能工具,但它是一个非常实用的“定制化论文公式修复工具”。在当前论文版本固定、公式范围明确的情况下,它是一个高效、可靠、值得使用的方案。