0x00 前言
有几个概念容易混淆:
Unicode
:指的是字符集。其为每一个文字符号分配了唯一的标识UTF-8、UTF-16
:指的是编码方式。其将字符集中的文字标识编码为用于传输或存储的字节序列GBK、GB2312
:为国家规范。既包含了中文字符集,同时也包含了相应的编码方式
0x01 Unicode
Unicode 对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。
对于 Unicode 中的每个字符,Unicode 为其分配了唯一的字符标识,即码位(Code Point),在 Unicode 标准中以前缀 U+
表示,Unicode 中定义的区域为 U+0000 ~ U+10FFFF
(两到三个字节)。
Unicode 的实现方式不同于编码方式。一个字符的 Unicode 编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式有所不同。Unicode 的实现方式称为 Unicode 转换格式(Unicode Transformation Format,简称为 UTF)。
0x02 UTF-8
UTF-8 是一种针对 Unicode 的,码元长度为 8 比特的变长编码方式。U+0080
的以下字符都使用内含其字符的单字节编码,这些编码正好对应 7 比特的 ASCII 字符。
代码范围 | UTF-8 |
---|---|
000000 - 00007F | 0zzzzzzz (00-7F) |
000080 - 0007FF | 110yyyyy (C0-DF) 10zzzzzz (80-BF) |
000800 - 00FFFF | 1110xxxx (E0-EF) 10yyyyyy 10zzzzzz |
UTF-8 以字节为编码单元,它的字节顺序在所有系统中都是一様的,没有字节序的问题,也因此它实际上并不需要 BOM。
0x03 UTF-16
UTF-16 也是一种变长编码方式,不过其码元长度为 16,且无法兼容 ASCII。UTF-16 的编码方式相对来说就比较复杂了,由于其码元的长度为双字节,所以涉及到字节序的问题。也因此,UTF-16 分为 UTF-16LE 和 UTF-16BE。
在 UTF-16 文件的开头,都会放置一个 U+FEFF
字符作为字节序标识(UTF-16LE 以 FF FE
代表,UTF-16BE 以 FE FF
代表)。其中 U+FEFF
字符在 Unicode 中代表的意义是 ZERO WIDTH NO-BREAK SPACE
,顾名思义,它是个没有宽度也没有断字的空白。
字符 | 普通二进制 | UTF-16 |
---|---|---|
U+0024 | 0000 0000 0010 0100 | 0000 0000 0010 0100 |
U+10437 | 0001 0000 0100 0011 0111 | 1101 1000 0000 0001 1101 1100 0011 0111 |
0x04 BOM
BOM 即为字节顺序标记(Byte-Order Mark),是位于码位 U+FEFF
的 Unicode 字符的名称。其一般出现与文件的开头,用于表明文件内容的字节序。
编码 | 表示(十六进制) | 表示(十进制) |
---|---|---|
UTF-8 | EF BB BF | 239 187 191 |
UTF-16BE | FE FF | 254 255 |
UTF-16LE | FF FE | 255 254 |
UTF-32BE | 00 00 FE FF | 0 0 254 255 |
UTF-32LE | FF FE 00 00 | 255 254 0 0 |
UTF-7 | 2B 2F 76和以下的一个字节:[ 38 | 39 | 2B | 2F ] | 43 47 118和以下的一个字节:[ 56 | 57 | 43 | 47 ] |
UTF-1 | F7 64 4C | 247 100 76 |
UTF-EBCDIC | DD 73 66 73 | 221 115 102 115 |
Unicode 标准压缩方案 | 0E FE FF | 14 254 255 |
BOCU-1 | FB EE 28 及可能跟随着 FF | 251 238 40 及可能跟随着 255 |
GB-18030 | 84 31 95 33 | 132 49 149 51 |
0x05 Python 中的编码问题
Python 中常见编码的问题主要有三个,分别为 UnicodeEncodeError
、UnicodeDecodeError
和 SyntaxError
:
UnicodeEncodeError
发生在将字符串转换为二进制序列的过程中。可以在.encode()
方法中指定errors
参数的方式来指定当前编码方式对未知字符该如何处理(ignore
、replace
、xmlcharrefreplace
)。UnicodeDecodeError
发生在将二进制序列转换为字符串的过程中。同样何在.decode()
方法中指定errors
参数来指定当前解码方式对未知字节的处理方式。也可使用 Python 的编码侦测库 Chardet 来检测未知的字节序列所采用的编码方式(仅针对部分流行编码)。SyntaxError
主要发生在源码的编码与预期不符,可通过在代码开始处指定编码方式解决:
# coding: utf-8
或者
# -*- coding: utf-8 -*-
编码默认值
《流畅的 Python》中有这样一段代码,可用于查看当前编码的默认值:
import sys, locale
expressions = """
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""
my_file = open('dummy', 'w')
for expression in expressions.split():
value = eval(expression)
print(expression.rjust(30), '->', repr(value))
其中 locale.getpreferredencoding()
的返回值最重要,其表示打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr
的默认编码。
sys.getfilesystemencoding()
用于编解码文件名(非文件内容),将最终的字节序列传递给系统 API。
sys.getdefaultencoding()
用于 Python 内部字节序列与字符串的转换。
在 Linux 中默认编码方式一般都为 UTF-8,而在 Windows 则不同,也因此在 Windows 中更容易遇到编码问题。
Unicode 三明治
处理文本的最佳实践是“Unicode 三明治”。对输入来说,要尽早把输入的字节序列解码成字符串。在程序的业务逻辑中只能处理字符串对象。对输出来说,要尽量晚地把字符串编码成字节序列。依赖默认编码可能会遇到麻烦,因此最好显示地指定编码方式。
Python 2 与 3
由于 Python 2 一开始没有考虑到其他语言的需求,其默认编码为 ascii
,处理对象为 str
。Python 2 中的 str
与 Python 3 中的 str
并不相同,二者所处的位置可以说是相对的。Python 2 中的 str
对象存储字节码,而 Python 3 中的 str
对象存储 Unicode 码。
在 Python 2.x 中:
内存中处理的对象为 str
类型(每个符号为字节)。转换关系如下:
str --- decode ---> unicode
unicode --- encode ---> str
在 Python 3.x 中:
内存中处理的对象为 str
类型(每个符号为 Unicode 码),涉及 IO 操作时处理的对象为 bytes
类型。转换关系如下:
bytes --- decode ---> str
str --- encode ---> bytes
0x06 Windows API 中的编码问题
曾经,Windows NT 面对国际化的需求采用了 UTF-16 作为系统字符编码(据说当时还没有 UTF-8)。
因此,大部分的 Windows API 都有两种版本,一种是 ANSI 版,一种是宽字符版:
...A()
:接受 char 类型参数...W()
:接受 wchar_t 类型参数(UTF-16)
以 MessageBox()
为例:
int MessageBoxA(HWND hWnd, const char* lpText, const char* lpCaption, unsigned int uType);
int MessageBoxW(HWND hWnd, const wchar_t* lpText, const wchar_t* lpCaption, unsigned int uType);
同时,还有一个没有后缀的 API,其具体实现取决于 UNICODE
宏是否定义:
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
为了迎合这个没有后缀的 API,用同样的方式定义了一个字符类型 TCHAR
:
#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif
参考资料
- https://zh.wikipedia.org/zh-hans/Unicode
- https://zh.wikipedia.org/wiki/UTF-8
- https://zh.wikipedia.org/wiki/UTF-16
- https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F
- https://www.amazon.cn/dp/B072HMKKPG
- https://stackoverflow.com/questions/3298569/difference-between-mbcs-and-utf-8-on-windows
- https://stackoverflow.com/questions/3298569/difference-between-mbcs-and-utf-8-on-windows

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。