前言
乱码是一个让人头痛的问题,码虫们或多或少都会遇到。
在度娘上搜答案,往往不能让开发者满意,我列出了一些外网搜索有帮助的关键词:
- 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 files
Win下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 ççä¸æ world
printf
是在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
表情字体等。通过换字体、换工具来解决。
参考资料
打不开链接的,请科学上网。