暗色模式

Linux 字符编码与乱码排查:从 iconv 转换到 locale 配置

技术教程
2026-09-11
11
0
本文要点
  • 乱码的本质是「字节没变、解读方式变了」:同一个「编」字,UTF-8 存成 e7 bc 96 三个字节,GBK 存成 b1 e0 两个字节——用错的编码去读,自然是一屏莫名其妙的中文
  • file -i 会把 GBK 文件误判成 iso-8859-1(本文实测),不能盲信:它是靠启发式猜的,判断真实编码要结合文件来源、字节数和 hexdump
  • iconv -f GBK -t UTF-8 一行完成转码;方向写反会报 illegal input sequence at position 6 并返回非零——这个报错本身就是「编码判断错了」的强信号
  • locale 不只管界面语言:实测同一条 sortLC_ALL=C 得到 B C a b(按字节值),LC_ALL=en_US.utf8 得到 a b B C(按字典序),结果完全不同
  • 演示机默认只有 C / C.utf8 / POSIX / en_US.utf8 四种 locale,locale-gen zh_CN.UTF-8 本地生成(约 3MB 写入 locale 归档)后 date 立刻输出「2026年 09月 10日 星期四」

Linux 字符编码与乱码排查:从 iconv 转换到 locale 配置

从 Windows 导出的 GBK 编码 CSV 传到 Linux 服务器上,cat 出来是一屏鬼画符;脚本里明明写了中文排序,结果顺序怎么都不对;date 输出的星期是英文,前端同事说要中文——这三个看似不相干的问题,根子上是同一件事:字符编码locale

本文在一台 Ubuntu 22.04 云服务器(KVM,内核 5.15.0-30-generic)上把这两件事从原理到命令实测一遍:先用 hexdump 看清乱码到底乱在哪,再用 iconv 把文件转回来,最后搞清楚 locale 如何悄悄影响排序、日期甚至报错信息。文中所有命令与输出均为真实执行结果,截图即真实终端。

乱码的本质:字节没变,解读方式变了

要理解乱码,先分清两个概念:

  • 字符集(charset):一套「字符 ↔ 编号」的对照表。Unicode 给每个字符分配一个码点,比如「编」是 U+7F16
  • 编码(encoding):把码点变成实际字节的规则。UTF-8 把 U+7F16 写成三个字节 e7 bc 96;GBK 则把「编」写成两个字节 b1 e0

文件在磁盘上永远只是一串字节,没有地方标注「我是用什么编码写的」。读文件时用哪套规则解码,完全由读取方决定。规则对上了就是正常中文,对不上就是乱码——字节从头到尾没变过。

先把同一个字符串分别用两种编码写成文件,看看字节层面差在哪:

mkdir -p /tmp/encdemo && cd /tmp/encdemo
printf 'Linux 编码教程\n' > u.txt
iconv -f UTF-8 -t GBK u.txt > g.txt
printf 'B\na\nC\nb\n' > t.txt

两个文件内容完全一样,只是编码不同。用 file 看识别结果、用 hexdump 看真实字节:

cd /tmp/encdemo && file -i u.txt g.txt && hexdump -C u.txt && hexdump -C g.txt

file 识别编码 + hexdump 对比 UTF-8 与 GBK 的字节布局

对着输出逐段读:

  • u.txt: charset=utf-8file 认出了 UTF-8,正确。
  • g.txt: charset=iso-8859-1这是误判。GBK 文件被认成了西欧的 Latin-1。file 的编码判断来自 libmagic 的启发式规则,对 GBK/GB18030 这类双字节中文编码支持很弱,只要高位字节密集就倾向猜 Latin-1。所以不要把 file 的输出当结论
  • UTF-8 的字节e7 bc 96(编)e7 a0 81(码)e6 95 99(教)e7 a8 8b(程),每个中文字符 3 字节,加上 Linux 和换行,文件共 00000013 = 19 字节
  • GBK 的字节b1 e0(编)c2 eb(码)bd cc(教)b3 cc(程),每个中文字符 2 字节,文件共 0000000f = 15 字节

同一个字符串,UTF-8 比 GBK 多出 4 个字节——这也是「中文 CSV 转成 UTF-8 后文件变大」的原因。

判断编码时,比 file 更可靠的思路是:看来源(Windows 简体中文环境导出的多半是 GBK,网页和 Linux 本地文件多半是 UTF-8)+ 看字节特征hexdump 里每个中文是否恒为 3 字节一组、高位字节是否呈 UTF-8 的 1110xxxx 10xxxxxx 10xxxxxx 模式)。

顺带记一个 .mime 形式的写法,输出更干净,适合塞进脚本做条件判断:

file --mime-encoding u.txt g.txt

iconv:一行完成转码

确认了 g.txt 是 GBK,转换只需要 iconv——-f 是来源编码(from),-t 是目标编码(to):

cd /tmp/encdemo && iconv -f GBK -t UTF-8 g.txt && python3 -c "print(open('u.txt','rb').read().decode('gbk').strip())"

iconv 正确转码 vs 用错编码解码产生的乱码

第一行 Linux 编码教程iconv -f GBK -t UTF-8 g.txt 的输出:GBK 文件被正确翻译成 UTF-8,中文正常显示。

第二行 Linux 缂栫爜鏁欑▼故意读错的结果:把 UTF-8 文件 u.txt 的字节硬按 GBK 解码(open('u.txt','rb').read().decode('gbk'))。这就是乱码的真身——每一组字节都不是合法 UTF-8 序列,但恰好是合法的 GBK 序列,于是被「翻译」成了一批毫不相干、但确实存在的汉字。所以乱码往往看起来「像中文但读不通」,而不是一堆问号。

如果反过来,把 GBK 文件当成 UTF-8 去转,iconv 会直接拒绝:

iconv -f UTF-8 -t GBK g.txt
Linux iconv: illegal input sequence at position 6

position 6 正是指向 Linux (6 字节)之后的第一个字节 b1——它不符合 UTF-8 的字节模式。这条报错极有价值:只要 iconvillegal input sequence,基本可以断定 -f 指定的来源编码错了,换成 GBK 或 GB18030 再试。注意命令返回码是 1,脚本里可以用 || 捕获。

万一文件里真的混着个别坏字节(日志被截断、转码转了一半),可以用 -c 静默跳过非法序列,保证整体能转出来:

printf '\xff\xfeabc\n' | iconv -c -f UTF-8 -t UTF-8
abc

iconv 支持的编码有 1179 种iconv -l | wc -l),常用的 GBKGB18030BIG5LATIN1UTF-16 都在列。查完整清单直接跑 iconv -l

批量转换:把整个目录的 GBK 文件洗成 UTF-8

单个文件好办,一整个目录的历史文件才是常态。iconv 只能写到标准输出,所以批量转换要「先转存临时文件、再覆盖回去」:

for f in batch/*.txt; do iconv -f GBK -t UTF-8 "$f" > "$f.u8" && mv "$f.u8" "$f"; done

这里用 && 而不是 ; 是关键:只有转换成功才覆盖原文件。如果 iconv 失败,$f.u8 是空文件,&& 会让 mv 不执行,原文件得以保全。实测两个 GBK 文件全部转换成功后,file 的识别结果从 iso-8859-1 变成了 utf-8

生产环境建议再加一层保险:转换前用 cp -a 备份整个目录,转换后抽查几个文件的中文是否正常,再删备份。

locale:决定排序、日期和报错语言

编码解决的是「字节怎么读」,locale 解决的是「读出来之后按什么规矩办事」。它是三个环境变量按优先级叠加的结果:

LC_ALL > LC_*(单项) > LANG(兜底)

LANG 是大方向,LC_* 可以逐项覆盖(LC_TIME 管日期、LC_COLLATE 管排序、LC_MESSAGES 管报错语言),LC_ALL 一票否决全部。

locale 设置最容易踩的坑是排序。看下面这条实测——同一个文件 t.txt(内容 B a C b),只换 LC_ALL

cd /tmp/encdemo && LC_ALL=C sort t.txt | tr '\n' ' '; echo; LC_ALL=en_US.utf8 sort t.txt | tr '\n' ' '; echo; LC_TIME=zh_CN.UTF-8 date

locale 影响排序结果与日期格式

  • LC_ALL=C 得到 B C a b:C locale 是「无语言」的裸字节序,按 ASCII 码值排——B(0x42) < C(0x43) < a(0x61) < b(0x62)。所以大写字母全部排在小写字母前面。
  • LC_ALL=en_US.utf8 得到 a b B C:英文 locale 按字典序,且忽略大小写差异,a/B/b/C 交替排列。

同一个 sort,两种结果,谁都没报错。这就是为什么「脚本在服务器上跑出来的顺序和本地不一样」——多半是两边的 LANG/LC_ALL 不同。写脚本时如果依赖排序结果,务必显式指定 LC_ALL=C(比如比较校验和、处理机器生成的 ID 列表),否则换台机器就可能出岔子。

生成缺失的 locale

第三行输出 2026年 09月 10日 星期四 显示中文,是因为显式指定了 LC_TIME=zh_CN.UTF-8。但这里有个前提:这台机器原本没有 zh_CN 这个 locale。

查看本机已安装的 locale:

locale -a
C
C.utf8
POSIX
en_US.utf8

只有四种——这是云服务器最小化安装的典型状态(默认 LANG=C.UTF-8,用 localectl status 可以看到 System Locale: LANG=C.UTF-8)。

好消息是生成 locale 不需要联网下载,系统的 locale 源文件就在 /usr/share/i18n/locales/ 下,locale-gen 直接本地编译:

locale-gen zh_CN.UTF-8
Generating locales (this might take a while)...
  zh_CN.UTF-8... done
Generation complete.

编译结果写进 /usr/lib/locale/locale-archive,生成后该文件为 6.0M——加一个中文 locale 的代价很小。之后 LC_TIME=zh_CN.UTF-8 date 就能输出上面的中文日期。

要让它对所有会话持久生效,用 localectl(不要手改 /etc/default/locale,容易写错格式):

localectl set-locale LANG=zh_CN.UTF-8

注意:把系统 LANG 整体改成中文后,很多命令的报错信息和手册页也会变中文。服务器上更稳妥的做法是保持 LANG=C.UTF-8(英文报错便于搜索),只在确实需要的程序里单独指定 LC_TIMELC_COLLATE

排查清单

遇到中文乱码,按这个顺序走一遍基本都能定位:

现象首选排查命令
文件内容乱码确认文件真实编码`hexdump -C 文件 \headfile -i 文件`
乱码「像中文但读不通」大概率是编码读错,不是文件坏iconv -f GBK -t UTF-8 try 一遍
iconvillegal input sequence-f 写错了换成 GBK / GB18030
个别坏字节导致整体转不出跳过非法序列iconv -c -f ... -t ...
排序结果和预期不符locale 作祟显式加 LC_ALL=C
日期/星期是英文缺中文 localelocale-gen zh_CN.UTF-8
程序报错 Cannot set LC_ALL to default localelocale 未生成locale -a 核对 + locale-gen

一句话总结:乱码先看字节(hexdump)再定编码(别信 file),转换用 iconv 且方向报错就是信号;locale 则记得 LC_ALL > LC_* > LANG,脚本里排序一律显式 LC_ALL=C

参考资料:

发表评论

暂无评论,快来抢沙发吧!