The note of String, byte and character encodings
Based on Learn Python3 in the hard way. By Zed A. Shaw
1.例子:
下载一个名为 languages.txt 的文本文件。(下载地址: https://learnpythonthehardway.org/python3/languages.txt,点开,右键,“另存为” txt 格式,放在你的练习文件夹,再打开。)
Code:
import sys
script, encoding, error = sys.argv
def main(language_file, encoding, errors):
line = language_file.readline()
if line:
print_line(line, encoding, errors)
return main(language_file, encoding, errors)
def print_line(line, encoding, errors):
next_lang = line.strip()
raw_bytes = next_lang.encode(encoding, errors=errors)
cooked_string = raw_bytes.decode(encoding, errors=errors)
print(raw_bytes, "<===>", cooked_string)
languages = open("languages.txt", encoding="utf-8")
main(languages, encoding, error)
这些例子用了 utf-8 、utf-16 和 big5 编码来说明这种转换,以及你可能会遇到的错误类型。这些名字在 Python 3 中被称为 “codec”(编码器),但是你要用参数“encoding”。
2.解释:
2.1. 数字逻辑(数字电路)相关:
现代计算机非常复杂,但是核心就是大量的电灯开关。计算机用电来切换开关。这些开关可以以“开”代表 1,以“关”代表 0。以前有各种各样奇怪的计算机做的不只是 1 和 0 的事情,但现在所有的计算机都是一堆 1 和 0。1 代表着运行、有电、开着、进行、存在。0 代表着结束、完成、消失、关机、没电。我们把这些 1 和 0 叫做 “比特”(bits)。
那么编码到底是什么意思?它其实就是一个关于比特序列如何表示数字的公认标准,比如人们约定 00000000 就代表数字 0,11111111 就代表数字 255,00001111 就代表数字 15。现在我们把一个“字节”(byte)称为 8 个比特(1 和 0)的序列(0 -> 255)。
2.2. ASCII && Unicode
一旦你有了字节,你就可以开始存储和显示文本了,不过要用另一种惯例来让数字映射(map)成文字。美国信息交换标准编码(即 ASCII 码)成为最流行的惯例。
可以在 Python 里面试试这个(Windows Powershell 输入 python ,然后回车):
首先,我用二进制写了数字 90,然后我基于字母 'Z' 得到了对应的数字,接着我把这个数字转化成字母 'Z' 。
>>> 0b1011010
90
>>> ord ( ' Z ' )
90
>>> chr ( 90 )
' Z '
>>>
但是,ASCII 有一个问题,它只能编码英文以及一些相似的语言,而且一个字节只能表示 256 个数字(0-255,或者 00000000-11111111)。很显然,世界上正在使用的语言远远超过 256 个字符。因此不同国家创建了针对他们自己语言的编码惯例,虽然这些都管用,但是它们只适用一种语言。这就意味着,如果你想把一本英语书的书名放在一个泰语句子中,就会比较麻烦,你就需要一个泰语编码和一个英语编码。
为了解决这个问题,一群人创建了 Unicode,也就是针对所有人类语言的“统一编码”(Universal encoding)。Unicode 提供的解决方案跟 ASCII 码表类似,但是相比之下,前者更大。你可以用 32 个比特来编码一个 Unicode 字符,这比我们能找到的所有字符可能都要多。
我们现在有了针对任何字符的编码协定,但是 32 比特是 4 个字节,这就意味着对于大多数我们想要编码的文本会浪费很多空间。我们也可以用 16 比特(2 个字节),但仍然很浪费。因此后来出现了一种很妙的惯例:用 8 个比特来编码大多数通用字符,然后当我们需要编码更多字符的时候再使用更多的数字。这意味着我们有了一种压缩(compression)编码惯例,使得用 8 个比特来编码大多数常用字符,并在需要时切换成 16 或 32 个比特这件事成为可能。
2.3. 分析结果
ex23.py 脚本其实就是把字节写在 b' ' 里面,然后把它们转换成 UTF-8 编码(或者其他你设定的编码)。左边是每一个 utf-8 字节对应的数字,右边是 utf-8 实际输出的字符。之所以这样呈现,是为了让你明白 <===> 左边是 Python 用来存储字符串的数字字节或者“原始”(raw)字节,设置 b' ' 是为了告诉 Python 这是“字节”(bytes)。这些原始字节之后被“加工”(cooked)然后显示在右边,以便让你看到你的终端呈现出来的真正的字符。
2.4. 分析之前的code
import sys
script, encoding, error = sys.argv
def main(language_file, encoding, errors):
line = language_file.readline()
if line:
print_line(line, encoding, errors)
return main(language_file, encoding, errors)
def print_line(line, encoding, errors):
next_lang = line.strip()
raw_bytes = next_lang.encode(encoding, errors=errors)
cooked_string = raw_bytes.decode(encoding, errors=errors)
print(raw_bytes, "<===>", cooked_string)
languages = open("languages.txt", encoding="utf-8")
main(languages, encoding, error)
第 1-2 行: 以通常的命令行参数开始
第 5 行: 将代码的主体部分定义为一个叫“main"的函数,这个函数会在脚本最后运行的时候被调用。
第 6 行:这个函数所做的第一件事就是从给出的 languages 文件中读取一行。
第 8 行:这是一个 if 语句,它让你在 Python 代码中做决定。你可以“测试”一个变量的真假,基于其真假,运行或者不运行这段代码。在本例中,我测试了一行中是否有内容。当 readline 函数到达文件末尾的时候,它会返回空字符串,if 这一行就是为了测试这个空字符串。只要 readline 给了我们一些东西,结果就会是 true ,后面的代码就会运行(比如缩进的 9-10 行),当结果是 false 的时候, python 就会跳过 9-10 行。
第 9 行: 然后调用了一个单独的函数打印出那一行。简化了代码,并且更容易理解。一旦我知道了 print_line 是做什么的,我就可以把我的记忆附到 print_line 这个名称下,然后忘掉细节。
第 10 行: 在这儿写了一小段非常神奇的代码。main 函数内部又调用了 main 函数。如果一个叫 main 的函数只是跳到顶部,而我在这个函数的底部调用它,它就会回到顶部然后再次运行,这样就会形成一个循环(loop)。现在看第 8 行,你会看到 if 语句避免了这个函数无限循环。
第 11 行 现在开始定义 print_line 函数,它用来编码 languages.txt 文件中的每一行内容。
第 13 行 现在终于获得了从 languages.txt 中收到的语言,并把它们编码成原始字节。“DBES” à “Decode Bytes, Encode Strings”,解码字节,编码字符串。next_lang 变量是一个字符串,因此要获得原始字节,我必须对它调用 .encode() 函数来“编码字符串”。我把我想要的编码以及如何处理错误传递给 encode() 。
第 14 行 然后做了额外一步,通过从 raw_bytes 创建一个 cooked_string 变量来逆向展示第 15 行。记住,“DBES”说的是“解码字节”,raw_bytes 是字节,所以我对它调用了 .decode() 来获取一个 python 字符串。这个字符串应该和 next_lang 变量是一样的。(encode && decode)
第 15 行 我已经定义完了所有函数,现在打开 languages.txt 文件。
第 16 行 在这个脚本的结尾只是用所有正确的参数运行了 main 函数,以保证一切正常运行,避免循环。记住这个之后会跳转到第 5 行 main 函数被定义的地方,然后在第 10 行又被调用了一次,会造成它的循环。不过第 8 行的 if 语句又会阻止它无限循环。