在我们使用计算机处理文本文件时,有没有想过文件内容里都有什么?是不是有英文、中文、emoji 表情等等, 再进一步追问这些内容又是通过什么方式存储的?这里就需要引入字符集和字符编码的概念。

字符集和字符编码

ASCII

起初计算机发展于西方,需要保存的内容主要以拉丁字母为主。 计算机中信息是以字节的形式存储于文件系统中,便制定了 ASCII 标准对这些字符进行编码、存储和传输。 这些字符的个数不足 256 个,只需要 1 个字节即可表示,比如数字 0 对应的 ASCII 是 0x30。

GBK

随着计算机在世界范围内发展,越来越多其他语言的文字需要传输存储,ASCII 编码便无能为力了,因为 根本没有为其他字符保留编码定义。于是,各国纷纷根据本国的字符制定字符集和编码方式, 比如中国的字符集需要包含大量的汉字(简体、繁体)、中文符号等等,同时也需要制定每个汉字怎样存储, 于是诞生了 GBK ,便是对这些字符集的标准定义和存储方案设计,细节参考 Wiki。

Unicode

与此同时,其他国家也在发展自己的字符集和编码方式,但都是以本国的语言为主,导致信息在世界范围 内传递存在困难,于是一个国际标准 Unicode 字符集便出现了,定义了每个字符对应的码位(code point)。

到目前为止,Unicode 中的字符个数已经达到了 10w+,为了能表示足够多的字符,至少需要 3 个字节。 那是不是也像 ASCII 一样字符按 3 字节存储?这样并不合理,因为不同字符在计算机世界里出现的 频率是不相同的,比如常出现的英文、中文等本来可以用 1-2 个字节存储传输,现在需要 3 个字节, 无端增加了存储传输成本,所以需要一种合理的编码方案,将码位映射到对应的字节信息,这就是编码方式。 对于 Unicode 存在 UTF-8、UTF-16LE、UTF-16BE 等编码方案,涉及变长编码和字节顺序的问题,细节参考 wiki。

以下表格汇总了不同的字符集和编码方式:

字符集合 编码方式
拉丁字母等 ASCII
中文等 GBK
Unicode UTF-8

再举例一些字符不同的编码:

字符 类型 Unicode GB- UTF-8
A ASCII 字符 U+0041 41 41
CJK 字符 U+4E00 D2BB E4 B8 80
笑哭 Emoji 字符 U+1F602 94 39 FC 38 F0 9F 98 82

程序编码

通过上面的信息,我们知道了不同的内容是如何存储、表示了,下一个问题是计算机程序是如何处理不同编码文件的?大概分为以下几个步骤:

磁盘文件 =>(读取) 二进制数据 => (编码方式识别及转码) 内码数据 => (字体编码) 屏幕显示
  • 首先,程序通过IO接口将文件的二进制文件读入,
  • 其次,按指定的编码方式或者自适应的编码方式来解码文件内容,
  • 第三,如果上述过程顺利,将内容按程序内部运行需要的内码编码进行转码,
  • 最后,如果需要显示,则识别对应的字符所需要的字体信息展示到屏幕上。

其中第二步是大家经常遇到乱码的环节,即没有使用对应的编码格式打开文件,下面以两个 常用的应用举例。

PYTHON

通过 sys.getdefaultencoding() 获取 Python 默认的编码方式,如果是 ASCII 码而且 代码文件中出现中文,则执行的时候回报错:

Non-ASCII character '\xe4' in file demo.py on line 1, but no encoding declared;

文件头中指定 # encoding:utf-8 ,同时文件的存储改为 UTF-8, Python 则可以按 UTF-8 编码文中的内容。如果文件头指定了 UTF-8,文件存储为 GBK, Python 运行依然出错,原因是 UTF-8GBK 不兼容,无法识别按 GBK 编码方式存储的字符。

Python 解码字节流用的方法是 decode,第一个参数为被解码的字节流原始编码格式, 需要根据存储编码格式填写,比如文件内容为 UTF-8 存储,用 GBK 解码会报错。

s = '中文'
z = s.encode('utf8').decode('GBK')  # error

VIM

Vim 里面的编码主要和三个参数有关:enc(encoding),fenc(fileencoding) 和 fencs(fileencodings)

fenc 是当前文件的编码,一个在 Vim 里正确显示的文件(前提是系统环境跟enc设置匹配)。 可以通过改变 fenc 将此文件保存成不同的编码,比如:set fenc=utf-8之后:w把文件存成 UTF-8。

fencs 是用来在打开文件的时候进行解码的猜测列表。Vim 默认的编码猜测列表比较少,可能不能识别中文,所以需要添加对应编码方式, 顺序是以从前往后尝试,所以一定要把要求严格的编码方式放在前面,把宽松的编码方式放在后面。vimrc 参考配置如下:

set fileencodings=ucs-bom,utf-8,cp936,gb18030,latin1

enc 是 Vim 内部使用的字符编码方式。当我们设置了 encoding 之后,Vim 内部所有的 buffer、寄存器、脚本中的字符串等,全都使用这个编码。 Vim 在工作的时候,如果编码方式与它的内部编码不一致,它会先把编码转换成内部编码。enc 需要与系统编码一致,能在系统里正确地显示出来。

  • Windows 中文系统中默认内码为 cp936,可以通过 chcp 命令查看,enc 默认是 cp936 不需要改;
  • Linux下,随着系统 locale 可能设为 zh_CN.utf-8,enc 要对应的设为 utf-8。

相关工具

下面介绍一些与编码相关的工具。

vim 编码修改

如果已经用错解码方式打开了文件,可重新设置编码格式:

:edit ++enc=utf8

bash 转换编码

iconv -f gbk -t utf8 file1 -o file2 # 将一个GBK 编码的文件转换成UTF-8编码

UTF-8 vs UTF-8 BOM

在 Windows 系统中 UTF-8 文件可能多增加了一个 BOM(byte-order mark),即字节顺序标记。 使用 Vim 打开的时候,文件开头会有 3 个字节,而 Linux 系统中一般没有,我们可以统一按没有使用。

sed -i '1s/^\xEF\xBB\xBF//' orig.txt

使用上述命令可以将 BOM 删除。

xxd 16进制转换

Linux 中如果要查看文件的 16 进制表示,可以通过 xxd 工具,在 Vim 中使用 :%!xxd 即全部内容执行外部命令 xxd, 反向转换为 :%!xxd -r

参考链接

  1. chcp 命令
  2. 谈谈Unicode编码
  3. VIM显示utf-8文档乱码解决方法