Skip to content

Latest commit

 

History

History
697 lines (324 loc) · 19.8 KB

文本和编码.md

File metadata and controls

697 lines (324 loc) · 19.8 KB

文本和编码

字符串是个相当简单的概念:一个字符串是一个字符序列.问题出在"字符"的定义上.

在2015 年,"字符"的最佳定义是Unicode字符.因此从Python 3的str对象中获取的元素是Unicode字符

Unicode标准把字符的标识和具体的字节表述进行了如下的明确区分.

  • 字符的标识,即码位是0~1 114 111的数字(十进制).在Unicode标准中以4~6个十六进制数字表示,而且加前缀U+.例如字母A的码位是U+0041,欧元符号的码位是U+20AC,高音谱号的码位是U+1D11E.

    Unicode 6.3标准中约10%的有效码位有对应的字符.

  • 字符的具体表述取决于所用的编码.编码是在码位和字节序列之间转换时使用的算法.在UTF-8编码中,A(U+0041)的码位编码成单个字节\x41,而在UTF-16LE编码中编码成两个字节\x41\x00.再举个例子:欧元符号(U+20AC)UTF-8编码中是三个字节--\xe2\x82\xac,而在UTF-16LE中编码成两个字节--\xac\x20.

把码位转换成字节序列的过程是编码,使用encode;把字节序列转换成码位的过程是解码,使用decode.

非英语用户常常会搞反所谓的编码解码,可以这样理解--把Unicode字符串想成"人类可读"的文本.那么

  • 把字节序列变成人类可读的文本字符串就是解码
  • 而把字符串变成用于存储或传输的字节序列就是编码
s = "中文文本"
len(s)
4
b = s.encode("utf-8")#编码
b
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
len(b)
12
b.decode("utf-8")
'中文文本'

混乱的编码问题

现今使用UTF-8编码是最通用的,但编码存在很多"历史遗留问题",比如中文编码混乱的问题(非英语都有这个问题)

chardet是一个用于推断编码类型的工具,可以使用pip安装,使用它可以大致判断出文本使用的是什么编码,并给出该编码的可能性大小.具体用法可以看它的文档,下面是最简单的用法

import chardet
chardet.detect(b)
{'confidence': 0.938125, 'encoding': 'utf-8', 'language': ''}
from requests import get
rawdata = get('http://yahoo.co.jp/').content

chardet.detect(rawdata)
{'confidence': 0.99, 'encoding': 'utf-8', 'language': ''}

基本的编解码器

Python自带了超过100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换.每个编解码器都有一个名称,如utf_8,而且经常有几个别名,如utf8utf-8U8.这些名称可以传给open()str.encode()bytes.decode() 等函数的encoding参数.

如下是几种最常见的编码:

  • latin1(即iso8859_1)

一种重要的编码,是其他编码的基础,例如cp1252和Unicode(注意latin1cp1252的字节值是一样的甚至连码位也相同)

  • cp1252

微软制定的latin1超集,也是windows下各种编码问题的万恶之源,相对于latin1添加了有用的符号,例如弯引号和€(欧元);有些Windows 应用把它称为ANSI,但它并不是ANSI标准.

  • cp437

IBM PC 最初的字符集,包含框图符号.与后来出现的latin1不兼容.

  • gb2312

用于编码简体中文的陈旧标准,这是亚洲语言中使用较广泛的多字节编码之一.网络上中文乱码的万恶之源之一

  • utf-8

目前Web中最常见的8位编码;与ASCII兼容(纯ASCII文本是有效的UTF-8文本)

  • utf-16le

UTF-16的16位编码方案的一种形式;所有UTF-16支持通过转义序列(称为"代理对",surrogate pair)表示超过U+FFFF的码位. UTF-16取代了1996年发布的Unicode 1.0编码(UCS-2).这个编码在很多系统中仍在使用,但是支持的最大码位是U+FFFF.从Unicode 6.3起,分配的码位中有超过50%在U+10000以上,包括逐渐流行的表情符号(emoji pictograph).

for codec in ['latin_1', 'utf_8', 'utf_16']:
    print(codec, 'El Niño'.encode(codec), sep='\t')
latin_1	b'El Ni\xf1o'
utf_8	b'El Ni\xc3\xb1o'
utf_16	b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

大字节序还是小字节序

你可能注意到了,UTF-16编码的序列开头有几个额外的字节\xff\xfe这是BOM,即字节序标记(byte-order mark),指明编码时使用Intel CPU的小字节序.

在小字节序设备中,各个码位的最低有效字节在前面:字母'E'的码位是U+0045(十进制数69),在字节偏移的第2位和第3位编码为69和0.

在大字节序CPU中,编码顺序是相反的;'E'编码为0和69.

为了避免混淆,UTF-16编码在要编码的文本前面加上特殊的不可见字符ZERO WIDTH NOBREAK SPACE(U+FEFF).在小字节序系统中,这个字符编码为b'\xff\xfe'(十进制数 255, 254).因为按照设计,U+FFFE字符不存在,在小字节序编码中,字节序列b'\xff\xfe'必定是ZERO WIDTH NO-BREAK SPACE,所以编解码器知道该用哪个字节序.

UTF-16有两个变种:

  • UTF-16LE,显式指明使用小字节序;
  • UTF-16BE,显式指明使用大字节序.

如果使用这两个变种,不会生成BOM.

与字节序有关的问题只对一个字(word)占多个字节的编码(如UTF-16UTF-32)有影响.UTF-8的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要BOM.尽管如此,某些Windows应用(尤其是Notepad)依然会在UTF-8编码的文件中添加BOM;而且Excel会根据有没有BOM确定文件是不是UTF-8编码,否则它假设内容使用Windows代码页(codepage)编码.UTF-8编码的U+FEFF字符是一个三字节序列:b'\xef\xbb\xbf'.因此,如果文件以这三个字节开头,有可能是带有BOM的UTF-8文件.然而,Python不会因为文件以b'\xef\xbb\xbf' 开头就自动假定它是UTF-8编码的.

ascii码支持

binascii是处理ascii编码的工具,binascii模块包含很多在二进制和ASCII编码的二进制表示转换的方法.通常情况不会直接使用这些功能,而是使用像UU,base64编码,或BinHex封装模块. binascii模块包含更高级别的模块使用的,用C语言编写的低级高效功能.

接口如下:

  • binascii.a2b_uu(string)

    将一行uuencoded数据转换回二进制并返回二进制数据.线通常包含45(二进制)字节,除了最后一行.行数据后面可以是空格.

  • binascii.b2a_uu(data)

    将二进制数据转换成一行ASCII字符,返回值是转换行,包括换行符.数据长度最多为45.

  • binascii.a2b_base64(string)

    将一个base64数据块转换回二进制数据并返回二进制数据.一次可能会传递多条线.

  • binascii.b2a_base64(data, *, newline=True)

    将二进制数据转换为base64编码中的ASCII字符行.返回值是转换行,如果换行符为true则包含换行符.此功能的输出符合RFC 3548.

  • binascii.a2b_qp(data, header=False)

    将可打印数据块转换回二进制数据并返回二进制数据.一次可能会传递多条线.如果可选参数头存在且为真,下划线将被解码为空格.

  • binascii.b2a_qp(data, quotetabs=False, istext=True, header=False)

    将二进制数据转换成可引用可打印编码的ASCII字符行.返回值是转换行.如果可选参数quotetabs存在且为真,则将对所有选项卡和空格进行编码.如果可选参数istext存在且为真,那么换行不会被编码,而尾随空白将被编码.如果可选参数头存在且为真,则空格将按照RFC1522编码为下划线.如果可选参数头存在且为false,则换行符也将被编码;否则换行转换可能会损坏二进制数据流.

  • binascii.a2b_hqx(string)

    将binhex4格式的ASCII数据转换为二进制,无需进行RLE解压缩.字符串应包含完整数量的二进制字节,或(在binhex4数据的最后一部分的情况下)剩余的位为零.

  • binascii.rledecode_hqx(data)

    根据binhex4标准对数据执行RLE解压缩.该算法在一个字节后使用0x90作为重复指示符,后跟计数.计数为0指定字节值0x90.例程返回解压缩的数据,除非数据输入数据在孤立的重复指示符中结束,在这种情况下会引发Incomplete异常.

  • binascii.rlecode_hqx(data)

    对数据执行binhex4风格的RLE压缩并返回结果

  • binascii.b2a_hqx(data)

    执行hexbin4二进制到ASCII转换并返回生成的字符串.该参数应该已经是RLE编码,并且可以将长度除以3(除了可能的最后一个片段)

  • binascii.crc_hqx(data, value)

    计算数据的16位CRC值,以值作为初始CRC开始,并返回结果.这使用CRC-CCITT多项式x16 x12 x5 1,通常表示为0x1021.该CRC用于binhex4格式.

  • binascii.crc32(data[, value])

    计算CRC-32,数据的32位校验和,以值的初始CRC开始.默认的初始CRC为零.该算法与ZIP文件校验和一致.由于该算法被设计为用作校验和算法,因此不适合用作通用散列算法.使用如下:

import binascii
print(binascii.crc32(b"hello world"))
# Or, in two pieces:
crc = binascii.crc32(b"hello")
crc = binascii.crc32(b" world", crc)
print('crc32 = {:#010x}'.format(crc))
222957957
crc32 = 0x0d4a1185
  • binascii.b2a_hex(data)

  • binascii.hexlify(data)

    返回二进制数据的十六进制表示.数据的每个字节被转换成相应的2位十六进制表示.因此返回的字节对象是数据长度的两倍

  • binascii.a2b_hex(hexstr)

  • binascii.unhexlify(hexstr)

    返回由十六进制字符串hexstr表示的二进制数据,这个函数是b2a_hex()的倒数.hexstr必须包含偶数个十六进制数字(可以是大写或小写),否则会出现错误异常.

python3支持Unicode

python3从头到脚都支持Unicode,这也意味着像java一样,你可以将类名,函数名,变量名都设为中文(或者其他语言).不少人认为这样做不好,但考虑到代码的传播范围,其实使用更加便于交流的文字是更好的方法.比如,这个代码是一个日本企业内部使用的,而且他们并不打算让外国人用也不打算向前兼容python2,那么他们完全可以使用全日语来写文档,定义变量,函数,类.

为了正确比较而规范化Unicode字符串

因为Unicode有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以拉丁语系文字比如法语,意大利语字符串比较起来很复杂,我们拿café这个词来作为例子.这个词可以使用两种方式构成,分别有4个和5个码位,但是结果完全一样.

s1 = 'café'
s2 = 'cafe\u0301'
s1, s2
('café', 'café')
len(s1), len(s2)
(4, 5)
s1 == s2
False

U+0301COMBINING ACUTE ACCENT,加在e后面得到é.在Unicode标准中,ée\u0301 这样的序列叫"标准等价物"(canonical equivalent),应用程序应该把它们视作相同的字符.但是,Python看到的是不同的码位序列,因此判定二者不相等.

这个问题的解决方案是使用标准库的unicodedata.normalize函数提供的Unicode规范化.这个函数的第一个参数是这4个字符串中的一个:'NFC'、'NFD'、'NFKC' 和'NFKD'

  • NFC(Normalization Form C)和NFD (Normalization Form D)

使用最少的码位构成等价的字符串,而NFD 把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期:

from unicodedata import normalize
s1 = 'café'
s2 = 'cafe\u0301'
len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)

西方键盘通常能输出组合字符,因此用户输入的文本默认是NFC形式.不过,安全起见,保存文本之前,最好使用normalize('NFC', user_text)清洗字符串.NFC也是W3C的Character Model for the World Wide Web: String Matching and Searching规范推荐的规范化形式。

  • NFKC和NFKD

在另外两个规范化形式(NFKC 和NFKD)的首字母缩略词中,字母K表示"compatibility"(兼容性).这两种是较严格的规范化形式,对"兼容字符"有影响.虽然Unicode的目标是为各个字符提供"规范的"码位,但是为了兼容现有的标准,有些字符会出现多次.例如虽然希腊字母表中有"μ"这个字母(码位是U+03BCGREEK SMALL LETTER MU),但是Unicode还是加入了微符号μ(U+00B5)以便与latin1相互转换.因此,微符号是一个"兼容字符".

在NFKC和NFKD形式中,各个兼容字符会被替换成一个或多个"兼容分解"字符,即便这样有些格式损失,但仍是"首选"表述--理想情况下格式化是外部标记的职责,不应该由Unicode处理.下面举个例子:

二分之一½(U+00BD)经过兼容分解后得到的是三个字符序列'1/2';微符号μ(U+00B5)经过兼容分解后得到的是小写字母μ(U+03BC)

from unicodedata import normalize, name
half = '½'
normalize('NFKC', half)
'1⁄2'
four_squared = '4²'
normalize('NFKC', four_squared)
'42'
micro = 'μ'
micro_kc = normalize('NFKC', micro)
micro, micro_kc
('μ', 'μ')
ord(micro), ord(micro_kc)
(956, 956)
name(micro), name(micro_kc)
('GREEK SMALL LETTER MU', 'GREEK SMALL LETTER MU')

使用1/2 替代½可以接受,微符号也确实是小写的希腊字母μ,但是把转换成42就改变原意了.某些应用程序可以把保存为4<sup>2</sup>,但是normalize函数对格式一无所知.因此NFKC 或NFKD可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述--用户搜索1 ⁄ 2 inch时如果还能找到包含½ inch的文档那么用户会感到满意.

使用NFKC和NFKD规范化形式时要小心,而且只能在特殊情况中使用,例如搜索和索引,而不能用于持久存储,因为这两种转换会导致数据损失.

大小写折叠

大小写折叠其实就是把所有文本变成小写,再做些其他转换.这个功能由str.casefold()实现.

对于只包含latin1字符的字符串s,s.casefold()得到的结果与s.lower()一样,唯有两个例外:

  • 微符号μ 会变成小写的希腊字母μ(在多数字体中二者看起来一样);
  • 德语Eszett("sharp s",ß)会变成ss

自Python 3.4 起,str.casefold()str.lower()得到不同结果的有116个码位。Unicode6.3命名了110122个字符,这只占0.11%

极端"规范化":去掉变音符号

去掉变音符号不是正确的规范化方式,因为这往往会改变词的意思,而且可能误判搜索结果.但是对现实生活却有所帮助--人们有时很懒或者不知道怎么正确使用变音符号,而且拼写规则会随时间变化.因此实际语言中的重音经常变来变去.

比如café,对于中国人来说é很难打出来,所以用户往往就打cafe了,我们需要一个去掉组合记号的函数用来实现这种极端的规范化

import unicodedata
import string
def shave_marks(txt):
    """去掉全部变音符号"""
    norm_txt = unicodedata.normalize('NFD', txt)
    shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c))
    return unicodedata.normalize('NFC', shaved)
order = '“Herr Voß: • ½ cup of OEtker™ caffè latte • bowl of açaí.”'
shave_marks(order)
'“Herr Voß: • ½ cup of OEtker™ caffe latte • bowl of acai.”'
Greek = 'Zέφupoς, Zéfiro'
shave_marks(Greek)
'Zεφupoς, Zefiro'

总结unicode规范化:

  • NFC和NFD可以放心使用,而且能合理比较Unicode字符串
  • 对大多数应用来说,NFC是最好的规范化形式
  • 不区分大小写的比较应该使用str.casefold()
  • 在必要的时候,我们可以删除一些变音符号来做规范化

文本排序

Python比较任何类型的序列时,会一一比较序列里的各个元素.对字符串来说,比较的是码位.可是在比较非ASCII字符时,得到的结果不尽如人意.

l = ["前","后","左","右"]
sorted(l)
['前', '右', '后', '左']

按照中文传统,我们应该希望按拼音首字母顺序排序即后前右左,但明显不是.

实际上在Python中,非ASCII文本的标准排序方式是使用locale.strxfrm函数,根据locale模块的文档,这个函数会"把字符串转换成适合所在区域进行比较的形式".使用locale.strxfrm函数之前,必须先为应用设定合适的区域设置,还要祈祷操作系统支持这项设置.在区域设为pt_BRGNU/Linux(Ubuntu 14.04)中.而在windows中还没这个功能.

在Linux操作系统中中国大陆的读者可以使用zh_CN.UTF-8,简体中文会按照汉语拼音顺序进行排序.

unicode排序工具PyUCA

James Tauber,一位高产的Django贡献者,他一定是感受到了这一痛点,因此开发了 PyUCA 库,这是Unicode排序算法包.

PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给Collator()构造方法。PyUCA 默认使用项目自带的allkeys.txt,这就是Unicode 6.3.0"Default Unicode Collation Element Table"的副本

import pyuca
coll = pyuca.Collator()
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=coll.sort_key)
sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

以指定列宽格式化字符串

文本标注

python定义字符串使用成对的',",""",'''构建而成,而文本可以标注为r,u,b,f

s = "这是一句话\n"
s
'这是一句话\n'

原始文本标注r

r标注的文本表示不会理会转义字符\

s = r"这是一句话\n"
s
'这是一句话\\n'

unicode文本标注u

u标注的文本表示字符串为unicode,python3中str就是Unicode,所以其实这个没什么意义,主要是为了给python3向前兼容的

btypes文本标注b

用b标注的文本标识文本为bytes,使用这个标注说明文本是字节序列

格式化文本标注f

用f标注的文本表示其中有用{}占位的内容由前面定义的变量填充,需要注意一旦标注为f则文本实际上已经不是文本了,而是一个函数,如果占位符没有找到对应的变量,则会报错,将f标注的文本放入函数中指定参数为其中的占位符也没有用

a = 1
b = 2
f"asdf{a}{b}"
'asdf12'

格式化字符串

python可以使用str.format()方法来格式化字符串这种方式更加直观

"{}是{}".format("一",1)
'一是1'
"{a}是{b}".format(a="一",b=1)
'一是1'

文本模板

python提供了一个模块from string import Template,可以用于定义文本模板.它通常用来作为文件的内容模板.

Template用起来和字符串的format方法类似,使用$标识要替换的占位字符,然后使用方法substitute来替换.以下是一个dockerfile的文本模板

from string import Template
file = Template("""
FROM python:$v1:$v2
ADD requirements/requirements.txt /code/requirements.txt
ADD $project_name.$suffix /code/$project_name.$suffix
WORKDIR /code
RUN pip install -r requirements.txt
""")
content = file.substitute(v1=3,v2=6,project_name="my project",suffix="pyz")
print(content)
FROM python:3:6
ADD requirements/requirements.txt /code/requirements.txt
ADD my project.pyz /code/my project.pyz
WORKDIR /code
RUN pip install -r requirements.txt