ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LaTeX编译报错:empty literal stack 与 hyperref 状态失衡全解析

LaTeX编译报错:empty literal stack 与 hyperref 状态失衡全解析 如果你正在编译 LaTeX 文档时突然看到一行! You cant pop an empty literal stack for entry xxxxx然后编译中断大概率会一脸懵这行报错既没有告诉你发生在第几行也没有指向你写的哪条命令只丢给你一个看起来很像标签名的xxxxx。这个报错我前前后后踩过好几次折腾多了就发现它其实是 LaTeX 世界里非常经典的一类宏包状态报错——核心问题几乎都集中在hyperref身上真正要排查的往往不是你“写错了什么”而是宏包之间、宏包版本之间、以及 PDF 底层对象管理的配合出了问题。这篇文章就围绕这个报错把原理、触发场景、排查流程和实测有效的修复方案一次说清楚。1. 一个莫名其妙的报错我先说现象1.1 报错出现时的现场先说一个我印象很深的现场。当时我在写一份带大量交叉引用的技术报告导言区里加载了hyperref正文里有各种\ref、\href、\label。编译到接近尾声也就是处理到\end{document}附近时控制台突然弹出! You cant pop an empty literal stack for entry appendix-A. \Hyliteralpop ...op an empty literal stack for entry #1 l.123 \end{document}注意看前面的 label 名是appendix-A但报错位置指向\end{document}。这就很迷惑我的\end{document}又没有写\label{appendix-A}凭什么说这个 entry 有问题更离谱的是我单独编译一个只有一章内容的最小例子完全没问题一旦把几个宏包组合起来错误就稳定复现。这个报错还有一个特点它可能出现也可能不出现。有时候清理一次辅助文件就消失了有时候改一个宏包顺序又能编译过去。这种“玄学”感才是它最让人头疼的地方。1.2 为什么我一开始没找到头绪第一次遇到时我的第一反应是怀疑自己某个\label写错了位置于是全局搜索appendix-A发现它在附录的一个章节标题下面看起来很正常。我又怀疑是\ref引用了不存在的标签结果也不是。折腾了半天最后发现罪魁祸首跟我的正文内容毫无关系而是hyperref内部的一段状态管理逻辑在\end{document}时崩了。这里要纠正一个直觉LaTeX 的报错并不总是直接指向“你写错的那一行”。很多报错是在宏包收尾时才爆发的尤其是hyperref这种需要跟 PDF 底层交互的宏包。它平时会记下一大堆状态等到文档结束时才统一检查发现不平衡就直接抛错。所以如果你也遇到类似情况第一步不是怀疑自己打错了字而是去搞清楚这个报错背后的机制。2. 拆开“literal stack”这个黑话2.1 hyperref 是怎么给 PDF“画”链接的要弄懂这个报错得先理解hyperref在底层做了什么。LaTeX 源文件编译成 PDF 时链接并不是“天然存在”的。你用\href写了一个超链接PDF 文件里必须有一段描述告诉阅读器从页面上的这块区域开始点击之后跳转到某个目的地。在 pdfTeX 引擎里这个动作靠两个原语完成\pdfstartlink和\pdfendlink。一个开始、一个结束必须成对出现。hyperref的工作就是把你写的\href、\ref、\label这些高层命令翻译成这一对原语。比如说你在文档里写\href{https://example.com}{点击这里}hyperref内部会大致把它展开成\pdfstartlink ... 点击这里 \pdfendlink如果只调用了\pdfstartlink忘记\pdfendlinkPDF 里的链接就会一直处于“未闭合”状态轻则点击区域异常重则直接编译报错。2.2 为什么必须要一个栈正常情况下\pdfstartlink和\pdfendlink是一一配对的就像写代码时开括号和闭括号一样一开一关即可。问题是LaTeX 文档里的链接并不总能在原地“顺手关上”。一个链接可能跨越段落、跨越表格行、甚至跨越页面。更复杂的是hyperref内部还有一些“延迟处理”的逻辑比如在页面输出时把某些 PDF 对象放到合适的位置这种时候就不能立刻关闭链接。于是hyperref用了一个栈来管理这些状态。你可以把栈想象成一叠盘子每遇到一个需要延后处理的链接就往栈顶“压入”一个记录等条件满足、可以关闭时再从栈顶“弹出”。如果代码逻辑正常压入和弹出的次数完全一样一旦某个宏包或自定义命令破坏了这种平衡就会出现“想弹出一个记录但栈里根本没有东西”的情况。报错信息里的literal stack指的就是这个栈。literal不是“字面量”的意思而是指 PDF 里跟链接、锚点相关的字面对象literal object。提示这个报错的本质是状态失衡而不是内容错误。你可以把栈想象成一个括号匹配问题整套文档的\pdfstartlink和\pdfendlink必须配对否则编译器就会在最后阶段提出抗议。2.3 报错里那个 xxxxx 到底是什么报错原话中的for entry xxxxx是最关键的线索。这里的xxxxx是一个 entry 名也就是hyperref内部记录的锚点名通常就是我们用\label定义的标签名。它不一定 100% 等于你写的标签有时hyperref会生成一些形如section.1、page.3的内部名字但绝大多数情况下它会对应到某个你可以在源文件里搜索到的\label{...}。为什么会给出这个名字因为hyperref在弹出栈失败时需要告诉用户“我本来在处理哪个 entry”。它记录了这个 entry 的名字方便你往回查。所以记住看到这个报错第一件事就是把xxxxx记下来然后在源文件里搜索它。它就是你定位问题的起点。3. 触发报错最常见的几类场景3.1 宏包版本错位一段绕不开的历史这个报错有一个非常著名的爆发期就是 2020 年前后。那段时间 TeX Live 里的expl3/l3kernel更新了一次结果跟hyperref的配合出了问题。很多用户反馈出现了几乎一模一样的You cant pop an empty literal stack for entry ...错误。后来hyperref很快发布了修复版本这个错误才逐渐平息。这段历史给我们的启示是如果你用的是较老的 TeX Live或者长时间没有更新宏包遇到这个报错时优先怀疑“版本错位”。不需要去研究自己写的代码因为很可能根本不是你的问题只需要把hyperref更新到最新版或者把整个 TeX 发行版升级一遍报错就自然消失了。我当时也踩过这个坑。某个旧项目的hyperref版本停留在很老的状态新装的系统里l3kernel已经是新版本结果一编译就报这个错。更新hyperref之后立刻正常。所以这个报错的第一条排查原则先看宏包版本再怀疑自己的代码。3.2 宏包交互beamer、longtable、footnotetext 们的恩怨版本问题之外另一大类触发场景是宏包交互。有些宏包会在内部改变hyperref的状态导致栈的压入和弹出失衡。我自己遇到过几个典型beamer配合hyperref时如果某个 frame 里用了\only、\pause这些叠加命令又同时在 frame 标题上放了超链接就比较容易触发。因为beamer的叠加机制会让同一段内容被排版多次hyperref的状态就乱了。longtable跨页表格里如果某个单元格包含\href或\hyperlink表格在断页时可能把“开始链接”和“结束链接”拆到不同页面栈就失衡了。footmisc或自定义脚注内容里嵌入链接处理脚注文本时会进入一个延迟盒子链接的闭合时机被改变也会出问题。这些场景的共同点是链接的生命周期被某个宏包拉长或者打断。链接本身没有错但开始和结束的位置被人为分开了hyperref在关闭链接时发现栈里已经没有对应的记录。如果你用到的宏包组合恰好命中这类情况不用死磕具体是哪个宏包改了什么直接采用后面的二分注释法很快就能锁定。3.3 自己写的命令和“位置不对”的 \label第三种场景比较常见也比较容易避免自己写的宏或者在链接内部放了不该放的东西。比如这种写法\newcommand{\mylink}[1]{% \href{https://example.com}{#1}\label{mylink-label}% }然后你在正文里多次调用\mylink。表面看没问题但\label跟在\href后面hyperref在处理这个 label 时可能正好处于一个链接尚未完全结束的状态栈的记录就被打乱。还有一种情况是章节标题里使用\footnotehyperref在生成书签、目录时会把标题里的内容再处理一遍脚注生成的盒子会让链接对象处于一个奇怪的位置。我自己试过在\section标题里写脚注编译到最后稳定复现这个报错把脚注移出去就没事了。所以记住一个口诀链接里不要塞\label标题里不要藏脚注。这些写法可能在视觉上没问题但会给底层的状态管理埋雷。3.4 对象流压缩一个看得到却摸不着的变量最后一个很隐蔽的触发因素是 PDF 的对象流压缩对应到 pdfTeX 里就是\pdfobjcompresslevel这个参数。PDF 文件由很多“对象”组成比如字体、页面、链接目的地。为了减小文件体积pdfTeX 可以把这些对象压缩到一起所以\pdfobjcompresslevel默认是开着的通常是2。在某些hyperref版本里开启对象流压缩后链接目的地的创建和关闭顺序会被打乱表现为这个empty literal stack报错。这个因素之所以“看得到却摸不着”是因为它不体现在你的 LaTeX 源码里而是由编译引擎默认决定的。如果你排查了一圈宏包都没找到问题尝试在导言区把对象流压缩关掉往往能立竿见影。还有一些类似的经验是设置\pdfcompresslevel0这个参数影响的是页面内容流压缩。实际测试中两者都可能对hyperref的栈管理产生影响优先尝试\pdfobjcompresslevel0。注意\pdfobjcompresslevel和\pdfcompresslevel是 pdfTeX 引擎独有的参数如果你用 luaLaTeX 编译这个命令可能不生效需要另找方案。这是很多人在网上照搬代码后无效的原因。4. 从报错到定位元凶的完整排查流程4.1 第一步把 .log 文件的上下文拉出来遇到这个报错不要急着改代码先把.log文件打开找到报错位置附近的内容。.log文件里会记录宏包加载顺序、版本号、以及报错发生时的上下文。我习惯重点看三个东西报错前最后几条消息是什么。有时候能直接看到是哪个宏包在输出时出了问题。hyperref的版本号。如果版本偏旧优先更新。报错信息里entry后面的名字也就是我们要找的那个标签。比如日志里如果写着entry section.2那就可以在源文件里搜索section.2看它在哪定义的。如果搜索不到再搜索它对应的章节名判断是否在标题、\caption、\footnote这些特殊位置。这个习惯非常重要。很多人看到报错直接去正文里找问题结果绕了一大圈。实际上.log文件里已经给了你线索只是需要静下来读一遍。4.2 第二步二分注释法如果日志里的线索不够直接下一步就是二分注释法。这个方法几乎所有 LaTeX 用户都会但遇到这种“位置不定、随机爆发”的报错时特别管用。做法是把当前文档分成两半注释掉一半的内容重新编译。如果报错消失说明问题在被注释掉的那一半里如果报错还在说明问题在保留的这一半里。然后继续对有问题的一半再二分直到定位到具体的章节、环境或宏包调用。我自己的经验是别按“章节”来二分按“宏包”来二分可能更快。因为这个报错经常跟宏包交互有关你可以先注释掉一半宏包看报错是否消失。当然直接注释宏包可能导致其他代码编译不过这时可以新建一个最小文档逐步加入宏包和内容直到复现报错。二分注释法虽然笨但极其可靠。它会把你从“瞎猜”变成“定位”这是排查这类状态类报错最重要的思路。4.3 第三步把根因分门别类等你定位到具体位置之后再看看这个位置属于哪一类处理方式完全不同定位结果判断方向处理思路报错跟某个旧宏包相关版本错位更新宏包或 TeX Live报错出现在 beamer / longtable / footmisc 环境宏包交互调整用法避免链接跨页或跨 overlay报错出现在自定义命令里的\href\label代码写法把\label移出链接报错在\end{document}附近且查不出内容问题对象流压缩尝试关闭\pdfobjcompresslevel这一步的关键是不要急。很多人到这里会因为“实在找不到”而开始盲目添加各种 hack结果越改越乱。按这个分类表对号入座基本不会跑偏。5. 实测有效的修复方案与避坑记录5.1 更新 hyperref 或整个 TeX 发行版最简单的方案也最优先尝试把hyperref更新到最新版。如果你用的是 TeX Live在命令行里执行tlmgr update hyperref也可以把 LaTeX 相关的宏包都更新一遍tlmgr update --all如果你用的是 MiKTeX可以直接打开 MiKTeX Console在 Updates 页面里检查并更新。我自己遇到过的情况是2020 年前后那段时间hyperref跟l3kernel的版本错位非常普遍很多人的电脑上其实已经有新版宏包只是编译时没走对路径。更新完之后报错就像没发生过一样。所以不要嫌这个方案简单它治好了我至少一半的类似问题。提示更新宏包后一定记得清理旧辅助文件。.aux、.out、.toc这些文件里残留了旧的链接状态不清理的话可能还会继续报错。5.2 关闭 PDF 对象流压缩如果更新之后问题依旧第二个实测有效的方案是在导言区里关闭 PDF 对象流压缩。写法是\pdfobjcompresslevel0把这行加在\usepackage{hyperref}之前。说直白点让hyperref在写完链接相关信息后这些信息不被塞进对象流里压缩从而避免栈的压入弹出被干扰。注意如果你用的是luaLaTeX这行代码不生效。这时候可以试试\pdfvariable objcompresslevel0在 luaLaTeX 的环境下一般不需要改这个因为 LuaTeX 的底层逻辑跟 pdfTeX 已经不一样了这个报错在新版本 TeX Live 里也很少出现。但如果你必须用老版本还是值得一试。这个方案看起来像“治标不治本”实际上对一些hyperref老版本来说它就是本。对象流压缩是 PDF 层面的优化它跟hyperref的栈管理在理论上不该互相干扰但现实就是某些版本组合下两者会打架。与其研究底层不如先关掉这个优化文档照样能用只是 PDF 文件体积略微变大。5.3 调整宏包顺序和标签写法第三类方案也是日常写作中更值得养成的习惯调整宏包加载顺序规范标签写法。hyperref有一个不成文的规矩尽量放在导言区靠后的位置尤其不要放在某些会改变字体、版面、脚注格式的宏包之前。虽然现代hyperref已经比早年健壮很多但顺序不对仍然可能引发奇怪问题。另外检查你的\label使用位置做到两条不要把\label直接写在\href内部。如果你需要给一个链接对应的目标做标记把\label放在链接开始之前或者链接结束之后。不要在章节标题、\caption文字里用\footnote。这一类内容会被多次处理脚注盒子跟链接对象一交错栈就容易失衡。我后来养成了一个习惯所有交叉引用的标签都单独占用一行尽量远离链接和脚注环境。这个习惯帮我省下了大量排查时间。5.4 在 VSCode 里搭一套顺手的排查环境排查过程和修复方案都讲完了我再顺手分享一个跟实际编译相关的技巧。很多人现在都习惯用 VSCode LaTeX Workshop 插件来写 LaTeX这个组合很方便但报错时其实不太容易看清.log文件的上下文。我的建议是把 LaTeX Workshop 的编译日志面板打开或者在出问题时直接用命令行编译保留完整输出。当你需要二分注释法时直接在 VSCode 里选中一段代码注释掉然后用快捷键重新编译即可。如果反复测试导致辅助文件堆积可以在 VSCode 的命令面板里执行“Clean up auxiliary files”把.aux、.out、.toc这些文件清掉再重新编译。另外提醒一点如果你用了latexmk它会自动判断需要编译几次。遇到这个报错时有时候多编译一轮就自己好了这是因为第二轮编译时辅助文件里的状态已经更新。但如果你想定位问题还是建议先把辅助文件清理干净让编译从初始状态开始这样才不会被旧状态干扰。如果你经常需要处理\cite的显示格式比如把引用标号设置成上标也建议在调试完hyperref之后再动cite、natbib这些宏包的设置。cite和hyperref的交互也有自己的坑尽量在基础环境稳定之后再叠加这些定制需求。6. 常见问题速查表我把这些年遇到的类似问题整理成了一张速查表方便你直接对照。报错或现象可能原因处理办法You cant pop an empty literal stack for entry xxx出现在\end{document}附近hyperref 栈失衡通常是宏包版本或交互问题更新 hyperref清理辅助文件关闭\pdfobjcompresslevel报错中的 entry 名可以在源文件里搜到\label可能位于链接、脚注、标题等特殊位置把\label挪到链接外报错在更新 TeX Live 后突然出现宏包版本错位tlmgr update --all或降级hyperref设置了\cite上标后出现类似报错cite / natbib 与 hyperref 交互先注释掉 cite 相关设置确认 hyperref 正常后再调清理辅助文件后不再复现旧的.aux/.out残留状态清理辅助文件后重新编译用 luaLaTeX 编译时\pdfobjcompresslevel0无效pdfTeX 专属参数不适用于 LuaTeX使用\pdfvariable objcompresslevel0或更新 TeX Livebeamer 文档中频繁触发overlay 机制与 hyperref 状态冲突避免在 frame 标题内放链接必要时给 frame 加[fragile]并升级宏包这张表不保证覆盖所有情况但覆盖了我实测过的绝大部分场景。如果你按照这个表格排查了一遍还不行那大概率是你用了一个非常冷门的宏包或者hyperref与其他宏包的某个极小细节冲突。遇到这种情况别自己硬扛去提 issue 的时候附上最小复现示例比在正文里盲目试错有效得多。7. 最后分享一点我自己的体会这个报错见过几次之后我最大的体会是LaTeX 的报错链条很多时候不是“你写错了”而是“宏包状态失衡”。尤其涉及hyperref时出错的位置往往距离真正的原因很远。第一次遇到empty literal stack时我也很慌四处搜解法试了各种不靠谱的 hack最后发现只是版本太旧。后来再遇到类似问题我已经形成了一套固定动作先记下 entry 名再翻.log该更新的更新该注释的注释该清理的清理基本上十分钟内能解决。最后再分享一个小技巧怀疑是宏包交互问题时不要在原文档里来回试专门建一个最小的.tex文件只放触发问题的宏包和一小段正文然后慢慢加内容。最小复现示例会让你从“觉得是玄学”变成“找到了规律”。很多看起来神秘莫测的 LaTeX 报错缩小范围之后都会变得异常清晰。
返回列表