在 Shift_JIS 里,「本」这个字是两个字节 96 7B,而 7B 就是 ASCII 的 {;「表」是 95 5C,5C 则是反斜杠。也就是说,字符编码一旦搞错,日文正文会逐字节变成 LaTeX 的控制序列和花括号。这正是 TeX 里的乱码从来不只是"出现看不懂的字"的原因,也是今天的 .tex 应当以 UTF-8 + LF 保存的原因。本页用真实的报错原文,说明遇到 Shift_JIS、EUC-JP、ISO-2022-JP 的遗留文件时会发生什么、该怎么转换,以及 platex 与 uplatex 能处理的字符范围有何不同。
.tex 应该用什么保存:UTF-8 与 LF
UTF-8(有无 BOM 皆可)与 LF。TeX Live 2024 的 uplatex、lualatex、xelatex 默认都按 UTF-8 读取,Git 生成差异时也以 UTF-8 和 LF 为前提。日文圈之所以在这里格外麻烦,是因为在 Unicode 普及之前,三种互不兼容的编码同时并存:Windows 用 Shift_JIS,Unix 用 EUC-JP,电子邮件为了通过七位信道而用 ISO-2022-JP(所谓"JIS 码")。同一个汉字有三套字节序列,而且光看文件内容也无法确凿分辨。所以打开一个旧实验室目录时,第一个该怀疑的就是编码。
| 编码 | 使用场景 | 如今还会遇到的地方 |
|---|---|---|
UTF-8 | 当前标准(Unicode) | 新工作唯一选择,也是 TeX Live 的默认 |
Shift_JIS | 旧 Windows、DTP、游戏主机 | 早年分发的模板、光盘附录;也叫 CP932 |
EUC-JP | 旧 Unix、大学计算中心 | 仍留在实验室共享目录里的 .tex 与 .sty |
ISO-2022-JP | 电子邮件("JIS 码") | 用转义序列切换字符集的七位方案;从邮件粘贴来的片段 |
把 Shift_JIS 文件当 UTF-8 打开会发生什么
屏幕不会被看不懂的字填满。报错会接连出现,最后倒在 ! Undefined control sequence. 上。 把 Shift_JIS 文件直接喂给 TeX Live 2024 的 uplatex,先出现 ! LaTeX Error: Invalid UTF-8 byte "93.,接着是 ! LaTeX Error: Invalid UTF-8 byte sequence (^^ea^^82̕).。到这里都还算友善——它们是对非法字节的如实报告。麻烦在后面:回显的行是 l.3 ^^93^^fa^^96{^^8c^^ea^^82̕\^^8e,一眼就能看出——「本」的后一个字节变成了 {,「表」的后一个字节变成了 \,而且都已经成了活的 TeX 语法。于是症状不是"日文显示不对",而是"LaTeX 说某个我根本没写过的命令未定义",这正是编码最不容易被怀疑的原因。
$ uplatex sjis.tex
! LaTeX Error: Invalid UTF-8 byte "93.
l.3 ^^93
! LaTeX Error: Invalid UTF-8 byte sequence (^^ea^^82̕).
! Undefined control sequence.
l.3 ^^93^^fa^^96{^^8c^^ea^^82̕\^^8e
$ uplatex -kanji=sjis sjis.tex # tell the engine what it is reading
Output written on sjis.dvi (1 page, 320 bytes).这场事故是 Shift_JIS 的设计所必然带来的。由于双字节字符的第二个字节被允许落在 ASCII 范围 0x40–0x7E 之内,第二个字节恰好是 0x5C(反斜杠)的字符有 52 个,恰好是 0x7B(左花括号)的有 50 个。前者包括 表、十、ソ、能、貼、暴、申、構;后者包括 本、宮、施、旬、養、鶏——都是日常日文里再普通不过的字。日本开发者把它们叫作"坏字",并与之缠斗多年,因为同样的事故也会发生在 shell 脚本和配置文件里,不只是 TeX。EUC-JP 没有这个问题——它的第二字节永远不小于 0x80——所以在 TeX 圈里曾有一段时间更偏爱 EUC-JP。
把 Shift_JIS 转换成 UTF-8:iconv 与 nkf
日文圈的经典工具是 nkf(Network Kanji Filter),但它需要额外安装——macOS 和多数 Linux 发行版都不自带。请先试 iconv:它是 POSIX 标准工具,在 macOS 与 Linux 上一律位于 /usr/bin/iconv。iconv -f CP932 -t UTF-8 old.tex > new.tex 就能完成 Shift_JIS 到 UTF-8 的转换。如果装了 nkf,nkf -w -Lu --overwrite *.tex 一行就能就地改写多个文件,接手整个目录时值得装上。无论用哪个,运行前一定先复制一份或先提交到 Git:--overwrite 会如字面所言毁掉原文件。
# iconv -- always present; safest one file at a time
iconv -f CP932 -t UTF-8 old.tex > new.tex # Shift_JIS -> UTF-8
iconv -f EUC-JP -t UTF-8 old.tex > new.tex # EUC-JP -> UTF-8
# whole tree, keeping a backup of every original
for f in *.tex; do cp "$f" "$f.bak"; iconv -f CP932 -t UTF-8 "$f.bak" > "$f"; done
# nkf, if installed: detect first, then convert in place to UTF-8 + LF
nkf -g old.tex
nkf -w -Lu --overwrite *.tex使用 iconv 时,编码名请写 CP932 而不是 SHIFT_JIS。两者常被当成一回事,其实不是。让 macOS 的 iconv 把含 ~(波浪号)或 ①(带圈数字)的文本转成 SHIFT_JIS,会停在 iconv: iconv(): Illegal byte sequence;换成 CP932 就能通过。SHIFT_JIS 是忠于 JIS X 0208 的窄定义,不包含 NEC 与 IBM 扩展字符——带圈数字、罗马数字之类。来自 Windows 的文件实际上都是 CP932,所以默认写 CP932 才对。需要反向(UTF-8 → Shift_JIS)转给老工具时,理由相同。
| nkf 选项 | 作用 | iconv 对应写法 |
|---|---|---|
-g | 检测当前编码与换行;不做转换 | 没有对应;改用 file 或 chardetect 之类 |
-w | 转换为 UTF-8(无 BOM) | iconv -t UTF-8 |
-s / -e / -j | 转换为 Shift_JIS / EUC-JP / ISO-2022-JP | iconv -t CP932 / -t EUC-JP / -t ISO-2022-JP |
-Lu / -Lw / -Lm | 把换行统一为 LF / CRLF / CR | 没有对应;用 sed、dos2unix 或 Git 的 eol=lf |
--overwrite | 直接改写给定文件 | 没有对应——iconv 写到标准输出,请重定向到新文件 |
-kanji=:不转换文件,直接告诉引擎怎么读
当你不想改写文件、或没有权限改写时,就改为告诉引擎。(u)platex 接受 -kanji=,可写 -kanji=sjis、-kanji=euc、-kanji=jis、-kanji=utf8。事实上,那个按 UTF-8 读会吐一大堆错误的 Shift_JIS 文件,在 uplatex -kanji=sjis sjis.tex 下顺利通过,只输出 Output written on sjis.dvi (1 page, 320 bytes).。但请把它当作急救措施。编辑器、Git、grep 以及用 \input 引入的其他文件仍然按 UTF-8 行事,编码一混就会引出另一场事故。把它用在"先把别人发来的稿子编译一次看看内容"这一步,看清之后就该转换。另外,lualatex 与 xelatex 根本没有 -kanji= 选项——它们始终按 UTF-8 读取。
platex 与 uplatex 的区别:同一个二进制,两个字符世界
差别在于能处理的字符范围,而不在程序本身。在 TeX Live 2024 上并排跑 platex --version 与 uplatex --version,两者都自称 e-upTeX 3.141592653-p4.1.1-u1.30-230214-2.6——同一个二进制。区别只在括号里:platex 是 (utf8.euc),uplatex 是 (utf8.uptex)。它们加载不同的格式,而格式决定了字符在内部如何存放。后果很具体:文档里写下 髙(U+9AD9,日本人姓氏常见的「高」的异体),platex 会停在 ! LaTeX Error: Unicode character ^^e9^^ab^^99 (U+9AD9),而 uplatex 一声不吭就排好了。普通 platex 的内部封闭在 JIS X 0208 的范围内,范围之外的字在门口就被挡下。新文档已经没有理由选 platex。 把 uplatex 设为默认,髙、𠮟 以及名册里的各种异体字都能通过。
$ platex --version | head -1
e-upTeX 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.euc) (TeX Live 2024)
$ uplatex --version | head -1
e-upTeX 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.uptex) (TeX Live 2024)
$ platex takashima.tex # the document contains 髙 (U+9AD9)
! LaTeX Error: Unicode character ^^e9^^ab^^99 (U+9AD9)
$ uplatex takashima.tex
Output written on takashima.dvi (1 page, 304 bytes).换行符与 BOM:LF、CRLF、CR 真会出事吗
LaTeX 本身对三者都照单全收。 把用 \r\n(CRLF,Windows)写成的 .tex 交给 uplatex,或者文件开头带着 UTF-8 的 BOM(EF BB BF),在 TeX Live 2024 上 uplatex 和 lualatex 都会毫无警告地编译通过。常听说 BOM 会在文档开头留下一个看不见的字符,至少对今天这两个引擎来说并非如此。受苦的不是 LaTeX,而是周边的工具。CRLF 与 LF 混杂的文件在 Git 里会显示为整篇都被改动,评审无从谈起。行尾残留 \r 的 .sty,grep 的行尾锚点可能失效。所以统一为 LF 是协作的决定,而不是排版的决定。用 Git 的话,在 .gitattributes 里加一行最为可靠,还能在检出时吸收各人机器之间的差异。
# .gitattributes -- normalise on checkin, hand out LF on checkout
*.tex text eol=lf
*.sty text eol=lf
*.bib text eol=lf
*.pdf binary
# one-off cleanup of a file that arrived with CRLF
sed -i.bak $'s/\r$//' old.tex- 新旧一律统一为 UTF-8 + LF。 这是 TeX Live 2024 三个引擎的默认,也与 Git 的预期一致。
- 转换从
iconv -f CP932 -t UTF-8开始。 写CP932而不是SHIFT_JIS,否则带圈数字和波浪号会转换失败。 - 若装有
nkf,nkf -w -Lu --overwrite *.tex最快——但 macOS 和多数 Linux 都不自带。 - 转换前先复制或提交到 Git。
--overwrite无法撤销。 - 新文档请用
uplatex(或lualatex)。 普通platex遇到 JIS X 0208 之外的字(如 髙,U+9AD9)会报错。 -kanji=sjis只是急救。 一旦看清内容,就把文件本身转成 UTF-8。