前言
乱码是一个让人头痛的问题,码虫们或多或少都会遇到。
在度娘上搜答案,往往不能让开发者满意,我列出了一些外网搜索有帮助的关键词:
- unreadable characters
- gibberish
- garbled text
- mojibake
本文从我学习到工作的经历中,选择了一部分的有趣的乱码遭遇,供大家围观。
[leo@leo-m ~]$ printf "\U0001F92A\U0001F92A\U0001F92A"
🤪🤪🤪电脑求救狂喊烫烫烫
这应该是每一个c/c++初学者高概率遇到过的问题。
|
|
使用visual studio 2019的Debug调试模式运行结果:

我遇到这个问题时,天下还是windows xp的,开发环境使用的古老的vc++ 6.0。
虽然这是一个典型问题,c/c++的字符串没有空结尾,导致输出了字符串后面的一段非法内存数据,但是为什么输出“烫烫烫”呢?
作为小白的我,当然不会care这种问题,只管“照着正确的方式做就行了”。况且依当年的互联网环境可能也搜不出答案,就算是搜出答案,我也大概率看不懂。
多年以后才明白,原来是visual studio在调试模式下,使用0xCC(INT3调试指令的操作码)标注未初始化的栈上内存,以方便错误检查。
查看Why does the not allocated memory is marked like 0xCC?
那么0xCC与“烫”是什么关系?其实“烫”的GBK编码就是0xCC 0xCC,在linux下我用两种方式展示字符编码与文字的转化:
[leo@leo-m ~]$ echo -e "\xcc\xcc"|iconv -f gbk
烫
[leo@leo-m ~]$ echo -ne "烫" | iconv -t gbk | hexdump
0000000 cccc
0000002命令解释:
man echo命令可以查看echo的帮助文档,-e是表示解析反斜杠转义符号(escape,这里就是指后面的\x),cc就是一个字节(BYTE)的编码,两个cc才能表示一个“烫”。这里可以使用echo输出,也可以使用printf命令。man iconv命令可以查看iconv的帮助文档,-f表示指定输入的字符编码(不指定时,默认为本地locale环境所指定的编码),-t表示转换后输出的编码。|管道,表示左边命令的输出作为右边命令的标准输入(stdin)。hexdump以16进制显示数据,这里可以用xxd工具替换。两者都很好用。
Linux文件名乱码
第一次尝试学习使用Linux发行版本是ubuntu 8.04,没用虚拟机,傻傻地直接安装在笔记本上,加上校园网客户端对Linux的不友好,很折腾。
各种不符合Win习惯的命令操作,加上移动硬盘上部分文件名乱码,直接把人整懵了。对于名声赫赫的Linux,我打心底觉得“真是难用”。
古老的乱码问题不知道原因,也找不到具体环境了,但文件名乱码问题偶尔还是会遇到。比如在Windows下,我使用7z压缩文件夹test为test.zip,目录结构如下:
test
├── 中文名.txt
├── 中文文件夹
│ └── 中文1.txt
└── xx.txt在Linux上使用unzip解压,便出现了乱码:
[leo@leo-m charset]$ unzip test.zip
Archive: test.zip
creating: test/
extracting: test/xx.txt
extracting: test/╓╨╬─├√.txt
creating: test/�������/
extracting: test/�������/����1.txt [leo@leo-m test]$ tree
.
├── \326\320\316\304\316\304\274\376\274\320
│ └── \326\320\316\3041.txt
├── ╓╨╬─├√.txt
└── xx.txt
1 directory, 3 files
WTF? 都2021年了,这么基础的操作也乱码?
这个问题也是字符编码与解码不一致造成的,在Win上7z压缩是使用GBK编码,而在本地Linux上,默认使用的utf-8解码。在Linux上查看本地编码设置:
[leo@leo-m ~]$ echo $LANG
zh_CN.UTF-8那么如何解决这个乱码问题呢?在archlinux有一个iconv的解压软件包unzip-iconv(其他发行版应该也有),能够指定编码。
使用unzip-iconv显示压缩包文件内容:
[leo@leo-m test]$ unzip -O GBK -l ../test.zip
Archive: ../test.zip
Length Date Time Name
--------- ---------- ----- ----
0 2021-08-23 11:42 test/
4 2021-08-19 10:13 test/xx.txt
9 2021-08-17 10:05 test/中文名.txt
0 2021-08-23 11:42 test/中文文件夹/
0 2021-08-23 11:41 test/中文文件夹/中文1.txt
--------- -------
13 5 filesWin下Python输出乱码
前面的乱码问题,都是小问题,就算遇到了,睁一只眼闭一只眼,继续吃的好睡得香。可是开发过程中遇到乱码问题不解决,老板是会打屁屁的。
那时主流用python2,python3还不稳定,用的人少。平时在Windows上开发,最后运行在Linux上。有时,同样的代码,Win上出问题,Linux上完好。
运行不了,出现\xHH乱码
现在看例子(代码文件用utf-8格式保存),在Windows下运行python2:
|
|
运行出现错误,字符串出现了\xHH:
C:\Users\leo\Desktop\test>python main.py
File "main.py", line 1
SyntaxError: Non-ASCII character '\xe4' in file main.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details好吧,此时我才知道utf8这种东西,以前最多停留在c/c++中char与wchar,visual studio里的多字节编码和Unicode,转码函数multibytetowidechar、mbstowcs这种基本使用上。
我们先看中文的utf-8编码,第一个字节为\xe4:
[leo@leo-m ~]$ printf 中文|xxd
00000000: e4b8 ade6 9687 ......在python2时代,默认的字符串解析,使用ASCII编码,最大表示0x7F,遇到\xe4自然不认识。查看码表:
[leo@leo-m ~]$ ascii
Usage: ascii [-adxohv] [-t] [char-alias...]
-t = one-line output -a = vertical format
-d = Decimal table -o = octal table -x = hex table -b binary table
-h = This help screen -v = version information
Prints all aliases of an ASCII character. Args may be chars, C \-escapes,
English names, ^-escapes, ASCII mnemonics, or numerics in decimal/octal/hex.
Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex
0 00 NUL 16 10 DLE 32 20 48 30 0 64 40 @ 80 50 P 96 60 ` 112 70 p
1 01 SOH 17 11 DC1 33 21 ! 49 31 1 65 41 A 81 51 Q 97 61 a 113 71 q
2 02 STX 18 12 DC2 34 22 " 50 32 2 66 42 B 82 52 R 98 62 b 114 72 r
3 03 ETX 19 13 DC3 35 23 # 51 33 3 67 43 C 83 53 S 99 63 c 115 73 s
4 04 EOT 20 14 DC4 36 24 $ 52 34 4 68 44 D 84 54 T 100 64 d 116 74 t
5 05 ENQ 21 15 NAK 37 25 % 53 35 5 69 45 E 85 55 U 101 65 e 117 75 u
6 06 ACK 22 16 SYN 38 26 & 54 36 6 70 46 F 86 56 V 102 66 f 118 76 v
7 07 BEL 23 17 ETB 39 27 ' 55 37 7 71 47 G 87 57 W 103 67 g 119 77 w
8 08 BS 24 18 CAN 40 28 ( 56 38 8 72 48 H 88 58 X 104 68 h 120 78 x
9 09 HT 25 19 EM 41 29 ) 57 39 9 73 49 I 89 59 Y 105 69 i 121 79 y
10 0A LF 26 1A SUB 42 2A * 58 3A : 74 4A J 90 5A Z 106 6A j 122 7A z
11 0B VT 27 1B ESC 43 2B + 59 3B ; 75 4B K 91 5B [ 107 6B k 123 7B {
12 0C FF 28 1C FS 44 2C , 60 3C < 76 4C L 92 5C \ 108 6C l 124 7C |
13 0D CR 29 1D GS 45 2D - 61 3D = 77 4D M 93 5D ] 109 6D m 125 7D }
14 0E SO 30 1E RS 46 2E . 62 3E > 78 4E N 94 5E ^ 110 6E n 126 7E ~
15 0F SI 31 1F US 47 2F / 63 3F ? 79 4F O 95 5F _ 111 6F o 127 7F DEL修改编码,依旧乱码
既然是解析问题,我们让python2以utf-8来解析字符串,添加注释# -*- coding: utf-8 -*-:
|
|
这次代码,在Linux上运行良好,但是在Win上奇奇怪怪:
C:\Users\leo\Desktop\test>python main.py
涓枃看着屏幕上自己的倒影,蹭亮的脑袋格外显眼,虽然物抗法抗反甲与生俱来,但是连机器世界的大佬都骗人,我们还要怎么做才能防火防盗防师妹呢?说好的跨平台,说好的移植性好呢?
然后内心防线崩溃了,开始口吐芬芳。支持个中文这么难,就像很多开源工具一样,“文件路径”不能有“空格”,不能有“中文”,python2看来也是“美帝良心”。
其实,这个问题也不是中文出现,在谷歌上,拥有世界的大韩民族,小日子过得还不错的大和民族,都跟爱嘲讽的我们一样。整个世界的非英语国家都受到了暴击。
网上最流行的方案,也是最合适的方案,就是标注字符串为unicode string:
# -*- coding: utf-8 -*-
s = u"中文"
print(s)
现在问题解决了,看起来也是编码问题,这个原因要怎么理解?
Win上默认使用GBK编码,命令行中使用chcp命令,查看代码页(Code page,代码页其实就是在Win、IBM等系列产品下,字符编码的另一套框架实现。cp936与GBK一个是从实现框架上说,一个是字符集映射上来说,但我们简单将cp936看作是GBK编码的):
C:\Users\leo\Desktop\test>chcp
活动代码页: 936而Linux上使用UTF-8编码:
[leo@leo-m ~]$ echo $LANG
zh_CN.UTF-8看到环境不一样,就能想到差异的由来:
- 代码中虽然指定了
coding: utf-8,但只是告诉python2如何来认识代码脚本。我们的代码文件是使用utf-8格式保存的,那就应该用utf-8来解析,不要看到越界(0 - 127,ASCII)的字符就报错。这就是使用coding: utf-8的目的。 - 字符串
中文在内存中虽然以utf-8的数据表示(0xe4 0xb8 0xad 0xe6 0x96 0x87) - 但是在
Win上使用(比如显示、打印字符)时,根据当前代码页设置,又使用GBK编码来映射字符,所以乱码了。 - 在
Linux上,当前编码同样是utf-8,所以不会乱码。如果不是utf-8,乱码也会出现。
我们可以验证下,确认中文的utf-8数据,用GBK编码解析看到相同的乱码:
[leo@leo-m ~]$ printf "中文"|iconv -f GBK -c
涓枃在Linux上模拟乱码
使用LANG=zh_CN.gbk xterm命令,目地是使用zh_CN.gbk编码上下文,打开一个新的xterm终端。注意不要直接在xterm中用export LANG=zh_CN.gbk修改环境,这样没有效果,因为xterm本身不在这个环境变量下。
[leo@leo-m ~]$ echo $LANG
zh_CN.gbk
[leo@leo-m ~]$
[leo@leo-m ~]$ python2 /tmp/main.py
涓枃其他解决办法
其他解决办法类似,这里提出来,只是帮助理解这个乱码问题。
- 使用unicode()
|
|
使用unicode()函数创建了个unicode string。
- 使用decode()
|
|
将utf-8编码的中文,使用utf-8解码为新的unicode string。
- 使用gbk编码
|
|
将coding改为gbk,最后使用GBK编码保存文件。
网页显示乱码
有了python2的一段乱码经历,对于解决乱码问题,知道一个编码要与解码对应起来。现在觉得这个问题简单,但以前知识太薄,解决乱码很迷茫。
几年前,同事写了个网页,遇到了乱码,我拿起utf-8的刀子就开始切,最后还是受到了社会无情的鞭打。
在Win上,使用gbk保存的一段html网页代码:
|
|

这个乱码很简单,Win上很多工具默认的文件编码为gbk,所以指定charset="UTF-8"解码不正确。
两种解决办法:
一,将文件用utf-8格式保存。推荐。
二,继续用gbk保存,把代码中的charset指定为gbk。
网站系统前后端交互乱码
这是两年前遇到的问题,后端对字符串一个字节一个字节地做异或、乱序、base64/unbase64等操作,然后前端js逆向操作解码出字符串。操作很简单,但是中文出现乱码。
后端使用openresty代替nginx,虽然其它后端语言一样会出现这个问题,但我还是拿原来的环境来说明。
nginx通常被用做静态网站的容器,以及反向代理的网关。openresty在nginx基础上,添加了lua脚本的支持,使之有动态内容的处理能力。比如在高并发网站系统中,与redis结合,形成后端系统的第一层缓存。
搭建openresty
这里我们测试时,可以使用docker来简单搭建环境,端口使用18880:
docker run -d --name openresty -p 18880:80 \
-v /opt/docker/openresty/conf.d:/etc/nginx/conf.d \
openresty/openresty:alpine
用lua在基础配置上添加一段location,返回一段字符串。(基础配置default.conf)如下:
|
|
将配置文件放置到/opt/docker/openresty/conf.d/目录下。docker restart openresty重启openresty。
linux终端下,测试后端配置正确,能正常访问:
[leo@leo-m ~]$ curl http://localhost:18880/test 2>/dev/null
hello 看看中文 world测试成功后,先在浏览器访问http://localhost:18880,之后在调试窗口中才可以发起同源地址的请求。在浏览器调试窗口中发起请求:
|
|

返回结果正常。
复现问题
将default.conf配置中
|
|
添加base64编码,改为
|
|
重启openresty后(docker restart openresty),终端中测试请求并解码:
[leo@leo-m ~]$ curl http://localhost:18880/test 2>/dev/null
aGVsbG8g55yL55yL5Lit5paHIHdvcmxk
[leo@leo-m ~]$ curl http://localhost:18880/test 2>/dev/null \
| base64 --decode
hello 看看中文 world测试成功后,在浏览器调试窗口中发起请求应使用atob解码:
|
|

中文显示乱码:
hello çç䏿 world
问题来了,为什么不使用base64时,中文正常显示,加上后却乱码了?眼花了,还是神经错乱?
现在知道为什么程序员虽是“农名工”却要拿高工资了吧?颈椎病事小,精神出问题事大。
乱码原因
还得搬出“编码不匹配”这把屠龙宝刀。
后端发送数据是utf-8编码,那么js的字符串使用什么编码?答案是utf-16(What is the default JavaScript character encoding)。
不加base64时为什么显示正常
简单一句话(在MDN上有明确描述Response.text()),在text()函数中,默认按utf-8编码转码了请求的返回数据。
但是我没有找到fetch发送数据时,转码为utf-8的描述。大佬不说,我就手动抓包测试一下。
准备发送数据的代码:
|
|
然后开启抓包(因为服务在本地,访问时也使用localhost,所以抓包的网络接口为lo的回环网口),再发送数据,得到:
[leo@leo-m ~]$ sudo tcpdump -A -X "tcp port 18880" -i lo
...
0x0000: 4500 02d8 7a25 4000 4006 bff8 7f00 0001 E...z%@.@.......
0x0010: 7f00 0001 96e6 49c0 cb75 9300 df86 66d9 ......I..u....f.
0x0020: 8018 0200 00cd 0000 0101 080a 97a0 1f94 ................
0x0030: 97a0 1f94 504f 5354 202f 7465 7374 2048 ....POST./test.H
0x0040: 5454 502f 312e 310d 0a48 6f73 743a 206c TTP/1.1..Host:.l
0x0050: 6f63 616c 686f 7374 3a31 3838 3830 0d0a ocalhost:18880..
0x0060: 5573 6572 2d41 6765 6e74 3a20 4d6f 7a69 User-Agent:.Mozi
0x0070: 6c6c 612f 352e 3020 2858 3131 3b20 4c69 lla/5.0.(X11;.Li
0x0080: 6e75 7820 7838 365f 3634 3b20 7276 3a39 nux.x86_64;.rv:9
0x0090: 312e 3029 2047 6563 6b6f 2f32 3031 3030 1.0).Gecko/20100
0x00a0: 3130 3120 4669 7265 666f 782f 3931 2e30 101.Firefox/91.0
0x00b0: 0d0a 4163 6365 7074 3a20 2a2f 2a0d 0a41 ..Accept:.*/*..A
0x00c0: 6363 6570 742d 4c61 6e67 7561 6765 3a20 ccept-Language:.
0x00d0: 656e 2d55 532c 656e 3b71 3d30 2e35 0d0a en-US,en;q=0.5..
0x00e0: 4163 6365 7074 2d45 6e63 6f64 696e 673a Accept-Encoding:
0x00f0: 2067 7a69 702c 2064 6566 6c61 7465 0d0a .gzip,.deflate..
0x0100: 5265 6665 7265 723a 2068 7474 703a 2f2f Referer:.http://
0x0110: 6c6f 6361 6c68 6f73 743a 3138 3838 302f localhost:18880/
0x0120: 0d0a 436f 6e74 656e 742d 5479 7065 3a20 ..Content-Type:.
0x0130: 7465 7874 2f70 6c61 696e 3b63 6861 7273 text/plain;chars
0x0140: 6574 3d55 5446 2d38 0d0a 4f72 6967 696e et=UTF-8..Origin
0x0150: 3a20 6874 7470 3a2f 2f6c 6f63 616c 686f :.http://localho
0x0160: 7374 3a31 3838 3830 0d0a 436f 6e74 656e st:18880..Conten
0x0170: 742d 4c65 6e67 7468 3a20 3435 0d0a 436f t-Length:.45..Co
0x0180: 6e6e 6563 7469 6f6e 3a20 6b65 6570 2d61 nnection:.keep-a
0x0190: 6c69 7665 0d0a 436f 6f6b 6965 3a20 6578 live..Cookie:.ex
0x01a0: 7065 7269 6d65 6e74 6174 696f 6e5f 7375 perimentation_su
0x01b0: 626a 6563 745f 6964 3d49 6a67 305a 6a5a bject_id=Ijg0ZjZ
0x01c0: 6959 6a4d 784c 5751 3259 546b 744e 4745 iYjMxLWQ2YTktNGE
0x01d0: 334d 4330 344f 5445 794c 5467 3059 5749 3MC04OTEyLTg0YWI
0x01e0: 304e 546b 3259 7a45 795a 4349 2533 442d 0NTk2YzEyZCI%3D-
0x01f0: 2d65 3961 3363 6465 3666 6361 3764 6265 -e9a3cde6fca7dbe
0x0200: 3036 6136 6437 6132 6533 3137 3062 6235 06a6d7a2e3170bb5
0x0210: 6238 6639 3932 3761 333b 2073 6964 6562 b8f9927a3;.sideb
0x0220: 6172 5f63 6f6c 6c61 7073 6564 3d66 616c ar_collapsed=fal
0x0230: 7365 0d0a 5365 632d 4665 7463 682d 4465 se..Sec-Fetch-De
0x0240: 7374 3a20 656d 7074 790d 0a53 6563 2d46 st:.empty..Sec-F
0x0250: 6574 6368 2d4d 6f64 653a 2063 6f72 730d etch-Mode:.cors.
0x0260: 0a53 6563 2d46 6574 6368 2d53 6974 653a .Sec-Fetch-Site:
0x0270: 2073 616d 652d 6f72 6967 696e 0d0a 5072 .same-origin..Pr
0x0280: 6167 6d61 3a20 6e6f 2d63 6163 6865 0d0a agma:.no-cache..
0x0290: 4361 6368 652d 436f 6e74 726f 6c3a 206e Cache-Control:.n
0x02a0: 6f2d 6361 6368 650d 0a0d 0ae4 b8ad e696 o-cache.........
0x02b0: 872a 2a2a 2a2a 2a2a 2a2a 2a2a 2a2a 2a2a .***************
0x02c0: 2a2a 2a2a 2a2a 2a2a 2a2a 2a2a 2a2a 2a2a ****************
0x02d0: 2a2a 2a2a 2a2a 2a2a ********
...看到最后几行,****前面的字节(...e696 87...),确实是utf-8编码:
[leo@leo-m ~]$ printf '\xe6\x96\x87'
文看来发送时,也会默认把utf-16编码转为utf-8。
为什么乱码
看到乱码,像是西欧字符,并且与Extended ASCII中字符相对应。latin系列就是玩西欧字符的,那就在终端中测试latin1解码这个utf-8数据:
[leo@leo-m ~]$ printf "hello 看看中文 world"|iconv -f latin1
hello çç䏿 worldprintf是在zh_CN.UTF-8编码下输出的数据,在用latin1来解析这段数据,得到相同的乱码文字。
看看中文的utf-8编码:
[leo@leo-m conf.d]$ printf 看看中文| xxd
00000000: e79c 8be7 9c8b e4b8 ade6 9687 ............再看看第一个字节\xe7,在浏览器调试窗口中,我们输入"\xe7",可以看到对应的ç乱码。
atob解码后,应该是还原了utf8的编码数据。
但是,上面不是说js用utf-16来识别这段数据吗,应该得到如下的乱码呀:
[leo@leo-m ~]$ printf 'hello 看看中文 world'|iconv -f UTF-16
敨汬鳧讜룤螖眠牯摬这又是为什么呢?
其实atob解码后,将数据按BYTE填充UTF-16的双字节,类似与下面一段操作:
|
|
其中s_u8为看看中文的utf-8编码,其中每个字节都会对应到utf-16的单个字符(一个字符占两字节)。最终得到那串乱码:

然后又有一个问题,utf-16怎么就显示latin1字符了?其实utf-16是latin1编码扩展,前者的低字节部分就是后者的编码。
[leo@leo-m ~]$ printf '\u00e7'
ç修复乱码
修复该乱码问题,主要是实现utf-8到utf-16转码,可以手动写,也可以用TextDecoder(),但要考虑浏览器兼容性。
|
|
操作说明:
- 使用
Uint8Array,还原utf-8的字符串Buffer。 - 使用
TextDecoder转码。

最后
乱码问题定位,即简单又复杂。它可能有多个原因:
- 有些乱码可能不是乱码,只是一种特殊字串。比如浏览器地址的中特殊字符转义(对应
ASCII值):%3d表示=。比如linux终端的颜色格式化:\033[1;31mred text\033[0m用于输出红色的red text。 - 数据本身不是合法字符,所有编码都不能表示它。
- 编码不匹配。编码与解码不匹配,数据与显示环境解码不匹配。文件数据可以使用
chardet工具来猜测编码。通过换编码、换工具等来解决。 - 字体不支持。西欧字体不支持中文显示,字体不支持
emoji表情字体等。通过换字体、换工具来解决。
参考资料
打不开链接的,请科学上网。