写数学建模论文时,我遇到了一个问题:论文内容已经基本完成,模型、公式、结果都整理好了,但 Word 文档里的很多数学公式并没有真正渲染,而是以普通文本形式存在。
例如论文中有很多类似这样的表达:
max Z(α) = α·G(x)/G* + (1−α)·E(x)/E*或者:
Ĝ = 7626 / 7629.6 = 0.9995这些内容在数学上是对的,但在 Word 里只是普通文本,对于数学建模论文来说,这会直接影响最终观感:公式显得像草稿,不符合正式论文的排版习惯。
这篇文章记录一次完整的修复思路:如何不用手动一个个改公式,而是通过 Python 直接操作 .docx 底层结构,把普通文本公式批量转换成 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> 文本节点。
二、不手动修
最直接的方法当然是手动修改:
- 在 Word 里按
Alt + = - 输入公式
- 调整居中
- 删除原公式文本
- 重复几十次
但问题是,数学建模论文里的公式数量通常很多。我的这份论文包括投资上下限、分段线性函数、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 文件的底层 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
脚本中最关键的类是:
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,就转换为对应希腊字母。
六、脚本选择按段落索引替换的原因
脚本里有一个非常关键的列表:
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)、α,如果用全文替换,很容易把普通解释文字也变成公式。
但是它也有一个风险:段落索引依赖当前文档版本。 所以这个脚本最适合用于“文档结构已经稳定,只剩公式显示需要修复”的阶段。
七、从后往前替换
脚本替换段落时用了倒序:
for idx, formulas in sorted(replacements, key=lambda item: item[0], reverse=True):这是为了防止段落索引错位。
假设你从前往后替换,第 100 段如果被删掉并拆成多个公式段落,那么第 101、102、103 段的位置就可能发生变化。而从后往前替换,后面的段落先处理,前面段落的编号不会受影响。
八、生成公式段落:不仅要变成公式,还要排版好看
脚本里有一个函数:
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 中进行人工检查。
十、这个脚本的优点
这个方案最大的优点是:避开了 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十一、脚本的不足
它的主要限制有三个。
第一,解析器只支持论文中用到的公式语法。它可以处理上下标、分式、求和、根号、帽子符号、常见希腊字母,但不适合复杂矩阵、分段函数、大括号方程组、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 原生 OMML
↓
得到可编辑、可打印、可提交的正式论文十四、最终总结
使用前提:
- 文档版本稳定;
- 段落索引准确;
- 转换后必须人工检查;
- 不要把它当成通用 LaTeX 编译器;
- 最好加入替换前文本校验,避免误伤正文。
对于数学建模论文、课程论文、科研报告这类包含大量公式的 Word 文档来说,这种方法非常有价值。它比手动修公式更高效,比截图公式更规范,比在线转换工具更可控,也更符合长期迭代的科研写作流程。