! LaTeX Error: Missing \begin{document}. 在 LaTeX 的错误里很少见:它是一条对自己撒谎的消息。会报这个错的文档,绝大多数都好端端地写着 \begin{document}。真相是——LaTeX 从来没有检查过 \begin{document} 在不在。它检查的只有「是否开始了一个段落」,并在导言区埋下机关,一旦段落开始就抛出这句话。所以这条错误正确的读法不是「缺少 \begin{document}」,而是「在 \begin{document} 之前出现了会被印出来的东西」。本页先在 LaTeX 内核源码里确认这个机关,再依次梳理三类原因:误入的正文、留在导言区的 \maketitle,以及一个看不见的字节。
这条消息为什么会撒谎
LaTeX 在导言区把 \@nodocument 装进了 \everypar,段落一开始它就会引爆。 \everypar 是 TeX 的钩子,每当一个段落开始就会执行;而段落开始的时刻,正是「出现了会被印出来的东西」的时刻。LaTeX 内核 latex.ltx 的相关几行里,还原样保留着开发者自己写的注释。
\gdef\@nodocument{%
\@latex@error{Missing \protect\begin{document}}\@ehd}
\everypar{\@nodocument} %% To get an error if text appears before the
\nullfont %% \begin{document}「为了在 \begin{document} 之前出现文字时报错」——注释说得明明白白。\begin{document} 的职责是拆掉这个机关,而机关本身根本没在找 \begin{document}。紧接着的 \nullfont 出自同一思路:整个导言区期间,当前字体是一个「一个字符都没有」的字体,这样即便不小心印了什么,纸面上也不会留下任何东西。修法因此也就定了:要找的不是 \begin{document},而是它之前那个会被印出来的东西。不过还是要确认一次那一行确实存在、拼写也没错——虽然少见,但 \begin{document} 真的缺失时,报的也是同一句话。看过这一眼之后,就不必再盯着它了。
日志会把那一个字符原样交给你
l.NN 那一行,恰好在肇事的那个字符之后折断。 TeX 把出错的行拆成「已读」和「未读」两半上下排列,所以上半行的末尾就是元凶本身。这也是为什么它看起来像把一个单词劈成了两截:在下面的例子里,第 3 行开头的 T 是第一个会被印出来的东西,它开启了一个段落,机关随即引爆。而这份文档的第 4 行上,\begin{document} 好端端地写着。
! LaTeX Error: Missing \begin{document}.
See the LaTeX manual or LaTeX Companion for explanation.
Type H <return> for immediate help.
...
l.3 T
his line is body text by mistake.在导言区里,什么算「会被印出来的东西」
导言区里只能放那些只「登记」某个值、不产生任何输出的命令。 \usepackage、\title、\author、\date、\newcommand 以及各类设置,都只是把信息存起来,所以不会开启段落。反过来,任何试图往纸上放东西的命令都会引发这个错误。分界线不是「看起来像不像正文」,而是「会不会产生输出」,因此才有了 \title{论文标题} 安全、而 \maketitle 危险这种乍看奇怪的区别。
- 正文误入导言区——
\documentclass下面残留着写了一半的句子,或者本想写成注释却忘了打%。这是最常见的原因。 \maketitle留在了导言区——把它和\title写在一起是高频失误。\maketitle应当放在\begin{document}之后。- 调用了会输出文字的宏——在导言区单独写了一个
\today,或者自己定义的那种「只是展开成一段字符串」的宏。定义是安全的,调用才危险。 - 多出一个符号——
\usepackage{amsmath}后面紧跟一个~,或者未闭合括号外遗留的字符。若是多出一个},报的则是! Too many }。
\documentclass{article}
\title{My Paper} % safe: records a value, prints nothing
\author{Me} % safe
\maketitle % ! LaTeX Error: Missing \begin{document}.
\begin{document}
\maketitle % this is where it belongs
\end{document}当罪魁是一个看不见的字节
如果导言区读了三遍都找不到会被印出来的东西,那么元凶极可能是不间断空格 U+00A0。 LaTeX 的 utf8enc.dfu 里有一行 \DeclareUnicodeCharacter{00A0}{\nobreakspace},把 U+00A0 定义成了一个会被印出来的空格。也就是说它是货真价实的输出,出现在导言区就会开启段落。而在屏幕上,它和普通空格根本分不出来。它混进来的方式再自然不过:从网页或 PDF 里复制的 \usepackage 行、Mac 上的 Option+Space、法语键盘布局等等。这种情况下,l.NN 上面那半行会呈现出一个显著特征——末尾看上去什么都没有。
| 字符 | 混入途径 | 放在导言区会怎样 |
|---|---|---|
U+00A0 | 不间断空格;从网页或 PDF 复制、Mac 的 Option+Space、法语键盘布局 | 会作为 \nobreakspace 印出来,因而报此错;各引擎表现一致 |
U+3000 | 全角空格;输入法还在中日文状态下敲的空格,或从中日文段落复制而来 | 在 xelatex、lualatex 上报此错;在 pdflatex 上报的是 Unicode character not set up |
U+FEFF | BOM;部分 Windows 编辑器保存 UTF-8 时会加在文件开头 | 在 TeX Live 2024 上,pdflatex、xelatex、lualatex 都会跳过它。不是原因 |
表格的最后一行否定了一条流传很广的建议。BOM 不是这个错误的原因。 在 TeX Live 2024 上实测,文件开头带 BOM 时,pdflatex、xelatex、lualatex 都能顺利通过,连警告都不给(即便 file 命令明确报告它是 UTF-8 (with BOM) text 也一样)。这正是「重新保存为无 BOM」这条建议常常无效的原因;与其花时间在这上面,不如去找 U+00A0。顺带一提,\usepackage[utf8]{inputenc} 现在也不必再写了——LaTeX News 28(2018 年)已把 UTF-8 定为默认输入编码。编码本身的处理见「编码与换行」那一页。
实际找出混进来的那个字节
导言区本来应该是纯 ASCII,所以机械地搜索「含有非 ASCII 字符的行」一下就能命中。 不过若对整个文件运行,中文或日文正文会行行命中,结果毫无用处。诀窍是先在 \begin{document} 处把文件截断。锁定行之后,用 sed -n l 查看这一行的字节——U+00A0 会显示成 \302\240,身份就此确定。剩下的只是把这两个字节重新敲成一个普通空格。
# list preamble lines that contain a non-ASCII character
sed -n '1,/begin{document}/p' paper.tex | LC_ALL=C grep -n '[^ -~]'
# reveal the bytes on a suspect line (U+00A0 shows up as \302\240)
sed -n '3p' paper.tex | sed -n l
# check whether the file carries a byte order mark
file paper.tex