专栏名称: VPointer
软件开发
目录
相关文章推荐
一条  ·  她30岁登顶亚洲,首次来沪一票难求 ·  3 天前  
广州日报  ·  大S今日正式安葬 ·  昨天  
广州日报  ·  大S今日正式安葬 ·  昨天  
漳视新闻  ·  开建!九龙公园游乐园,要变成这样! ·  昨天  
漳视新闻  ·  开建!九龙公园游乐园,要变成这样! ·  昨天  
51好读  ›  专栏  ›  VPointer

Python学习之路23-文本和字节序列

VPointer  · 掘金  ·  · 2018-06-18 09:54

正文

Python学习之路23-文本和字节序列

《流畅的Python》笔记。

本篇主要讲述不同编码之间的转换问题,比较繁杂,如果平时处理文本不多,或者语言比较单一,没有多语言文本处理的需求,则可以略过此篇。

1. 前言

本篇主要讲述Python对文本字符串的处理。主要内容如下:

  • 字符集基本概念以及Unicode;
  • Python中的字节序列;
  • Python对编码错误的处理以及BOM;
  • Python对文本文件的编解码,以及对Unicode字符的比较和排序,而这便是 本篇的主要目的
  • 双模式API和Unicode数据库

如果对字符编码很熟悉,也可直接跳过第2节。

2. 字符集相关概念

笔者在初学字符集相关内容的时候,对这个概念并没有什么疑惑:字符集嘛,就是把我们日常使用的字符(汉子,英文,符号,甚至表情等)转换成为二进制嘛,和摩斯电码本质上没啥区别,用数学的观点就是一个函数变换,这有什么好疑惑的?直到后来越来也多地接触字符编码,终于,笔者被这堆概念搞蒙了:一会儿Unicode编码,一会儿又Unicode字符集,UTF-8编码,UTF-16字符集还有什么字符编码、字节序列。到底啥时候该叫“编码”,啥时候该叫“字符集”?这些概念咋这么相似呢?既然这么相似,干嘛取这么多名字?后来仔细研究后发现,确实很多学术名次都是同义词,比如“字符集”和“字符编码”其实就是同义词;有的译者又在翻译外国的书的时候,无意识地把一个概念给放大或者给缩小了。

说到这不得不吐槽一句,我们国家互联网相关的图书质量真的低。国人自己写的IT方面的书,都不求有多经典,能称为好书的都少之又少;而翻译的书,要么翻译得晦涩难懂,还不如直接看原文;要么故作风骚,非得体现译者的文学修养有多“高”;要么生造名词,同一概念同一单词,这本书里你翻译成这样,另一本书里我就偏要翻译成那样(你们这是在翻译小说吗)。所以劝大家有能力的话还是直接看原文吧,如果要买译本,还请大家认真比较比较,否则读起来真的很痛苦。

回到主题,我们继续讨论字符集相关问题。翻阅网上大量资料,做出如下总结。

2.1 基本概念

始终记住编码的核心思想:就是 给每个字符都对应一个二进制序列 ,其他的所有工作都是让这个过程更规范,更易于管理。

现代编码模型将这个过程分了5个层次,所用的术语列举如下(为了避免混淆,这里不再列出它们的同义词):

  1. 抽象字符表 (Abstract character repertoire):

    系统支持的所有抽象字符的集合。可以简单理解为人们使用的文字、符号等。

    这里需要注意一个问题:有些语系里面的字母上方或者下方是带有特殊符号的,比如一点或者一撇;有的字符表里面会将字母和特殊符号组合成一个新的字符,为它单独编码;有的则不会单独编码,而是字母赋予一个编码,特殊符号赋予一个编码,然后当这俩在文中相遇的时候再将这俩的编码组合起来形成一个字符。后面我们会谈到这个问题,这也是以前字符编码转换常出毛病的一个原因。

    提醒 :虽然这里扯到了编码,但 抽象字符表 这个概念还和编码没有联系。

  2. 编码字符集 (Coded Character Set, CCS ):字符 --> 码位

    首先给出总结: 编码字符集就是用数字代替抽象字符集中的每一个字符!

    将抽象字符表中的每一个字符映射到一个坐标(整数值对:(x, y),比如我国的GBK编码)或者表示为一个非负整数N,便生成了编码字符集。与之相应的还有两个 抽象 概念: 编码空间 (encoding space)、 码位 (code point)和 码位值 (code point value)。

    简单的理解,编码空间就相当于许多空位的集合,这些空位称之为码位,而这个码位的坐标通常就是码位值。我们将抽象字符集中的字符与码位一一对应,然后用码位值来代表字符。以二维空间为例,相当于我们有一个10万行的表,每一行相当于一个码位,二维的情况下,通常行号就是码位值(当然你也可以设置为其他值),然后我们把每个汉字放到这个表中,最后用行号来表示每一个汉字。**一个编码字符集就是把抽象字符映射为码位值。**这里区分码位和码位值只是让这个映射的过程更形象,两者类似于座位和座位号的区别,但真到用时,并不区分这两者,以下两种说法是等效的:

    字符A的码位是123456
    字符A的码位值是123456(很少这么说,但有这种说法)
    

    编码空间并不只能是二维的,它也可以是三维的,甚至更高,比如当你以二维坐标(x, y)来编号字符,并且还对抽象字符集进行了分类,那么此时的编码空间就 可能 是三维的,z坐标表示分类,最终使用(x, y, z)在这个编码空间中来定位字符。不过笔者还没真见过(或者见过但不知道......)三维甚至更高维的编码,最多也就见过变相的三维编码空间。但编码都是人定的,你也可以自己定一个编码规则~~

    并不是每一个码位都会被使用,比如我们的汉字有8万多个,用10万个数字来编号的话还会剩余1万多个,这些剩余的码位则留作扩展用。

    注意 :到这一步我们只是将抽象字符集进行了编号,但这个编号并不一定是二进制的,而且它一般也不是二进制的,而是10进制或16进制。该层依然是个抽象层。

    而这里之所以说了这么多,就是为了和下面这个概念区分。

  3. 字符编码表 (Character Encoding Form, CEF ):码位 --> 码元

    将编码字符集中的码位转换成有限比特长度的整型值的序列。这个整型值的单位叫 码元 (code unit)。即一个码位可由一个或多个码元表示。而这个整型值通常就是码位的二进制表示。

    到这里才完成了字符到二进制的转换。程序员的工作通常到这里就完成了。但其实还有后续两步。

    注意 :直到这里都还没有将这些序列存到存储器中!所以这里依然是个抽象,只是相比上面两步更具体而已。

  4. 字符编码方案 (Character Encoding Scheme,CES):码元 --> 序列化

    也称为“serialization format”(常说的“序列化”)。将上面的整型值转换成可存储或可传输8位字节序列。简单说就是将上面的码元一个字节一个字节的存储或传输。每个字节里的二进制数就是字节序列。这个过程中还会涉及大小端模式的问题(码元的低位字节里的内容放在内存地址的高位还是低位的问题,感兴趣的请自行查阅,这里不再赘述)。

    直到这时,才真正完成了从我们使用的字符转换到机器使用的二进制码的过程。 抽象终于完成了实例化。

  5. 传输编码语法 (transfer encoding syntax):

    这里则主要涉及传输的问题,如果用计算机网络的概念来类比的话,就是如何实现透明传输。相当于将上面的字节序列的值映射到一个更受限的值域内,以满足传输环境的限制。比如Email的Base64或quoted-printable协议,Base64是6bit作为一个单位,quoted-printable是7bit作为一个单位,所以我们得想办法把8bit的字节序列映射到6bit或7bit的单位中。另一个情况则是压缩字节序列的值,如LZW或进程长度编码等无损压缩技术。

综上,整个编码过程概括如下:

字符 --> 码位 --> 码元 --> 序列化 ,如果还要在特定环境传输,还需要再映射。从左到右是编码的过程,从右到左就是解码的过程。

下面我们以Unicode为例,来更具体的说明上述概念。

2.2 统一字符编码Unicode

每个国家每个地区都有自己的字符编码标准,如果你开发的程序是面向全球的,则不得不在这些标准之间转换,而许多问题就出在这些转换上。Unicode的初衷就是为了避免这种转换,而对全球各种语言进行统一编码。既然都在同一个标准下进行编码,那就不存在转换的问题了呗。但这只是理想,至今都没编完,所以还是有转换的问题,但已经极大的解决了以前的编码转换的问题了。

Unicode编码就是上面的编码字符集CCS。而与它相伴的则是经常用到的UTF-8,UTF-16等,这些则是上面的字符编码表CEF。

最新版的Unicode库已经收录了超过10万个字符,它的码位一般用16进制表示,并且前面还要加上 U+ ,十进制表示的话则是前面加 &# ,例如字母“A”的Unicode码位是 U+0041 ,十进制表示为 &#065

Unicode目前一共有17个Plane(面),从 U+0000 U+10FFFF ,每个Plane包含65536(=2^16^)个码位,比如英文字符集就在0号平面中,它的范围是 U+0000 ~ U+FFFF 。这17个Plane中4号到13号都还未使用,而15、16号Plane保留为私人使用区,而使用的5个Plane也并没有全都用完,所以Unicode还没有很大的未编码空间,相当长的时间内够用了。

注意 :自2003年起,Unicode的编码空间被规范为了21bit,但Unicode编码并没有占多少位之说,而真正涉及到在存储器中占多少位时,便到了字符编码阶段,即UTF-8,UTF-16,UTF-32等,这些字符编码表在编程中也叫做 编解码器

UTF-n表示用n位作为码元来编码Unicode的码位。以UTF-8为例,它的码元是1字节,且最多用4个码元为Unicode的码位进行编码,编码规则如下表所示:

表中的 × 用Unicode的16进制码位的2进制序列从右向左依次替换,比如 U+07FF 的二进制序列为 : 00000,11111,111111 (这里的逗号位置只是为了和后面作比较,并不是正确的位置);

那么 U+07FF 经UTF-8编码后的比特序列则为 110 11111,10 111111 ,暂时将这个序列命名为 a

至此已经完成了前3步工作,现在开始执行序列化:

如果CPU是大端模式,那么序列 a 就是 U+07FF 在机器中的字节序列,但如果是小端模式,序列 a 的这两个字节需要调换位置,变为 10 111111,110 11111 ,这才是实际的字节序列。

3. Python中的字节序列

Python3明确区分了人类可读的字符串和原始的字节序列。Python3中,文本总是Unicode,由 str 类型表示,二进制数据由 bytes 类型表示,并且Python3不会以任何隐式的方式混用 str bytes 。Python3中的 str 类型基本相当于Python2中的 unicode 类型。

Python3内置了两种基本的二进制 序列 类型:不可变 bytes 类型和可变 bytearray 类型。这两个对象的每个元素都是介于0-255之间的整数,而且它们的切片始终是同一类型的二进制序列(而不是单个元素)。

以下是关于字节序列的一些基本操作:

>>> "China".encode("utf8")  # 也可以 temp = bytes("China", encoding="utf_8")
b'China'
>>> a = "中国"
>>> utf = a.encode("utf8")
>>> utf
b'\xe4\xb8\xad\xe5\x9b\xbd'
>>> a
'中国'
>>> len(a)
2
>>> len(utf)
6
>>> utf[0]
228
>>> utf[:1]
b'\xe4'
>>> b = bytearray("China", encoding="utf8")   # 也可以b = bytearray(utf)
>>> b
bytearray(b'China')
>>> b[-1:]
bytearray(b'a')

二进制序列实际是整数序列,但在输出时为了方便阅读,将其进行了转换,以 b 开头,其余部分:

  • 可打印的ASCII范围内的字节,使用ASCII字符本身;
  • 制表符、换行符、回车符和 \ 对应的字节,使用转义序列 \t \n \r \\
  • 其他字节的值,使用十六进制转义序列,以 \x 开头。

bytes bytesarray 的构造方法如下:

  • 一个 str 对象和一个 encoding 关键字参数;
  • 一个可迭代对象,值的范围是 range(256)
  • 一个实现了缓冲协议的对象(如 bytes bytearray memoryview array.array ),此时它将源对象中的字节序列复制到新建的二进制序列中。并且,这是一种底层操作,可能涉及类型转换。

除了格式化方法( format format_map )和几个处理Unicode数据的方法外, bytes bytearray 都支持 str 的其他方法,例如 bytes. endswith bytes.replace 等。同时,re模块中的正则表达式函数也能处理二进制序列(当正则表达式编译自二进制序列时会用到)。

二进制序列有个 str 没有的方法 fromhex ,它解析十六进制数字对,构件二进制序列:

>>> bytes.fromhex("31 4b ce a9")
b'1K\xce\xa9'

补充 :struct模块提供了一些函数,这些函数能把打包的字节序列转换成 不同类型字段 组成的元组,或者相反,把元组转换成打包的字节序列。struct模块能处理 bytes bytearray memoryview 对象。这个不是本篇重点,不再赘述。

4. 编解码器问题

如第2节所述,我们常说的UTF-8,UTF-16实际上是字符编码表,在编程中一般被称为编解码器。本节主要讲述关于编解码器的错误处理: UnicodeEncodeError UnicodeDecodeError SyntaxError

Python中一般会明确的给出某种错误,而不会笼统地抛出 UnicodeError ,所以,在我们自行编写处理异常的代码时,也最好明确错误类型。

4.1 UnicodeEncodeError

当从 文本转换成字节序列 时,如果编解码器没有定义某个字符,则有可能抛出 UnicodeEncodeError

>>> country = "中国"
>>> country.encode("utf8")
b'\xe4\xb8\xad\xe5\x9b\xbd'
>>> country.encode("utf16")
b'\xff\xfe-N\xfdV'
>>> country.encode("cp437")
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "E:\Code\Python\Study\venv\lib\encodings\cp437.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-1: character 
maps to <undefined>

可以指定错误处理方式:

>>> country.encode("cp437", errors="ignore")  # 跳过无法编码的字符,不推荐
b''
>>> country.encode("cp437", errors="replace") # 把无法编码的字符替换成“?”
b'??'
>>> country.encode("cp437", errors="xmlcharrefreplace") # 把无法编码的字符替换成XML实体
b'&#20013;&#22269;'

4.2 UnicodeDecodeError

相应的,当从 字节序列转换成文本 时,则有可能发生 UnicodeDecodeError

>>> octets.decode("cp1252")
'Montréal'
>>> octets.decode("iso8859_7")
'Montrιal'
>>> octets.decode("utf_8")
Traceback (most recent call last):
  File "<input>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: 
invalid continuation byte

# 解码错误的处理与4.1类似
>>> octets.decode("utf8", errors="replace")
# "�"字符是官方指定的替换字符(REPLACEMENT CHARACTER),表示未知字符,码位U+FFFD
'Montr�al'  

4.3 SyntaxError

当加载Python模块时,如果源码的编码与文件解码器不符时,则会出现 SyntaxError 。比如Python3默认UTF-8编码源码,如果你的Python源码编码时使用的是其他编码,而代码中又没有声明编解码器,那么Python解释器可能就会发出 SyntaxError 。为了修正这个问题,可在 文件开头 指明编码类型,比如表明编码为UTF-8,则应在源文件 顶部 写下此行代码: #-*- coding: utf8 -*- ” (没有引号!)

补充 :Python3允许在源码中使用非ASCII标识符,也就是说,你可以用中文来命名变量(笑。。。)。如下:

>>> 甲="abc"
>>> 'abc'

但是 极不推荐 !还是老老实实用英文吧,哪怕拼音也行。

4.4 找出字节序列的编码

有时候一个文件并没有指明编码,此时该如何确定它的编码呢?实际并没有100%确定编码类型的方法,一般都是靠试探和分析找出编码。比如,如果 b"\x00" 字节经常出现,就很有可能是16位或32位编码,而不是8位编码。Chardet就是这样工作的。它是一个Python库,能识别所支持的30种编码。以下是它的用法,这是在终端命令行中,不是在Python命令行中:

$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99

4.5 字节序标记BOM(byte-order mark)

当使用UTF-16编码时,字节序列前方会有几个额外的字节,如下:

>>> 'El Niño'.encode("utf16")
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'   # 注意前两个字节b"\xff\xfe"

BOM用于指明编码时使用的是大端模式还是小端模式,上述例子是小端模式。UTF-16在要编码的文本前面加上特殊的不可见字符 ZERO WIDTH NO-BREAK SPACE ( U+FEFF )。UTF-16有两个变种:UTF-16LE,显示指明使用小端模式;UTF-16BE,显示指明大端模式。如果显示指明了模式,则不会生成BOM:

>>> 'El Niño'.encode("utf_16le")
b'E\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
>>> 'El Niño'.encode("utf_16be")
b'\x00E\x00l\x00 \x00N\x00i\x00\xf1\x00o'

根据标准,如果文件使用UTF-16编码,且没有BOM,则应假定它使用的是UTF-16大端模式编码。然而Intel x86架构用的是小端模式,因此很多文件用的是不带BOM的小端模式UTF-16编码。这就容易造成混淆,如果把这些文件直接用在采用大端模式的机器上,则会出问题(比较老的AMD也有大端模式,现在的AMD也是x86架构了)。







请到「今天看啥」查看全文