字符串编码入门科普

背景

对于单纯作前端或者后端的同窗来讲,通常很难接触到编码问题,由于在同一个平台上,通常都是使用同一种编码方式,天然问题不大。但对于写爬虫的同窗来讲,编码极可能是遇到的第一个坑。这是由于字符串没法直接经过网络被传输(也不能直接被存储),须要先转换成二进制格式,再被还原。所以凡是涉及到经过网络传输字符的地方,一般都容易遇到编码问题。前端

概念定义

为了方便解释,咱们首先来定义一些概念。每一个开发者都知道字符串,它是一些字符的集合, 好比 hello world 就是一个最多见的字符串。相对来讲,字符 比较难定义一些。从语义上来说,它是组成字符串的最基本单位,好比这里的字母、空格,以及标点、特定语言(中文、日文)、emoji 符号等等。python

字符是语言中的概念,可是计算机只认识 0 和 1 这两个数字。所以要想让计算机存储、处理字符串,就必须把字符串用二进制表示出来。在 ASCII 码中,每一个英文字母都有本身对应的数字。咱们一般把 ASCII 码称为字符集,也就是字符的集合。了解 ASCII 码的同窗应该都知道小写字母 a 能够用 97 来表示,97 也被称为字符 a 在 ASCII 字符集中的码位后端

若是要设计一种密码,最简单的方式就是把字母转换成它在 ASCII 码中的码位再发送,接受者则查找 ASCII 码表,还原字符。可见把字符转换成码位的过程相似于加密(encrypt),咱们称之为编码(encode),反则则相似于解密,咱们称之为解码(decode)网络

图片
图片

编码方式

字符转换成码位的过程是编码,这个过程有无数种实现方式。好比 a -> 97b -> 98 这种就是 ASCII 编码,由于 255 = 2 ^ 8,因此全部 ASCII 编码下的码位刚好均可以由一个字节表示。ide

ASCII 比较诞生得比较早,随着愈来愈多的国家开始使用计算机,0-255 这么点码位确定不够用了。好比中国人为了展现汉字,发明了 GB2312 编码。GB2312编码彻底向下兼容 ASCII 编码,也就是说 全部 ASCII 字符集中的字符,它在 GB2312 编码下的码位与 ASCII 编码下的码位彻底一致,而中文则由两个字节表示,这也就是为何早期咱们通常认为一个中文等于两个英文的缘由。函数

Unicode

除了中国人本身的编码方式,各个地区的人也都根据本身的语言拓展了相应的编码方式。那么问题就来了, 给你一个码位 0xEE 0xDD,它到底表示什么字符,取决于它是用哪一种编码方式编码的。这就比如你拿到了密文,但没有密码表同样。所以,要想正确显示一种语言,就必须携带这个语言的编码规范,要想正确显示世界上全部的语言,看起来就比较困难了。编码

所以 Unicode 其实是一种统一的字符集规范,每个 Unicode 字符由 6 个十六进制数字表示,所以理论上能够表示出 16 ^ 6 = 16777216 个字符,显然是绰绰有余了。加密

Unicode 编码怎么样

看起来 Unicode 就是一种很棒的编码方式。诚然,Unicode 能够表示全部的字符,但过于全面带来的缺点就是过于庞大。对于字符 a 来讲,若是使用 ASCII 编码,能够表示为 0x61,只要一个字节就能存下,但它的 Unicode 码位是 0x000061,须要三个字节。所以采用 Unicode 编码的英文内容,会比 ASCII 编码大三倍。这大大增长了文件本地存储时占用的空间以及传输时的体积。spa

所以,咱们有了对 Unicode 字符再次编码的编码方式,常见的有 utf-8,utf-16 等。UTF 表示 Unicode Transfer Format,所以是针对 Unicode 字符集的一系列编码方式。utf-8 是一种变长编码,也就是说不一样的 Unicode 字符在 utf-8 编码下的码位长度可能不一样,以下表所示:操作系统

Unicode 编码(16进制) utf-8 码位(二进制)
000000-00007F 0xxxxxxx
000080-0007FF 110xxxxx 10xxxxxx
000800-00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000-1FFFFF 11110xxx10xxxxxx10xxxxxx10xxxxxx

这个表有两点值得注意。一个是 ASCII 字符集中的全部字符,它们的 utf-8 码位依然占用一个字节,所以 utf-8 编码下的英文字符不会向 Unicode 同样增长大小。另外一个则是全部中文的 utf-8 码位都占用 3 个字节,大于 GBK 编码的 2 字节。所以若是存在明确的业务须要,是能够用 GBK 编码取代 utf-8 编码的。

图片
图片

尽管 utf-8 很是经常使用,但它可变长度的特色不只会致使某些场景下内容过大,也为索引和随机读取带来了困难。所以在不少操做系统的内存运算中,一般使用 utf-16 编码来代替。utf-16 的特色是全部码位的最小单位都是 2 字节,虽然存在冗余,但易于索引。因为码位都是两个字节,就会存在字节序的问题。所以 utf-16 编码的字符串,一开头会有几个字节的 BOM(Byte order markd)来标记字节序,好比 0xFF 2(FE0x55,254) 表示 Intel CPU 的小字节序,若是不加 BOM 则默认表示大字节序。须要注意的是,某些应用会给 utf-8 编码的字节也加上 BOM。

虽然看起来问题变得复杂了,为了存储/传输一个字符,居然须要两次编码,但别忘了,Unicode 编码是通用的,所以能够内置于操做系统内部。因此咱们平时所谓的对字符串进行 utf-8 编码,其实说的是对字符串的 Unicode 码位进行 utf-8 编码。

这一点在 python3 中获得了充分的体现,字符串由字符组成,每个字符都是一个 Unicode 码位。

编解码错误处理

若是把编解码理解成利用密码表进行加解密,那么就容易理解,为何编码和解码过程都是易错的。

若是被编码的 Unicode 字符,在某种编码中根本没有列出编码方式,这个字符就没法被编码:

city = 'São Paulo'
b_city = city.encode('cp437')
# UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined>

b_city = city.encode('cp437', errors='ignore') 
# b'So Paulo'

b_city = city.encode('cp437', errors='replace')
# b'S?o Paulo'复制代码

同理,若是被解码的码位,在编码表中找不到与之对应的字符,这个码位就没法被解码:

octets = b'Montr\xe9al'
s_octest1 = octets.decode('utf8')
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte

s_octest1 = octets.decode('cp1252')
# Montréal

s_octest1 = octets.decode('iso8859_7')
# Montrιal

s_octest1 = octets.decode('utf8', errors='replace')
# Montr�al复制代码

python 的解决方案是,encodedecode 函数都有一个参数 errors 能够控制如何处理没法被编、解码的内容。它的值能够是 ignore(忽略这个错误并继续执行),也能够是 replace(用系统的占位符填充)。

通常来讲,没法从码位推断出编码方式,就像你不可能从密文推断出加密方式同样。可是某些编码方式会留下很是显著的特征,一旦这些特征频繁出现,基本就能够判定编码方式。Python 提供了一个名为 Chardet 的包,能够帮助开发者推断出编码方式,而且会给出相应的置信度。置信度越高,说明是这种编码方式的可能性越大。

octets = b'Montr\xe9al'
chardet.detect(octets)
# {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''}

octets.decode('ISO-8859-1')
# Montréal复制代码

总结

  1. 编码是为了把人类人类可读的字符转换成计算机容易存储和传输的二进制,解码反之,编码后获得的结果称之为码位。
  2. Unicode 是一种通用字符集,从字符到 Unicode 字符集中码位的转换也能够叫作 Unicode 编码
  3. Unicode 编码对英文字符不友好,所以出现了针对 Unicode 码位的再次编码,好比 utf-8,但愿在节省空间的同时保留强大的表达能力
  4. 各个编码之间的关系以下图所示:

相关文章
相关标签/搜索