练习23--字符串、字节和编码
一 数据在计算机中的存储方式
1 实质
现代计算机非常复杂,但是核心就是大量的电灯开关。计算机用电来切换开关。这些开关可以以“开”代表 1,以“关”代表 0。以前有各种各样奇怪的计算机做的不只是 1 和 0 的事情,但现在所有的计算机都是一堆 1 和 0。1 代表着运行、有电、开着、进行、存在。0 代表着结束、完成、消失、关机、没电。我们把这些 1 和 0 叫做 “比特”(bits)。
但是,一个只能让你用 1 和 0 操作的计算机将会非常低效和无聊。计算机接收了这些 1 和 0 之后,会用它们来编码更大的数字,比如用 8 个 1 和 0 来编码 256 个数(0-255)。那么编码到底是什么意思?它其实就是一个关于比特序列如何表示数字的公认标准,比如人们约定00000000 就代表数字 0,11111111 就代表数字 255,00001111 就代表数字 15。即便是在计算机诞生早期的世界级战争中,计算机也是用这些约定的 1 和 0 来做大规模计算的。
2 编码
- 字节(byte)——8 个比特(1 和 0)的序列。
- 惯例,它定义了我们对于字节的编码。
- 编码规范的发展过程:字符编码经历了ASCII码->unicode->UTF-8的演变过程
-
美国信息交换标准编码(即 ASCII 码)
-
这个标准建立了从一个数字到一个字母的映射,比如 90 是 Z,用比特的话就是 1011010,对应到计算机里面的 ASCII 码表。用 8 个比特(即一个字节)给一个字符编码,我们就可以把它们“串”(string)在一起来组成单词。
-
不过 ASCII 有一个问题,它只能编码英文以及一些相似的语言,而且一个字节只能表示 256 个数字(0-255,或者 00000000-11111111)。
-
-
-
- Unicode
-
-
- 是针对所有人类语言的“统一编码”
- 可以用 32 个比特来编码一个 Unicode 字符,这比我们能找到的所有字符可能都要多。
- 32 比特是 4 个字节,这就意味着对于大多数我们想要编码的文本会浪费很多空间。我们也可以用 16 比特(2 个字节),但仍然很浪费。
-
-
-
utf-8,即“Unicode Transformation Format 8 Bits”
-
用 8 个比特来编码大多数通用字符,然后当我们需要编码更多字符的时候再使用更多的数字。
-
它是一个把 Unicode 字符编码成字节序列(字节即比特序列,比特序列又即开关转换序列)的惯例
-
-
二 编码与解码过程详解
在 Python 中,一个字符串就是一个 UTF-8 编码的字符序列,用来显示或者进行文本操作。而字节是一些“原始”的字节序列,Python 用来存储这些 UTF-8 字符串并以 b' 开头来告诉 python 你正在处理原始字节。
- 编码过程:如果你有原始字节,那你必须用 .decode() 来获取字符串。
-
原始字节没有相关惯例,它们只是一些没有意义的数字组成的字节序列。所以你必须告诉 Python“把这些解码成 utf 字符串”。
-
-
解码过程:如果你有一个字符串,并且想要发送、保存、分享它,或者对它做一些其它的操作
-
通常情况下都可行,但是有时 Python 会扔出一个错误说它不知道如何编码。
-
其实,Python 知道它内部的惯例,它只是不知道你需要的是哪个
-
在这种情况下,你必须用 .encode() 来获取你需要的字节。DBES
-
-
DBES
-
“Decode Bytes Encode Strings”(解码字节,编码字符串),当你思考如何转换字节和字符串的时候,可以在脑子里默念“迪拜斯”(DBES 发音),有字节要字符串,解码字节,有字符串要字节,编码字符串
-
三 代码解读
1 ex23.py文件
1 import sys 2 script,encoding,error = sys.argv 3 4 def main(language_file,encoding,errors): 5 line = language_file.readline() 6 7 if line: 8 print_line(line,encoding,errors) 9 return main(language_file,encoding,errors) 10 11 def print_line(line,encoding,errors): 12 next_lang = line.strip() 13 raw_bytes = next_lang.encode(encoding,errors=errors) 14 cooked_string = raw_bytes.decode(encoding,errors=errors) 15 16 print(raw_bytes,"<==>",cooked_string) 17 18 languages = open("languages.txt",encoding="utf-8") 19 20 main(languages,encoding,error)
- 第 1-2 行: 以通常的命令行参数开始,这个你已经学过了。
- 第 4 行: 我把这段代码的主体部分定义为一个叫“main"的函数,这个函数会在脚本最后运行东西的时候被调用。
- 第 5 行:这个函数所做的第一件事就是从给出的 languages 文件中读取一行。你之前已经做过这个操作了,所以这儿没什么新内容,就像以前一样读取文本文件即可。
- 第 7 行: 现在我用了一些新东西。你将会在这本书的后半部分学到相关内容,所以把这里当作一个尝鲜吧。这是一个 if 语句,它让你在 Python 代码中做决定。你可以“测试”一个变量的真假,基于其真假,运行或者不运行这段代码。在本例中,我测试了一行中是否有内容。当readline 函数到达文件末尾的时候,它会返回空字符串,if 这一行就是为了测试这个空字符串。只要 readline 给了我们一些东西,结果就会是 true ,后面的代码就会运行(比如缩进的9-10 行),当结果是 false 的时候, python 就会跳过 9-10 行。
- 第 8 行: 然后我调用了一个单独的函数来做这一行的真正打印。这简化了我的代码,并且让我更容易理解。如果我想学习这个函数的作用,我可以跳到那儿进行学习。一旦我知道了print_line 是做什么的,我就可以把我的记忆附到 print_line 这个名称下,然后忘掉细节。
- 第 9 行: 我在这儿写了一小段非常神奇的代码。我在 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 变量是一样的。
- 第 18 行 我已经定义完了所有函数,现在我想打开 languages.txt 文件。
- 第 20 行 在这个脚本的结尾只是用所有正确的参数运行了 main 函数,以保证一切正常运行,避免循环。记住这个之后会跳转到第 5 行 main 函数被定义的地方,然后在第 10 行又被调用了一次,会造成它的循环。不过第 8 行的 if 语句又会阻止它无限循环。
ex23.py 脚本其实就是把字节写在 b' ' 里面,然后把它们转换成 UTF-8 编码(或者其他你设定的编码)。左边是每一个 utf-8 字节对应的数字,右边是 utf-8 实际输出的字符。之所以这样呈现,是为了让你明白 <===> 左边是 Python 用来存储字符串的数字字节或者“原始”(raw)字节,设置 b' ' 是为了告诉 Python 这是“字节”(bytes)。这些原始字节之后被“加工”(cooked)然后显示在右边,以便让你看到你的终端呈现出来的真正的字符。