断行与页面调整

LaTeX 的分页和断行的机制毫无相似之处。段落会一次性权衡所有可能的断法、挑出整体最好的一种;而页面一旦断开就不会回头重算。所以在没弄清「这一页为什么在这里结束」之前就敲下 \newpage,往往会把后面四十页一起拖下水。本页实测文档节奏背后的机制——widow 与 orphan(\widowpenalty\clubpenalty,默认值都是 150)、\raggedbottom\flushbottom 的区别,以及浮动体如何把正文挤走——并给出一套诊断糟糕分页的顺序,而不是硬来。

这一页为什么在这里结束——分页与断行的根本差别

一页在哪里结束,只取决于排这一页时已经掌握的信息。TeX 自上而下地灌入正文,盯住行与行之间出现的合法断点;一旦累积高度超过目标,它就退回到目前为止代价最小的候选点并在那里切断。段落内部的断行是对所有可能排布的全局最优化,而分页是一边前进一边做出的顺序决策。这个差别在实务上最要紧,也正是修改第 40 页的分页会让第 41 页之后全部错位的原因。

顺序决策也意味着事后没有讨价还价的余地。TeX 剩下的手段,只有用一个数字声明「有多不想在这里断」——那就是惩罚值(penalty)。惩罚值是附加在断点上的整数,值越大越不愿意在此断开。10000 是「绝不在此断开」的特殊值,-10000 则相反,表示「必须在此断开」。避免段落只剩一行被孤立的机制、安排浮动体位置的机制、禁止在标题之后立刻断开的机制,全都用这一种数字来表达。所以调整分页,实质上就是调整惩罚值

widow 与 orphan——\widowpenalty\clubpenalty 的默认值都是 150

article 类里实测,\widowpenalty\clubpenalty 都是 150\displaywidowpenalty50widow 指段落的最后一行被单独留在页面开头orphan 指段落的第一行被单独留在页面末尾。德语印刷界把二者分别叫作 Hurenkind 和 Schusterjunge。第一个绊住人的是命名:压制 orphan 的参数并不是 \orphanpenalty——TeX 和 LaTeX 里根本没有这个变量——而是 \clubpenalty,这是旧日排版行话把段首孤行称作 club line 留下的痕迹。

latex
% Print the defaults into the .log yourself:
%   pdflatex penalties.tex  ->  grep MEASURE penalties.log
\documentclass{article}
\begin{document}
\typeout{MEASURE widowpenalty=\the\widowpenalty}
\typeout{MEASURE clubpenalty=\the\clubpenalty}
\typeout{MEASURE displaywidowpenalty=\the\displaywidowpenalty}
\typeout{MEASURE brokenpenalty=\the\brokenpenalty}
x
\end{document}

150 这个数字有来历。在 TeX Live 2024 的 latex.ltx 第 514–516 行可以看到这三条赋值,而完全相同的三行也出现在 Knuth 的 plain.tex 第 290–292 行。也就是说,LaTeX 只是原封不动继承了 plain TeX 的默认值,四十年来没有重新审视过。而 150 在实务上几乎不起作用:在垂直方向几乎没有可伸缩空白的正文里,把一页缩短一行所带来的丑陋程度(badness)轻易就会超过 1000。150 的惩罚值在这场较量里基本赢不了。

参数默认值阻止什么
\widowpenalty150段落最后一行单独落在页首
\clubpenalty150段落第一行单独留在页尾
\displaywidowpenalty50在行间公式之前只剩一行
\brokenpenalty100在带连字符的行之后分页
\interlinepenalty0在段落中间任意处分页(默认不加阻止)
\predisplaypenalty10000在行间公式之前分页(实际上被禁止)

只有一个地方,LaTeX 自己放弃了这些默认值。article.clsthebibliography 环境一进入就调用 \sloppy,并设置 \clubpenalty4000\widowpenalty4000(TeX Live 2024 所带 article.cls 的第 576–579 行)。参考文献表是一长串短条目,容易出现 widow,而一个条目被拆到两页上确实难读——标准类唯独在这里认真起来,原因就在于此。反过来说也成立:在正文里,标准类什么忙也帮不上。

\widowpenalty 设为 10000 会发生什么——实测这笔交易

widow 消失了,取而代之的是一页变短。\flushbottom 排一份每页 20 行、每段 6 行的文档实测:保持默认时,第 1 页排入 23 行,第四段的最后一行被单独留在第 2 页开头。加上 \widowpenalty=10000 之后,第 1 页减到 22 行,最后两行一起挪到下一页,widow 不见了。但日志里多出一行:

log
Underfull \vbox (badness 10000) has occurred while \output is active []

这条警告的意思是「没能把页面的纵向空隙填满」,而且只在 \flushbottom 下出现。同样的实验换成 \raggedbottom 就一次也不会报——页面允许不排到底,也就没有需要填的东西。也就是说,\widowpenalty=10000 的代价表现为:\flushbottom 下行距被拉开而参差不齐,在 \raggedbottom 下页脚的空白伸缩不定。无论哪种,代价都不为零。即便如此,对于以正文为主的稿件,10000 仍是站得住脚的设置:读者一定会注意到孤零零的一行,却几乎没人会注意到某页少排了一行。

latex
% A defensible pair of settings for a text-heavy manuscript.
\widowpenalty=10000
\clubpenalty=10000
% Also stop a two-line gap opening before a displayed formula:
\displaywidowpenalty=10000
% Prefer this over sprinkling \newpage through the source.

\raggedbottom\flushbottom 的区别,以及你现在用的是哪一个

\flushbottom 会伸缩纵向空白,让所有页面的底边对齐;\raggedbottom 则允许底边参差。很多资料在这里说错了:单面排版的 article,默认是 \raggedbottom 而不是 \flushbottom article.clsreport.clsbook.cls 末尾都会执行 \if@twoside \else \raggedbottom \fi,所以只要没指定 twoside,生效的就是 \raggedbottom。只有默认就是 twosidebook 留在 flush-bottom 那一侧。而一旦指定双栏,这几个类都会显式调用 \flushbottom

类选项实际生效的是页脚的样子
\documentclass{article}\raggedbottom每页底边高度不同
\documentclass[twoside]{article}\flushbottom底边对齐;行距可能被拉开
\documentclass{book}\flushbottombook 默认就是 twoside,所以落在这一侧
\documentclass[twocolumn]{article}\flushbottom由类显式调用;双栏不齐会很显眼

机制简单得几乎让人失望。在 latex.ltx 里,\raggedbottom 只是被定义为 \def\@textbottom{\vskip \z@ \@plus.0001fil}\flushbottom 则是 \let\@textbottom\relax。前者在页面最下方放了一段「长度为零但可无限伸展」的胶水,这个 fil 会压倒正文中那点有限的伸缩量,于是所有余量都被页脚吸收。让 TeX 输出 \meaning\@textbottomarticle 会回答 macro:->\vskip \z@ \@plus .0001fil,而 book 回答 \relax——在导言区写一行就能当场确认自己的文档处于哪种模式。

latex
% Which bottom mode am I in? Put this in the preamble and read the .log.
\makeatletter
\AtBeginDocument{\typeout{MEASURE textbottom=\meaning\@textbottom}}
\makeatother
% article  -> macro:->\vskip \z@ \@plus .0001fil   (\raggedbottom)
% book     -> \relax                                (\flushbottom)

当浮动体把正文挤走——\topfraction\textfraction

当分页看起来莫名其妙时,罪魁祸首通常是浮动体。放置图表之前,LaTeX 会检查它在这一页上允许占多大比例、已经放了几个。标准类的实测值是:顶部浮动体最多占正文高度的 \topfraction = .7,底部最多 \bottomfraction = .3,正文至少要留下 \textfraction = .2,一页上的浮动体总数不超过 totalnumber = 3(顶部 topnumber = 2,底部 bottomnumber = 1)。只要有一条不满足,这张图就不会放在本页,而是推迟到后面的页面。被推迟的浮动体会排在其后正文之前,于是就出现了「文字在第 12 页、图在第 14 页」这种错位。

参数默认值含义
\topfraction.7顶部浮动体可占正文高度的比例
\bottomfraction.3底部浮动体可占的比例
\textfraction.2带浮动体的页面上必须留给正文的最小比例
\floatpagefraction.5允许生成纯浮动体页面所需的最小占满率
totalnumber3一页上允许的浮动体总数
topnumber2页面顶部允许的浮动体数量
bottomnumber1页面底部允许的浮动体数量

浮动体作祟时,日志会先告诉你。若只写了 [h] 的图放不下,LaTeX 会提示 LaTeX Warning: 'h' float specifier changed to 'ht'. 并自作主张把它挪到顶部。若积压的浮动体太多,就会以 ! LaTeX Error: Too many unprocessed floats.latex.ltx 中的 \@fltovf)停下来。实务上的常规做法是:在章节交界处放上 placeins\FloatBarrier,把图限制在所属章节之内;或使用 afterpage 宏包的 \afterpage{\clearpage},在当前页排完之后再清空队列。直接就地写 \clearpage,会在正文中间留下一大片空白。

latex
\usepackage{placeins}   % \FloatBarrier
\usepackage{afterpage}  % \afterpage{\clearpage}

% Keep every figure inside the section that discusses it:
\let\oldsection\section
\renewcommand\section{\FloatBarrier\oldsection}

% Or loosen the rules instead of forcing breaks:
\renewcommand{\topfraction}{0.85}
\renewcommand{\textfraction}{0.1}
\renewcommand{\floatpagefraction}{0.75}

糟糕的分页怎么修——动手硬来之前先做什么

这是有先后顺序的:先定位原因,再修改全局设置,最后才动手处理那唯一还不对的一页。 顺序反过来,上游只要多加一句话,所有手工调整就前功尽弃。定位从 .log 开始:出现 Underfull \vbox 说明 \flushbottom 和惩罚值在互相拉扯;出现 float specifier changed 说明是浮动体;成串的 Overfull \hbox 则说明问题在断行那一侧,那是另一页的地盘。弄清原因之后,再决定该动惩罚值、浮动体比例,还是段落本身的长度。

  • 先读 .log。出现的是 Underfull \vboxfloat specifier changed 还是 Overfull \hbox,决定了谁是元凶。
  • 在导言区提高 \widowpenalty\clubpenalty。因为作用于整篇文档,修好的地方不会复发。
  • 若是浮动体的问题,用 \FloatBarrier 或调整 \topfraction。把位置说明放宽为 [htbp] 同样有效。
  • 把段落本身缩短或加长一行。改动一处措辞,往往才是最耐久的修法。
  • \enlargethispage{\baselineskip} 只让这一页多容纳一行。到这一步才算动手。
  • \newpage\pagebreak 是最后手段。一旦插入,每次改稿都得重新确认位置。

要请 TeX「把段落缩短一行」,工具是 \looseness。在段落内部写 \looseness=-1 表示「若可能就少排一行」,+1 则是多排一行。不过实测的结果发人深省:在设置了 \tolerance=9999\emergencystretch=3em 的段落里,\looseness=1 确实把 6 行拉成了 7 行,代价是冒出 6 条 Underfull \hbox。而 \looseness=-1——恰恰是大家真正想要的方向——完全没能改变行数,因为在 \tolerance 允许的范围内根本不存在更短的解。\looseness 是一步值得一试的棋,而不是一定奏效的杠杆。它不起作用时,老老实实从稿子里删掉一个词反而更快。

latex
% Ask for one line fewer inside the paragraph itself.
% \looseness is reset by every \par, so it must sit inside the paragraph.
\tolerance=9999 \emergencystretch=3em
Some paragraph text ... \looseness=-1

% Only after the above has failed: one page, one line taller.
\enlargethispage{\baselineskip}

\pagebreak\linebreak\- 在哪里讲

本页处理的是整篇文档的节奏,具体的断开命令由别的页面负责。页面一侧的 \pagebreak[n] / \nopagebreak[n](n 为 0–4)、无条件的 \newpage、连同浮动体一起清出的 \clearpage,以及双面排版时补空白页的 \cleardoublepage,请看「分页微调」。行的一侧 \\\newline\linebreak[n]\nolinebreak[n]、任意连字符 \-、导言区的 \hyphenation{man-u-script}hyphenat 宏包,请看「断行微调」。段落本身的结构与 \par 归「段落与换行」所有。而 Overfull \hbox\sloppy\emergencystretch 有专门的错误页面,那里也实测出 \sloppy 不过是四条赋值(\tolerance 9999\emergencystretch 3em\hfuzz .5\p@\vfuzz\hfuzz)。

最后,这一族里有一个成员,LaTeX 早已不声不响地替你用上了\section 之类的标题命令,会把紧随其后的那一段的 \clubpenalty 临时提到 10000,让紧接标题之后不可能分页——这正是 latex.ltx\@clubpenalty 那套机制的用途。所以「标题孤零零留在页尾」这种事故,在原装 LaTeX 里基本不会发生。真发生了,那多半是有人用自制宏替换了标题命令。