编码及字符串

你是否知道 Bougainville 人有世界上最小的字母表?他们的 Rotokas 字母表只包含了12个字母: A, E, G, I, K, O, P, R, S, T, U, 和 V。另一方面,像汉语,日语和韩语这些语言,它们则有成千上万个字符。当然啦,英语共有26个字母 — 如果把大写和小写分别计算的话,52个 — 外加少量的标点符号,比如!@#$%&

当人们说起“文本”,他们通常指显示在屏幕上的字符或者其他的记号;但是计算机不能直接处理这些字符和标记;它们只认识位(bit)和字节(byte)。实际上,从屏幕上的每一块文本都是以某种字符编码(character encoding)的方式保存的。粗略地说就是,字符编码提供一种映射,使屏幕上显示的内容和内存、磁盘内存储的内容对应起来。有许多种不同的字符编码,有一些是为特定的语言,比如俄语、中文或者英语,设计、优化的,另外一些则可以用于多种语言的编码。

在实际操作中则会比上边描述的更复杂一些。许多字符在几种编码里是共用的,但是在实际的内存或者磁盘上,不同的编码方式可能会使用不同的字节序列来存储他们。所以,你可以把字符编码当做一种解码密钥。当有人给你一个字节序列 — 文件,网页,或者别的什么 — 并且告诉你它们是“文本”时,就需要知道他们使用了何种编码方式,然后才能将这些字节序列解码成字符。如果他们给的是错误的“密钥”或者根本没有给你“密钥”,那就得自己来破解这段编码,这可是一个艰难的任务。有可能你使用了错误的解码方式,然后出现一些莫名其妙的结果。 你所了解的关于字符串的知识都是错的。

你肯定见过这样的网页,在撇号(')该出现的地方被奇怪的像问号的字符替代了。这种情况通常意味着页面的作者没有正确的声明其使用的编码方式,浏览器只能自己来猜测,结果就是一些正确的和意料之外的字符的混合体。如果原文是英语,那只是不方便阅读而已;在其他的语言环境下,结果可能是完全不可读的。

现有的字符编码各类给世界上每种主要的语言都提供了编码方案。由于每种语言的各不相同,而且在以前内存和硬盘都很昂贵,所以每种字符编码都为特定的语言做了优化。上边这句话的意思是,每种编码都使用数字(0–255)来代表这种语言的字符。比如,你也许熟悉ASCII编码,它将英语中的字符都当做从0–127的数字来存储。(65表示大写的“A”,97表示小写的“a”,&c。)英语的字母表很简单,所以它能用不到128个数字表达出来。如果你懂得2进制计数的话,它只使用了一个字节内的7位。

西欧的一些语言,比如法语,西班牙语和德语等,比英语有更多的字母。或者,更准确的说,这些语言含有与变音符号(diacritical marks)组合起来的字母,像西班牙语里的ñ。这些语言最常用的编码方式是CP-1252,又叫做“windows-1252”,因为它在微软的视窗操作系统上被广泛使用。CP-1252和ASCII在0–127这个范围内的字符是一样的,但是CP-1252为ñ(n-with-a-tilde-over-it, 241),Ü(u-with-two-dots-over-it, 252)这类字符而扩展到了128–255这个范围。然而,它仍然是一种单字节的编码方式;可能的最大数字为255,这仍然可以用一个字节来表示。

然而,像中文,日语和韩语等语言,他们的字符如此之多而不得不需要多字节编码的字符集。即,使用两个字节的数字(0–255)代表每个“字符”。但是就跟不同的单字节编码方式一样,多字节编码方式之间也有同样的问题,即他们使用的数字是相同的,但是表达的内容却不同。相对于单字节编码方式它们只是使用的数字范围更广一些,因为有更多的字符需要表示。

在没有网络的时代,“文本”由自己输入,偶尔才会打印出来,大多数情况下使用以上的编码方案是可行的。那时没有太多的“纯文本”。源代码使用ASCII编码,其他人也都使用字处理器,这些字处理器定义了他们自己的格式(非文本的),这些格式会连同字符编码信息和风格样式一起记录其中,&c。人们使用与原作者相同的字处理软件读取这些文档,所以或多或少地能够使用。

现在,我们考虑一下像email和web这样的全球网络的出现。大量的“纯文本”文件在全球范围内流转,它们在一台电脑上被撰写出来,通过第二台电脑进行传输,最后在另外一台电脑上显示。计算机只能识别数字,但是这些数字可能表达的是其他的东西。Oh no! 怎么办呢。。好吧,那么系统必须被设计成在每一段“纯文本”上都搭载编码信息。记住,编码方式是将计算机可读的数字映射成人类可读的字符的解码密钥。失去解码密钥则意味着混乱不清的,莫名其妙的信息,或者更糟。

现在我们考虑尝试把多段文本存储在同一个地方,比如放置所有收到邮件的数据库。这仍然需要对每段文本存储其相关的字符编码信息,只有这样才能正确地显示它们。这很困难吗?试试搜索你的email数据库,这意味着需要在运行时进行编码之间的转换。很有趣是吧…

现在我们来分析另外一种可能性,即多语言文档,同一篇文档里来自几种不同语言的字符混在一起。(提示:处理这样文档的程序通常使用转义符在不同的“模式(modes)”之间切换。噗!现在是俄语 koi8-r 模式,所以241代表 Я;噗噗!现在到了Mac Greek模式,所以241代表 ώ。)当然,你也会想要搜索这些文档。

现在,你就哭吧,因为以前所了解的关于字符串的知识都是错的,根本就没有所谓的“纯文本”。


Unicode#

Unicode入门。

Unicode编码系统为表达任意语言的任意字符而设计。它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。每个数字代表唯一的至少在某种语言中使用的符号。(并不是所有的数字都用上了,但是总数已经超过了65535,所以 2个字节的数字是不够用的。)被几种语言共用的字符通常使用相同的数字来编码,除非存在一个在理的语源学(etymological)理由使不这样做。不考虑这种情况的话,每个字符对应一个数字,每个数字对应一个字符。即不存在二义性。不再需要记录“模式”了。U+0041总是代表'A',即使这种语言没有'A'这个字符。

初次面对这个创想,它看起来似乎很伟大。一种编码方式即可解决所有问题。文档可包含多种语言。不再需要在各种编码方式之间进行“模式转换“。但是很快,一个明显的问题跳到我们面前。4个字节?只为了单独一个字符‽ 这似乎太浪费了,特别是对像英语和西语这样的语言,他们只需要不到1个字节即可以表达所需的字符。事实上,对于以象形为基础的语言(比如中文)这种方法也有浪费,因为这些语言的字符也从来不需要超过2个字节即可表达。

有一种Unicode编码方式每1个字符使用4个字节。它叫做UTF-82,因为32位 = 4字节。UTF-32是一种直观的编码方式;它收录每一个Unicode字符(4字节数字)然后就以那个数字代表该字符。这种方法有其优点,最重要的一点就是可以在常数时间内定位字符串里的第N个字符,因为第N个字符从第4×Nth个字节开始。另外,它也有其缺点,最明显的就是它使用4个“诡异”的字节来存储每个“诡异”的字符…

尽管有Unicode字符非常多,但是实际上大多数人不会用到超过前65535个以外的字符。因此,就有了另外一种Unicode编码方式,叫做UTF-16(因为16位 = 2字节)。UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的“星芒层(astral plane)” 内超过这65535范围的Unicode字符,则需要使用一些诡异的技巧来实现。UTF-16编码最明显的优点是它在空间效率上比UTF-32高两倍,因为每个字符只需要2个字节来存储(除去65535范围以外的),而不是UTF-32中的4个字节。并且,如果我们假设某个字符串不包含任何星芒层中的字符,那么我们依然可以在常数时间内找到其中的第N个字符,直到它不成立为止这总是一个不错的推断…

但是对于UTF-32和UTF-16编码方式还有一些其他不明显的缺点。不同的计算机系统会以不同的顺序保存字节。这意味着字符U+4E2D在UTF-16编码方式下可能被保存为4E 2D或者2D 4E,这取决于该系统使用的是大尾端(big-endian)还是小尾端(little-endian)。(对于UTF-32编码方式,则有更多种可能的字节排列。)只要文档没有离开你的计算机,它还是安全的 — 同一台电脑上的不同程序使用相同的字节顺序(byte order)。但是当我们需要在系统之间传输这个文档的时候,也许在万维网中,我们就需要一种方法来指示当前我们的字节是怎样存储的。不然的话,接收文档的计算机就无法知道这两个字节4E 2D表达的到底是U+4E2D还是U+2D4E。

为了解决这个问题,多字节的Unicode编码方式定义了一个“字节顺序标记(Byte Order Mark)”,它是一个特殊的非打印字符,你可以把它包含在文档的开头来指示你所使用的字节顺序。对于UTF-16,字节顺序标记是U+FEFF。如果收到一个以字节FF FE开头的UTF-16编码的文档,你就能确定它的字节顺序是单向的(one way)的了;如果它以FE FF开头,则可以确定字节顺序反向了。

不过,UTF-16还不够完美,特别是要处理许多ASCII字符时。如果仔细想想的话,甚至一个中文网页也会包含许多的ASCII字符 — 所有包围在可打印中文字符周围的元素(element)和属性(attribute)。能够在常数时间内找到第Nth个字符当然非常好,但是依然存在着纠缠不休的星芒层字符的问题,这意味着你不能保证每个字符都是2个字节长,所以,除非你维护着另外一个索引,不然就不能真正意义上的在常数时间内定位第N个字符。另外,朋友,世界上肯定还存在很多的ASCII文本…

另外一些人琢磨着这些问题,他们找到了一种解决方法:UTF-8

UTF-8 The range of integers used to code the abstract characters is called the codespace. A particular integer in this set is called a code point. When an abstract character is mapped or assigned to a particular code point in the codespace, it is then referred to as an encoded character. <-->

UTF-8是一种为Unicode设计的变长(variable-length)编码系统。即,不同的字符可使用不同数量的字节编码。对于ASCII字符(A-Z, &c.)UTF-8仅使用1个字节来编码。事实上,UTF-8中前128个字符(0–127)使用的是跟ASCII一样的编码方式。像ñ和ö这样的“扩展拉丁字符(Extended Latin)”则使用2个字节来编码。(这里的字节并不是像UTF-16中那样简单的Unicode编码点(unicode code point);它使用了一些位变换(bit-twiddling)。)中文字符比如“中”则占用了3个字节。很少使用的“星芒层字符”则占用4个字节。

缺点:因为每个字符使用不同数量的字节编码,所以寻找串中第N个字符是一个O(N)复杂度的操作 — 即,串越长,则需要更多的时间来定位特定的字符。同时,还需要位变换来把字符编码成字节,把字节解码成字符。

优点:在处理经常会用到的ASCII字符方面非常有效。在处理扩展的拉丁字符集方面也不比UTF-16差。对于中文字符来说,比UTF-32要好。同时,(在这一条上你得相信我,因为我不打算给你展示它的数学原理。)由位操作的天性使然,使用UTF-8不再存在字节顺序的问题了。一份以UTF-8编码的文档在不同的计算机之间是一样的比特流。

 

摘自:深入Python3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值