ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

高效处理LaTeX论文修改:latexdiff使用全攻略

高效处理LaTeX论文修改:latexdiff使用全攻略

写论文改到第N版,最痛苦的不是改,而是改完之后还得告诉别人到底改了什么。尤其当文档用了LaTeX之后,普通文本比较工具基本帮不上忙。我第一次用latexdiff时,感觉像找到了救星:它能自动把两个LaTeX文件之间的增删改妥妥地标出来,生成一份可以直接编译的diff文档,谁是新增、谁是删除、哪句话被动过,一眼就能看明白。这篇东西就围绕latexdiff的使用全过程展开,从安装、跑通、调输出样式,到接进Git工作流和踩坑经验,全是我在实际项目中验证过的方法,适合写毕业论文、投稿返修、多人共同维护一份LaTeX文档的朋友。

1. 为什么论文修改记录要用latexdiff而不是肉眼对比

1.1 你需要处理的真实场景

大部分人的“版本管理”其实是这样:今天存一个main_20250101.tex,明天再存一个main_20250102.tex,改了几句话、换了两个公式、调整了一段表格,然后就再也没有人确切记得哪一版是“最终版”。

等导师、合作者问起来“你这次改了哪些内容”时,你就得手动打开两个PDF并排比对。眼睛扫过一整页,找出一两行不同,效率极低。这还不算最麻烦的——LaTeX文档里往往有一堆\newcommand、\ref、\input等命令,同一个句子只要改了引用的标签,肉眼看起来内容是没变的,可底层文本已经完全不同。普通diff工具会把一行LaTeX中断成好几段,输出结果惨不忍睹。

latexdiff要解决的问题很简单:让两个LaTeX源文件的差异变得可读、可编译、可分享。它不只告诉你哪一行变了,而是按照LaTeX的语法结构去理解文档,把“新增内容”“删除内容”“替换内容”分别用标记包起来,再输出成一个新的.tex文件。你用正常的编译流程处理这个新文件,就能得到一个带红绿标注的PDF,谁都能看懂。

1.2 普通diff和latexdiff的本质差别

很多人一开始不理解:明明是文本文件,直接用diff命令不就行了吗?

区别在于“行”和“语义”这两个层级完全不同。普通diff按行比较,适合程序源码、配置文件的版本追溯;但LaTeX的排版逻辑不是按行组织的。一个句子可以跨行写,一个\begin{equation}环境从第3行开始到第30行结束,中间任何一行改动都会引发大面积diff误报。

比较维度普通difflatexdiff
比较单位逐行逐词、逐句、按环境结构
对LaTeX命令的处理当普通文本拆开识别命令边界和参数
输出结果纯文本片段可编译的LaTeX文件
能否生成直观PDF不能能,且自动上色标注
处理多文件文档不支持支持通过--flatten展开子文件

这个差异在实际使用里特别明显。我用普通diff比较一份带表格的论文时,\和&这类符号被拆得到处都是,几乎无法判断真正的改动在哪里。换成latexdiff之后,所有差异都落到具体的词或段落上,引用、公式、环境这些结构都不会被误伤。

1.3 它解决什么,不解决什么

讲清楚边界,后面用起来才不慌。

latexdiff能解决的问题包括:修改了小段落、增删了句子、替换了关键词、重排了章节顺序、调整了公式内的细节,这些都能在输出里标出来。它的输出文件可以直接编译,也可以发给没装latexdiff的同事——对方只需要把它当成普通.tex编译即可。

但有些事它不负责。第一,它不会检查文档能否编译,如果新旧版本本身有语法错误,diff结果大概率也编译不过;第二,它不比较参考文献数据库(.bib文件)的内容,只能标出\cite命令引用条目的变化;第三,它不理解每个人自定义宏的语义,如果旧版本里有一个\foo新版本里被换成了\bar,它会标出所有相关位置的替换,但不保证替换后的命令仍然合法。

我习惯把它定位成“差异可视化工具”,而不是“LaTeX语义裁判”。真正决定哪些改动是合理的,还得靠自己审。

2. 装好latexdiff的几道坎:从依赖到命令不可用

2.1 不同发行版的安装路线

latexdiff本身是一组Perl脚本,不是必须编译的二进制程序,所以安装方式跟着你的LaTeX发行版走。

如果你用的是完整版TeX Live,一般已经自带latexdiff,可以直接在终端敲latexdiff -v验证。如果发现没有这个命令,通常是发行版安装时只装了基础方案。Linux下用系统包管理器装是最省事的:

sudo apt install texlive-extra-utils # Debian/Ubuntu sudo dnf install texlive-latexdiff # Fedora sudo pacman -S texlive-extra-utils # Arch Linux

macOS用户如果装了MacTeX,latexdiff也在其中;如果用的是BasicTeX,需要手动补上相应包。Windows用户一般装MiKTeX,在MiKTeX Console的包管理界面搜latexdiff就能直接安装。

我见过最隐蔽的情况是:命令能找到,但一运行就报Perl模块缺失。latexdiff依赖不少Perl库,某些精简版发行包装完脚本但不装依赖,需要用cpan把缺失模块补上。遇到报错时先看缺少哪个module,再针对性处理。

2.2 命令行不可用的排查顺序

如果latexdiff命令第一次运行就报“command not found”,我建议按下面顺序排查:

  1. 确认发行版包含latexdiff:kpsewhich latexdiff.pl,能输出路径说明文件在。
  2. 检查PATH环境变量:TeX发行版的bin目录是否在PATH里。Linux常见路径是/usr/bin,macOS用户可能需要eval "$(/usr/libexec/path_helper)"。
  3. 运行perl -v确认Perl存在。latexdiff是Perl脚本,没有Perl肯定跑不了。
  4. 执行latexdiff --help,如果能打印帮助,说明脚本本身没问题。

很多人卡在第二步。尤其Windows用户装了多个发行版,PATH里指向旧版TeX目录,导致新版带的latexdiff根本不会被找到。我自己调试过一台机器,装了好几版TeX环境,最后发现shell里生效的latexdiff来自一个五年前的精简包,更新PATH之后才正常。

2.3 --flatten依赖的latexpand

latexdiff有一个非常常用的参数--flatten,它能把\input、\include的子文件内容直接展开到主文件中,这样diff结果只有一个文件,处理起来特别干净。

但--flatten不是latexdiff单独完成的,它要调用一个叫latexpand的辅助脚本。如果系统里只有latexdiff而没有latexpand,运行时会报错。Debian系安装texlive-extra-utils时一般会一起装上,但某些自定义安装可能会漏掉。验证方法也很简单:

latexpand --version

没有这个命令的话,先解决它,再回来用latexdiff。我习惯在所有环境里都先跑一遍latexdiff --version和latexpand --version,两个都能出现,后面的流程才会顺。

3. 第一次完整跑通:old.tex与新.tex之间的差异标注流程

3.1 最简命令和编译流程

假设你手上有两个文件:old.tex是上一版,new.tex是改后版本,最基础的用法就一条命令:

latexdiff old.tex new.tex > diff.tex

执行完后,当前目录会多出一个diff.tex。这个文件就是融合了旧版和新版所有内容的“标注版”,你需要像编译普通LaTeX文档一样编译它:

pdflatex diff.tex pdflatex diff.tex

如果文档里有BibTeX,还需要额外跑一轮bibtex diff。为什么要编译两遍?因为diff.tex里不仅有你的原文内容,还包含一层差异标记命令,这些命令需要第一遍编译才能正确写入辅助文件,第二遍才稳定显示。

我第一次用的时候只顾着生成diff.tex,直接用旧版的编译脚本去跑,结果PDF里所有标记颜色都消失了,只看到文字,还以为命令写错了。后来才意识到,diff.tex的编译流程和原文档不完全一样,尤其是涉及\label、\ref、目录时,多编译一遍能解决很多莫名其妙的问题。

3.2 diff.tex内部发生了什么

把生成的diff.tex用文本编辑器打开,你会发现它的结构很有特点:导言区多出一整段“差异控制代码”,正文里的改动位置被包进了类似\DIFadd{...}和\DIFdel{...}的宏里。

举个例子,如果old.tex里写的是:

This is an old statement.

new.tex里改成:

This is a new statement.

那么diff.tex里大概会变成:

\DIFdelbegin \DIFdel{This is an old statement.}\DIFdelend \DIFaddbegin \DIFadd{This is a new statement.}\DIFaddend

\DIFadd表示新增,\DIFdel表示删除。默认样式里,新增内容用蓝色加下划线,删除内容用红色加删除线。这套宏定义都在导言区,所以diff.tex才能独立编译。

理解这个结构的主要意义在于:当你对默认标记不满,或出现编译冲突时,可以直接改动这些宏,而不是去改动正文里的差异标记。后面我会专门写一节怎么调样式。

3.3 多文件文档必须用--flatten

我的论文是主文件加多个\input子文件的结构,直接运行latexdiff main_old.tex main_new.tex > diff.tex,只会比较主文件里直接写的内容,子文件里的改动全部看不到。

后来才体会到--flatten的重要性:

latexdiff --flatten main_old.tex main_new.tex > diff.tex

这个参数会把所有被\input或\include引入的文件内容展开到主文件中,再统一做差异比较。这样即使改动发生在第二章的子文件里,也能被正确标出来。

不过要注意,--flatten生成的diff.tex会很大,因为它把子文件内容全部塞进来了。如果你只想比较某一个子文件,可以直接用latexdiff对那个子文件单独操作,没必要每次都展开整个论文。

3.4 一个小型示例实操

我常用下面这样一组微型文件来测试latexdiff环境是否正常。

old.tex:

\documentclass{article} \begin{document} Hello world. This is version one. \end{document}

new.tex:

\documentclass{article} \begin{document} Hello LaTeX world. This is version two. \end{document}

然后执行:

latexdiff old.tex new.tex > diff.tex pdflatex diff.tex

生成的PDF里,“LaTeX”和“version two”会用新增样式标出来,而“version one”会被标成删除。这一套流程能跑通,说明环境没问题,后面处理复杂文档就不会再被工具本身绊住。

4. 标注样式与输出类型:让diff结果符合投稿/合作要求

4.1 默认样式和两种定制思路

latexdiff默认是“删除线+下划线+颜色”的混合方案,适用于大部分内部协作场景。可如果要把diff结果发给导师或期刊编辑,有时需要更含蓄的标注方式,比如只变色、不画删除线。

最简单的改动方式是直接编辑diff.tex里导言区对\DIFadd和\DIFdel的宏定义。我的一般做法是先编译一次diff.tex,然后在生成的.tex里搜索DIFadd,找到类似这样的定义:

\providecommand{\DIFadd}[1]{{\protect\color{blue}#1}}

想改成红色作为新增色,可以追加重定义:

\renewcommand{\DIFadd}[1]{{\protect\color{red}#1}}

删除标注同理:

\renewcommand{\DIFdel}[1]{{\protect\color{gray}#1}}

注意顺序:重定义必须放在latexdiff生成的宏定义之后,否则会被覆盖。实际操作时我会把自定义放在\begin{document}前最后一块区域,确保覆盖生效。

4.2 常用--type标记类型

latexdiff的--type参数用来决定整体标记风格。我自己最常用的默认值是--type=CFLUA,也就是彩色+下划线+删除线的风格。如果你希望改动看起来更像“修订模式”,可以在生成diff.tex时直接指定:

latexdiff --type=CFLUA old.tex new.tex > diff.tex

除了CFLUA,还支持好几种不同风格,比如只上色不画删除线的版本、把整段改动当作一个块来标注的版本等等。具体支持哪些,以你当前版本的帮助输出为准:

latexdiff --help

我通常会把日常比较模板固定为CFLUA,只有遇到需要严格控制版面样式的场景才去调整。因为--type影响的是所有差异标记的顶层实现,换一次就要重新编译验证,比较费时间。

4.3 生成便于他人审阅的diff

如果diff结果要发给不太熟悉LaTeX的人看,PDF是更好的载体。但直接发diff.tex和编译后的PDF也有区别:diff.tex保留了所有差异宏,对方可以自己重新编译;PDF则是一张静态图,不容易进一步追踪。

我建议在这种场景下做两件事。第一,给diff.tex单独建一个工作目录,避免它和原文件的辅助文件混在一起;第二,在diff.tex的导言区加一行:

\renewcommand{\DIFdel}[1]{\textit{#1}}

这样删除内容不是被划掉,而是以斜体呈现,视觉效果更温和。配合颜色调整,能在“一眼看出改动”和“不刺眼”之间找到平衡。

5. 与Git和在线写作平台配合的自动化方案

5.1 latexdiff-vc直接对接版本库

如果你用Git管理LaTeX文档,那latexdiff还带了一个更省事的姊妹工具:latexdiff-vc。它能直接从Git仓库里取出指定版本,再自动生成diff文件,不需要你手动保存一堆old.tex和new.tex。

我习惯这样比较最近两次提交:

latexdiff-vc -r HEAD~1 -r HEAD --pdf main.tex

这条命令会比较HEAD~1和HEAD两个提交里的main.tex,生成diff文件并自动编译PDF。如果想比较某个历史提交和当前工作区:

latexdiff-vc -r HEAD~2 --pdf main.tex

这个方式最大的好处是可复现。版本号写进命令里,命令写进脚本里,每次想追溯某一轮修改,直接改版本号就行,不用到处翻历史文件。配合--flatten参数,多文件项目也能一次完成。

5.2 批量对比多个文件

有些项目不是单一主文件结构,而是拆分得很细。这时候我会写一个简单的shell循环,把每个子文件单独拉出来做diff:

for f in chapter1.tex chapter2.tex chapter3.tex; do latexdiff "old_$f" "new_$f" > "diff_$f" done

要注意的是,各个子文件的diff结果如果都要编译成PDF,宏定义和导言区会重复,直接整合到一个文档会有冲突。所以批量比较通常用来快速定位改动范围,而不是直接生成一个合并PDF。真要生成统一PDF,还是要回到主文件加--flatten的方案。

5.3 在线协作场景下的处理思路

现在很多人直接在某在线LaTeX协作平台上写作,平台自带类似“修订记录”的图形化功能,看起来比latexdiff更直观。但这类功能往往只能显示平台自己记录的变化,无法拿两个本地区文件做任意比较。

如果项目要长期离线备份,又得在协作平台给合作者展示差异,我会把latexdiff生成的PDF上传或转发过去。对方不需要装任何额外工具,甚至不需要懂LaTeX,打开PDF就能看到红绿标注。相比平台自带的修订逻辑,latexdiff的好处是不依赖某个特定平台的存储格式,只要手里有两个.tex文件,就能随时还原出一次完整的审阅记录。

6. 我踩过的坑:中文、宏包和数学公式的误报与规避

6.1 中文文档的编码与标点

中文用户用latexdiff最容易遇到的就是编码问题。现在写LaTeX基本都用UTF-8,两个文件编码一致时,latexdiff一般能正确处理中文。但如果你拿到的旧文件是GBK编码,新文件是UTF-8,那生成的diff.tex必然乱码。

我的习惯是先统一编码,再跑diff。Linux下可以用file命令查看编码,然后用iconv转换:

iconv -f GBK -t UTF-8 old.tex > old_utf8.tex

另外,中文标点和英文标点混用常常产生“假差异”。比如一个句子结尾从半角句号改成全角句号,latexdiff会认为整句发生了变化。这不算bug,但对审阅者来说很干扰。我的处理方法是把这种纯标点的小改动合并到下一次实质内容修改时一起比较,避免单独提交一次diff。

6.2 宏包顺序与自定义命令导致的编译失败

latexdiff在比较“宏”的时候很机械。如果新版文档调整了宏包的加载顺序,或者删除了某个宏包,而旧版文档里还在用那个宏包定义的命令,diff.tex编译时就会报“undefined control sequence”。

处理这类问题,我坚持一个原则:先用独立脚本确认old.tex和new.tex都能正常编译,再跑latexdiff。如果diff.tex编译失败,先看报错发生的位置,再判断是否与自定义宏有关。有些情况下可以手动在diff.tex导言区补上一段宏定义,比如:

\newcommand{\somecustom}[1]{...}

把新旧文档中共同生效的宏定义补齐,diff就能编过了。

还有一种常见情况:新版里把一个命令改名了,比如\result改成\res。latexdiff会标出所有替换位置,但生成的diff.tex中可能出现\res{...}在新版环境里合法、在旧版环境里不合法的情况。这时候我通常只在diff.tex里保留最核心的文字差异,把命令改名带来的噪音手动清理掉。

6.3 公式、图表和参考文献的假差异

数学公式是latexdiff最容易“过敏”的地方。一个公式只要稍微改变一个符号,它可能把整个公式标记为删除再加一个完整的新公式,导致PDF里出现大段红蓝区块,审阅起来非常吃力。

遇到公式密集的文档,我会尝试降低数学标记的敏感度。latexdiff提供了--math-markup参数,可以调节公式内差异标记的粒度,具体取值以当前版本的help为准。如果公式变化确实不重要,我甚至会生成一份不带公式差异的diff版本,只在正文区域标注修改,公式部分保持旧版原文。

图表环境也有自己的脾气。图的内容不会真的被diff,但图题、标签、\label命令的变化会被标出来。参考文献同理,biblatex条目变化需要单独看.bib文件,latexdiff能标出的只是\cite里面引用键的变化。遇到这种情况,我得手动去检查对应的参考文献条目,不能只看diff PDF就完事。

6.4 一份快速排查清单

症状常见原因处理方式
命令找不到发行版精简或PATH未配置安装texlive-extra-utils;检查PATH
--flatten报错缺少latexpand安装latexpand
中文乱码新旧文件编码不一致统一转为UTF-8
编译undefined宏包/命令被新版移除手动补全diff.tex导言区宏定义
公式大面积标红数学标记粒度过细调整--math-markup或忽略公式差异
子文件改动缺失没用--flatten改用--flatten比较主文件

这套清单是我每次帮人排查latexdiff问题时一定会过一遍的流程。八成以上的问题,最后都能落到这三件事上:环境没装完整、文件编码不统一、宏定义不兼容。

最后再分享一个小技巧:latexdiff生成的diff.tex不要和原始文件放在同一个目录里直接覆盖,我一般会新建一个diff_output/目录,把生成文件放进去。这样不仅避免clean脚本误删原始文件,还能把多轮修改的diff集中归档,哪天需要回看“第三轮修改到底改了哪些内容”,直接打开对应目录就行。工具本身并不复杂,真正有价值的是你把它用成工作流的一部分。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进