再次总结字符编码问题

encode and decode

encode 是把明文编码为密文;decode 是把密文解码为明文。

这里的明文是什么呢?明文是相对意义上的。对大部分中国人而言,中文和英文或许明文,但葡萄牙语大概率上是密文。

在计算机领域,最开始的明文只有英文,对应的 encode 方法是 ascii。随着计算机进入其它国家,出现了各种互不相容的编码方式。

在互不相容的编码方式之间进行转换是一件非常琐碎的事情。

ascii gbk unicode utf-8

ascii 是最早的编码规范,由于只针对一个字节,所以规范只对应一个实现方法。

但是随着计算机扩展到全球各个国家和地区,一个字节已经无法容纳人类使用的字符串,尤其是单单东亚地区的「汉字」总量就有 10 万多。这种情况下,别说一个字节,两个字节也无法容纳这么多字符。

gbk 类编码一开始中国等地区自己搞出来的针对汉字的编码规范和编码实现。

在 90 年的时候,出现了 unicode 编码规范。这套编码规范只是给出了一套字符集,并未指定具体的编码实现方式。

一个 unicode 字符用一个唯一的 4 位 16进制数字表示。

在具体实现时,使用四个字节一一对应是最直接的方法:utf-32。但是也非常耗费存储空间。

后来出现了 utf-16,utf-8。

现在至少在互联网领域,utf-8 已经成为事实上的 unicode 编码实现标准。

Python 中字符串代表的意思

有了 unicode 字符集之后,编程语言中的字符串的表示方式就有了大概两类方法:

在 Python2 中的字符串默认表示为编码后的二进制字节码,而且默认为 ascii 编码方式,后来为了支持国际化,要在头部声明使用 utf-8。

在 Python3 中,字符串才是真正意义上的字符串:unicode 字符集。字符串中每一个字符一一对应于字符集中的一个字符。

所以,unicode 字符集是明文,用某种具体的编码方式编码的行为就是 encode;将某种编码方式下的二进制字节码解码为 unicode 字符集的行为就是 decode。

识别文件流的编码方案

对「编码」有了一个具体的理解之后,最要紧的事情就变成了如何识别「现有的二进制字节流」的编码方案。

毕竟,计算机的一切都是二进制数字,unicode 字符也不例外。

其它语言中的字符串

Java:和 Python3 一样,在程序环境中默认使用 unicode 字符集表示字符串。

Go:在Go语言中,没有字符类型,字符类型是rune类型,rune是int32的别称。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。

UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。

幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。

总结起来,Go 中表示字符串的方式和声明了国际化支持之后的 Python2 类似,但做了一些更方便的支持。

C / C++:和 Python2 中的默认方式一致,而且无法直接声明国际化,转换 unicode 需要库的支持。

JS:自诞生以来就采用Unicode字符集,但是只支持一种编码方法,用的是UCS-2(因为当时只有这种编码实现。)!由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。JavaScript 的字符函数都受到这一点的影响,无法返回正确结果。所以在 JS 中处理大量字符串问题时,需要注意编码问题。

JavaScript的下一个版本ECMAScript 6(简称ES6),大幅增强了Unicode支持,基本上解决了这个问题。

URL:”只有字母和数字、一些特殊符号、以及某些保留字,才可以不经过编码直接用于URL。

这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致”URL编码”成为了一个混乱的领域。

现在大致有四种不同的情况,在每一种情况中,浏览器的URL编码方法都不一样。清除差异之后,可以学习使用 Javascript 提供的统一的编码方法。

JS 统一编码:

HTML:在元信息中声明编码方式。

unicode 补遗

Unicode只规定了每个字符的码点,到底用什么样的字节序表示这个码点,就涉及到编码方法。很明显,unicode 可以有多种编码方式。

JAVA 的 String 对象是以 Unicode 编码存在的,所以 JAVA 程序员主要关心的是读入时判断字节流的编码,从而确保可以正确的转化为 Unicode 编码;相比之下,C/C++ 将外部文件读出的数据存为字符数组、或者是 string 类型;而 wstring 才是符合 Unicode 编码的双字节数组。一般常用的方法是 C 标准库的 wcstombs、mbstowcs 函数,和 windows API 的 MultiByteToWideChar 与 WideCharToMultiByte 函数来完成向 Unicode 的转入和转出。

编码选择:多国语言环境的编程,以使用 UTF 编码为原则,减少字符集转换。

string 并不包含明文编码信息,但是编码确定了 string 的二进制表示。

读写一致:读入时使用的字符集要与写出时使用的一致。如果不需要改变字符串内容,仅仅是将字符串读入、再写出,建议不要调整任何字符集——即使程序使用的系统默认字符集 A 与文件的实际编码 B 不符合,写出的字符串依然会是正确的 B 编码。(输入和输出正好默认实现了逆转换)

读入已知:对于必须处理、解析或显示的字符串,从文件读入时必须知道它的编码,避免处理字符串的代码简单使用系统默认字符集;即便对于程序从系统中收集到的内存字符串,也应知道其符合的编码格式——一般为系统默认字符集。

尽量避免在文件中直接使用 Unicode:这里是说将非 ASCII 编码的 16 进制或者 10 进制数值用 &# 与 ; 包含起来的使用方式,例如将中文“一”写成“&#4e00;”。这种方法的实质是 Unicode 编码直接写入文件。这不仅会降低代码的通用性、输出文件的可读性,处理起来也很困难。比如法文字符在其他字符集中是大于 80H 的单字节字符,程序同时要支持中文的时候,很有可能会将多字节的中文字符错误割裂。

避免陷入直接的字符集编程:国际化、本地化的工具已经比较成熟,非纯粹做编码转换的程序员没有必要自己去处理不同编码表的映射转换问题。

Unicode/UTF8 并不能解决一切乱码问题:Unicode 可以说是将世界上所有语言字符统一起来的一套编码。但是这并不意味着在一个系统中可以正常显示的按照 UTF8 编码的文件,在另一个系统中也可以正常显示。例如,在中文的 UTF8 编码或者 Unicode 编码在没有东亚语言包支持的法文系统中,依然是不可识别的乱码——尽管 UTF8、Unicode 它们都支持。因为人类使用的字符集符号还需要计算机系统提供支持。


不知是该恭喜,还是该怎样,总之阅读到该文的,你是第 人。每一次刷新,都是不同的自己。