没有人会把一篇 300 页的学位论文写在单个 .tex 文件里。LaTeX 对大型文档给出的答案是三条命令——\input、\include 和 \includeonly——它们把项目拆成每章一个文件,再让你只重新编译正在写的那一章。有意思的是底下的机制:每排完一个 \include 进来的章,LaTeX 都会往这一章的 .aux 里写下一个 检查点,把当时每个计数器的值记下来,页码也在其中。跳过某一章之所以不会打乱其后各章的编号,靠的就是它。本页从目录结构讲起,途经几乎人人都踩过的相对路径陷阱,最后收在只有完整构建才会暴露的故障上。
大型 LaTeX 项目的目录结构怎么安排
起点只有一条:主文件里不写一行正文。main.tex 只放文档类、导言区和一串 \include,别的什么都没有。章放在 chapters/,图放在 figures/,文献数据库放在 bib/。这样一来 main.tex 本身就像一份目录,调整章节顺序不过是调换几行。同样的性质也让合著变得可行:各人改各人的文件,冲突自然少,Git 差异只落在你改的那一章里。导言区变长之后,把它拆到 preamble.tex,再用 \input{preamble} 读进来——导言区绝不能用 \include,原因下一节自会说明。
thesis/
main.tex
preamble.tex % packages and settings
chapters/01-intro.tex 02-method.tex 03-results.tex
figures/ % all images, next to main.tex
bib/refs.bib% main.tex -- no prose here, just structure
\documentclass[11pt,a4paper]{report}
\input{preamble}
\begin{document}
\tableofcontents
\include{chapters/01-intro}
\include{chapters/02-method}
\include{chapters/03-results}
\bibliographystyle{plain}
\bibliography{bib/refs}
\end{document}给文件名加上 01-、02- 这样的序号,编辑器的文件列表就会按阅读顺序排列。另一行值得加的是章文件开头的 % !TEX root = ../main.tex。TeXShop、TeXstudio、VS Code 等多数编辑器会读这一行,即便当前打开的是某一章,构建时仍然去编译 main.tex。没有它,早晚会单独编译某个章文件,然后撞上 ! LaTeX Error: Missing \begin{document}.——文件里没有 \documentclass,这个结果理所当然,可想明白之前照样会耗掉几分钟。
\input 与 \include 的区别
\input{f} 只是把 f.tex 的内容原样粘贴在那个位置,此外什么都不做。\include{f} 则是章一级的操作:前后各执行一次 \clearpage,而且——这才是关键——它会另开一个 f.aux,把辅助信息改写到那里。这份“每章一个 .aux”正是 \include 存在的全部理由。页码、交叉引用标签和目录行按章分开保存,于是日后跳过某章时,恰好可以把那一章的信息从上一次运行的结果里读回来。相反,\input 不留下任何文件边界的痕迹,因此比章更小的部件都归它管:加载导言区、共用宏、表格正文、反复使用的套话。
| 命令 | 作用 | 换页 | 可嵌套 |
|---|---|---|---|
\input | 在该位置展开某个 .tex 文件的内容 | 无 | 可以 |
\include | 按章引入,并拥有自己的 .aux | 前后各一次 \clearpage | 不可 |
\includeonly | 只能写在导言区;限定处理哪些 \include | — | — |
\subfile | subfiles 提供;该部分也能单独编译 | 无 | 可以 |
\subimport | import 提供;其中的相对路径以该目录为基准 | 无 | 可以 |
表格最右一栏是这一节里代价最高的差别。在一个被 \include 进来的文件里再写 \include,编译会停在 ! LaTeX Error: \include cannot be nested. 上。这看着像是随手加的限制,可从实现看却是必然:内核里为章的 .aux 只准备了一条输出流,内层的 \include 根本没有地方写自己的 .aux。所以要把一章再分成小节,就在章文件里用 \input{chapters/02-method/setup} 调用。还有一条:\include 也不能写在导言区,写了会得到警告 \include should only be used after \begin{document}。导言区之所以用 \input 读,原因正在这里。
还有一个不知道就会悄悄吃亏的不对称。对不存在的文件写 \input{chapters/ch9} 会停在 ! LaTeX Error: File ... not found.,可同样情况下 \include{chapters/ch9} 只往日志里写一句 No file chapters/ch9.tex.,然后若无其事地排完。也就是说,\include 的文件名打错了,得到的不是报错,而是整章凭空消失的 PDF。改过章节名之后,请养成在日志里搜索 No file 的习惯。
用 \includeonly 只编译一章——页码为什么不会乱
在导言区写上 \includeonly{chapters/02-method},就只处理这一条 \include,其余的跳过。原本要几分钟的完整构建几秒钟就结束,而且被跳过章节的页码和交叉引用依旧正确。诀窍分两层。其一,即便某章被跳过,LaTeX 仍会往 main.aux 里写下 \@input{chapters/01-intro.aux} 这一行——上一次运行的 .aux 总会被读回来,其中记录的 \newlabel 依然有效,\ref 因此仍能解析。其二,每排完一章,LaTeX 都会把此刻所有计数器的值追加到该章的 .aux 末尾。内核源码就把这条记录直呼为 检查点;跳过一章时只要把它重放一遍,页码、章号和图表编号就一步跳到“该章结束时”的位置。
% in the preamble of main.tex
\includeonly{chapters/02-method}
% several at once, comma separated, no spaces needed around the commas
% \includeonly{chapters/02-method,chapters/03-results}% chapters/01-intro.aux, written by the last full build (trimmed)
\newlabel{ch:intro}{{1}{2}{}{}{}}
\@setckpt{chapters/01-intro}{
\setcounter{page}{5}
\setcounter{chapter}{1}
\setcounter{figure}{0}
}正因为有这两层,部分构建出来的 PDF 比想象中更接近成品。连目录都不会坏:.toc 是在运行结束时由各个 .aux 写出的,所以被跳过的章仍会带着上一次的页码留在目录里。不过有一个前提必须守住:先做一次完整构建。跳过一个 .aux 尚不存在的章,它的引用就会停在 ??,日志里留下 LaTeX Warning: There were undefined references.。还有两个细节:\include 和 \includeonly 在比较名字前都会去掉结尾的 .tex,所以写成 \includeonly{chapters/02-method.tex} 也能匹配;而 \includeonly 只能出现在导言区,写在 \begin{document} 之后会得到 ! LaTeX Error: Can be used only in preamble.。提交前务必删掉这一行,重新完整构建所有章节。部分构建的 PDF 是工作中的近似,不是定稿。
相对路径为什么从主文件而不是章文件算起
\input 和 \include 都不会改变当前目录。TeX 把所有相对路径都相对于本次运行的工作目录来解析,而它通常就是 main.tex 所在的位置。所以写在 chapters/02-method.tex 里的图片路径,也要按从 main.tex 看过去的样子写。\includegraphics{figures/plot} 能用;而从章文件所在目录看似乎正确的 \includegraphics{../figures/plot} 会以 ! LaTeX Error: File ... not found. 告终。很多人在这里断定“把章挪了个位置,图就找不着了”。挪动的是文件,参照点从头到尾都没变,它一直是 main.tex。
补救办法有两种。常用的一种是 graphicx 的 \graphicspath,把要搜索的文件夹登记进去。它的写法很特别,值得记牢:每个文件夹各用一对花括号包住,且都要带结尾的斜杠——\graphicspath{{figures/}{chapters/figures/}}。此后任何一章都能写 \includegraphics{plot},文件夹和扩展名都省掉。路径分隔符即便在 Windows 上也用正斜杠。第二种适合让每章自带图片文件夹的项目:import 宏包的 \subimport{chapters/}{02-method} 会让该章内部的相对路径以 chapters/ 为基准解析。如果这一章日后可能被搬进别的项目,这种安排更便于移植。
% option A -- one shared figure folder, registered once in the preamble
\usepackage{graphicx}
\graphicspath{{figures/}{chapters/figures/}} % braces per folder, trailing slash
% then, anywhere in any chapter:
% \includegraphics[width=0.8\linewidth]{plot}
% option B -- each chapter carries its own figures
\usepackage{import}
% in main.tex, instead of \include{chapters/02-method}:
\subimport{chapters/}{02-method} % paths inside resolve from chapters/让单章也能独立编译:subfiles 与 standalone
\includeonly 是用来快速排出“整本中的一章”的工具,并不会把某章变成独立的 PDF。若想让章本身就是一份文档,就用 subfiles 宏包。在章文件开头写 \documentclass[../main]{subfiles},它就能借用主文件的导言区单独编译,同时主文件仍照旧用 \subfile{chapters/02-method} 把它嵌进正文。图那边也有同样的思路:按 standalone 类写成的 TikZ 图可以单独排成一页 PDF,主文档只需加上 \usepackage{standalone} 再普通地 \input 即可贴入。
% main.tex
\documentclass{report}
\usepackage{graphicx}
\usepackage{subfiles}
\begin{document}
\subfile{chapters/02-method}
\end{document}
% chapters/02-method.tex -- also compiles on its own
\documentclass[../main]{subfiles}
\begin{document}
\chapter{Method}
This chapter builds alone and inside the book.
\end{document}代价同样清楚。单独编译出来的章从第 1 页开始,也看不到别的章里定义的 \label,于是 \ref 变成 ??,日志里出现 LaTeX Warning: There were undefined references.。按用途选就好:想在保持全书编号的前提下跑得快,用 \includeonly;要把“第 3 章”单独交给导师,用 subfiles。最终以单个 PDF 提交的学位论文,配 \include 加 \includeonly 最顺;各章还要作为论文、讲义或讲义材料独立存在的项目,则更适合 subfiles。在同一个项目里两者混用,等于把导言区维护两遍,通常并不划算。
用 draft 选项加快试排
\documentclass[draft]{report} 在大文档试排时做两件事。其一,把每一行越出版心的地方——overfull hbox——用页边的黑色竖条标出来,断行的毛病一眼可见。其二,不再真正绘制图片,而用一个写着文件名的方框代替;省掉图像处理后编译明显轻快,图越多的章收益越大。若只想让图片进入草稿模式,用 \usepackage[draft]{graphicx} 缩小范围;反过来,若想照常显示图片、只标出溢出的行,设 \overfullrule=5pt 就只留下竖条。最终构建时别忘了把 draft 换回 final。
\documentclass[draft]{report} % skip images, show overfull rules
% scope it to images only:
% \usepackage[draft]{graphicx}
% keep images, still flag overfull lines:
% \overfullrule=5pt单章能编译、整本却失败时
这种症状的原因几乎逃不出四种。(1) 该章用到了只存在于它自己导言区里的宏包或命令——单独编译没事,放进整本就是 ! Undefined control sequence.。(2) 两章定义了同一个 \label,于是出现 LaTeX Warning: Label ... multiply defined.,而引用悄悄指向了别处。(3) 写了从章所在目录看才正确的相对路径,也就是上一节那个陷阱。(4) .aux 过期。其中 (2) 最危险,因为它不报错,只是印出错误的编号;给标签加上章名前缀,例如 \label{fig:method-setup},就能从结构上杜绝。
.aux 是怎么坏掉的,也值得记一记。中途打断构建,或者给某章改了名,都可能留下一个写了一半的 .aux。下一次运行读到它,就会在与你刚才所改之处毫无关系的行上翻车。凡是错误出现在你没动过的地方,先删掉生成文件再谈别的。手工删就是 .aux、.toc、.lof、.lot、.out——注意每章的 .aux 还躺在 chapters/ 里面;用 latexmk 的话,latexmk -c 清掉中间文件,latexmk -C 连输出一并清掉。删完之后跑两遍,让引用和目录稳定下来。
- 开始
\includeonly的工作之前,先完整构建一次,让每章都有新鲜的.aux。 - 沉重的 TikZ 图要么外部化,要么预先渲染成 PDF 再用
\includegraphics读入。 - 在每个章文件开头写上
% !TEX root = ../main.tex,无论打开哪个文件都去构建主文档。 - 除非使用
import,否则把figures/和bib/放在main.tex旁边,而不是章文件旁边。 - 给标签加上章的前缀,例如
fig:method-setup,从结构上杜绝multiply defined。 - 提交前移除
\includeonly和draft,删除生成文件做一次干净构建,并把日志读到底,留意Warning和No file。
最后说说节奏。长文档写得顺利的人,既不是每次都完整构建的人,也不是永远只构建片段的人,而是懂得交替的人。日常只用 \includeonly 反复打磨当前章节,保存后的重新构建交给 latexmk。到了节点,就撤掉 \includeonly 和 draft 排一遍全文,看编号、目录、索引和参考文献各就各位。提交前删掉生成文件做一次干净构建,把日志读到最后。拆分看起来只是为了快,其实同样是为了安心——它让“随时都能把整本正确地重排一遍”变成一件廉价的事。