python3的str对象中获取的元素是Unicode字符,这相当于从python2中的unicode对象中获取元素,而不是从python2的str对象中获取原始字节序列
python3的str类型基本相当于python2的unicode类型。
把码位转换成字节序列的过程是编码。 encode (把字符串转换成用于存储或者传输的字节序列)
把字节序列转换为码位的过程是解码。 decode (把字节序列变成人类可读的文本字符串)
s = 'café' b = s.encode('utf-8') # str转换为bytes对象 print(b) s = b.decode('utf-8') # bytes对象转换为str对象 print(s) 打印 b'caf\xc3\xa9' café
字节介绍
bytes和bytearray对象的各个元素是介于0~255(包含)之间的整数。
bytes为不可变的字节序列。bytearray可变的字节序列,相当于bytes的可变版本,可以称为字节数组。
cafe = bytes('café', encoding=''utf-8")
cafe[0] 结果为99
然而,字节序列(二进制)的切片却始终是字节序列
cafe[:1] 结果为'c'
cafe_array = bytearray(cafe)
cafe_array[-1:] 结果为bytearray(b'\xa9') # bytearray的切片还是bytearry对象
二进制序列其实是整数序列,但是他们的字面展示各个字节的值,会有三种方式:
- 可打印范围内的字节,使用ASCII字符本身。 比如以上的 caf
- 制表符、换行符、回车符等 \t \n \r
- 其他字节的值,使用十六进制转移序列。 比如以上的 \xc3\xa9
结构体struct和内存视图memoryview
struct模块提供了一些函数,比如把打包的字节序列转换成不同类型字段组成的元祖,还有一些函数可以反向转换,把元祖转换成打包的字节序列。
struct模块能处理bytes、bytearray、memoryview对象。
memoryview是共享内存,让你访问其他二进制序列、打包的数组、缓冲中的数据切片,而无需复制字节序列。
memoryview对象的切片是一个新的memory对象,而且不会复制字节序列。
示例,使用struct和memoryview查询GIF图像的头部信息
import struct fmt = "<3s3sHH" # 结构体: <是小字节序列, 3s3s是两个3字节序列,HH是两个16位二进制整数。 with open('s.gif', 'rb') as f: memoryview_ = memoryview(f.read()) # 创建一个memoryview对象 gif_header = memoryview_[:10] # 使用切片再创建一个memoryview对对象,这里不会复制字节序列! print(bytes(gif_header)) uppack = struct.unpack(fmt, gif_header) # 拆包,等到一个元祖,包含了图片信息:类型,版本,宽度,高度 print(uppack) del gif_header # 删除引用,释放memoryview实例所占的内存 del memoryview_ 打印 b'GIF89a,\x01,\x01' (b'GIF', b'89a', 300, 300)
编码器介绍
Python自带了超过100种编码器,用于文本和字节的转换。每个编码器都有一个名称,比如"utf_8" 还有别名utf8/utf-8/U8
这些名称都可以传递给open() str.encode() bytes.decode() 等函数的的encoding参数。
一些典型的编码:
latin1 (iso8859_1) 一种重要的编码,是很多编码的基础。
cp1252 是微软指定的latin1的超集,新增了一些符号。
gb2312 简体中文编码的旧标准。
cp936 其实就是GBK,IBM在发明Code Page的时候将GBK放在第936页,所以叫CP936
utf-8 目前最常见的8位编码。可以兼容ASCII所有
utf-16le 是utf-16的16位编码的另一种形式,
UnicodeEncodeError
多数的非UTF编码器只能处理Unicode字符的一小部分。把文本转换为字节序列的时候,如果编码中没有定义某个字符,就会抛出异常UnicodeEncodeError
>>> city = 'Sāo Paulo'
>>> city.encode('utf8')
b'S\xc4\x81o Paulo'
>>> city.encode('cp437')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\lijiachang\AppData\Local\Programs\Python\Python37-32\lib\encodings\cp437.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\u0101' in position 1: character maps to <undefined>
编码时可以指定errors='ignore'来忽略错误,变成空白字符
或者是errors='replace'把无法编码的字符变为? (推荐这种方式,用户知道编码出了问题)
或者是errors='xmlcharrefreplace'把无法编码的字符变为XML实体
>>> city.encode('cp437',errors='ignore')
b'So Paulo'
>>> city.encode('cp437',errors='replace')
b'S?o Paulo'
>>> city.encode('cp437',errors='xmlcharrefreplace')
b'Sāo Paulo'
>>>
UnicodeDecodeError
同样不是每一个字符序列都有有效的utf-8或者utf-16对应。
>>> city = b'S\xc4\x81o Paulo'
>>> city.decode('utf-8')
'Sāo Paulo'
>>> city.decode('cp1252')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\lijiachang\AppData\Local\Programs\Python\Python37-32\lib\encodings\cp1252.py", line 15, in decode
return codecs.charmap_decode(input,errors,decoding_table)
UnicodeDecodeError: 'charmap' codec can't decode byte 0x81 in position 2: character maps to <undefined>
同样也可以使用errors参数来处理错误:
>>> city.decode('cp1252',errors='replace')
'S�o Paulo'
�的含义是Unicode官方指定的未知字符。
SyntaxError 加载模块时预期之外的编码
Python3中默认使用的是UTF-8 编码源码。
Python2中默认使用是的ASCII编码。
比如在python3中加载py模块的时候包含了除utf-8之外的编码,而且没有编码声明,就报错SyntaxError。
为了修正这个问题,可以在顶部加入coding注释
#coding:cp1252
在Python2常见到文件头部的# coding:utf-8 在Python3中就不需要了,因为Python3默认使用了utf-8
找出字节序列的编码
使用编码侦测包 chardet,在代码中动态检测当前页面或者文件中的编码格式信息。
import chardet import urllib.request TestData = urllib.request.urlopen('http://www.baidu.com/').read() print(chardet.detect(TestData)) 打印 {'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
以上的结果分析, 其准确率99%的概率,编码格式为utf-8
*BOM:字节顺序标记
byte-order mark 字节顺序标记,出现在文件的头部,用来标记用哪种编码(小字节序,大字节序)。
编码的默认值
在windows上的默认编码问题,可以看出在多设备下运行的代码,一定不能依赖默认编码
以utf-8编码写入文件,然后以默认编码读取:
with open('cafe.txt', 'w', encoding='utf-8') as f: f.write('café') with open('cafe.txt', 'r') as f: print(f) print(f.read()) 打印 <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'> caf茅
读取出现乱码的原因就是,windows中系统的默认编码可以看到打印encoding='cp936' CP936其实就是GBK
以二进制模式打开文本文件,使用chardet猜编码:
with open('cafe.txt', 'rb') as f: b = f.read() print(chardet.detect(b)) 打印 {'encoding': 'utf-8', 'confidence': 0.505, 'language': ''}
查看系统的一些编码
locale.getpreferredencoding() 这个是文本文件默认使用的编码,很重要。如果打开文件没有指定encoding,就会以这个编码。还有重定向文件内容的时候,也会使用这个编码。
sys.stdout.encoding 控制台编码
sys.getfilesystemencoding() 用于编解码文件名(不是文件内容)。比如把字符串参数作为文件名传给open()函数就会使用他,如果传入的文件名是字节序列,就不经改动直接给OS API。
sys.getdefaultencoding() python在二进制和字符串之间的转换,内部使用的这个编码。
import sys, locale my_file = open('dummy', 'w') expressions = """locale.getpreferredencoding() type(my_file) my_file.encoding sys.stdout.encoding sys.stdin.encoding sys.getdefaultencoding() sys.getfilesystemencoding() """ for ex in expressions.split(): value = eval(ex) print(ex.rjust(30), ' > ', repr(value)) 打印 locale.getpreferredencoding() > 'cp936' type(my_file) > <class '_io.TextIOWrapper'> my_file.encoding > 'cp936' sys.stdout.encoding > 'UTF-8' sys.stdin.encoding > 'UTF-8' sys.getdefaultencoding() > 'utf-8' sys.getfilesystemencoding() > 'utf-8'
以上查看了文件默认编码,在Python中是无法修改的,需要在Linux系统中配置/etc/profile
export LC_ALL="en_US.utf8"
export LANG="en_US.utf8"
规范化Unicode字符串 NFC/NFD
café这个词有两种构成方式,分别有4个和有5个码位,但是结果是一样的
以下示例是在linux的python3中,windows可能有不同的打印
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1,s2
('café', 'café')
>>> len(s1),len(s2)
(4, 5)
>>> s1==s2
False
U+0301是音符,加到e后面得到了é。在Unicode标准中é和e/u0301这样的序列叫做标准等价物,应用程序应该把他们视为相同的字符。但是在Python中看到的是不同的码位序列,判断了二者不相等。
解决方法就是使用unicodedata.normalize函数提供Unicode规范化,这个函数的第一个参数可以是四种模式:'NFC' 'NFD' 'NFKC' 'NFKD'
NFC: Normalization Form C 使用最少的码位构成等价的字符串。(最少)
NFD: 把组合字符分解成基字符和单独的组合字符。(最多)
from unicodedata import normalize s1 = 'café' s2 = 'cafe\u0301' print(len(normalize('NFC', s1)), len(normalize('NFC', s2))) print(len(normalize('NFD', s1)), len(normalize('NFD', s2))) print(normalize('NFC', s1) == normalize('NFC', s2)) print(normalize('NFD', s1) == normalize('NFD', s2)) 打印 4 4 5 5 True True
NFKC和NFKD基于以上两种模式,提高了兼容性。K表示compatiblity兼容性。这种模式中,各个兼容字符会被替换成一个或者多个兼容分解的字符,这样会有些格式损失。
比如½符号会被分解成三个字符序列1/2
比如4²会被转换成42 这就难以接受了。。
from unicodedata import normalize s1 = '½' s2 = '4²' print(normalize('NFKC', s1), normalize('NFKC', s2)) 打印 1⁄2 42
str.casefold() 这个是把字符串转换为小写,和str.lower()非常类似。但是个别字符和lower函数结果不一样, 比如说'ß'会转换成'ss'
casefold函数可识别更多的对象将其输出为小写,而lower函数只能完成ASCII码中A-Z之间的大写到小写的转换
规范化文本匹配的函数
from unicodedata import normalize def nfc_equal(str1, str2): """以NFC模式处理文本后对比""" return normalize('NFC', str1) == normalize('NFC', str2) def fold_equal(str1, str2): """转为小写对比""" return normalize('NFC', str1.casefold()) == normalize('NFC', str2.casefold()) print(nfc_equal('café', 'cafe\u0301')) print(fold_equal('Strß', 'strss')) 打印 True True
去掉音符的函数
from unicodedata import normalize, combining def shave_marks(str1): """去掉字符串中的音符""" norm_text = normalize('NFD', str1) # 把字符串分解成基字符和组合记号 shave_text = ''.join(s for s in norm_text if not combining(s)) # 过滤掉组合记号。 combining()返回字符chr的权威组合值,若未定义这样的值,则返回0。 return normalize('NFC', shave_text) # 重组基字符 print(nfc_equal('café')) 打印 cafe
Unicode排序
Python比较任何类型序列,都会一个个比较其中的元素。对于字符串来说,比较的是码位。在处理非ASCII码时,结果可能有问题。
Python中处理非ASCII码的标准排序方式是用local.strxfrm函数,这个函数的官方说明是:把字符串转换成合适所在区域进行比较的形式。
使用local.strxfrm函数之前,要先设置setlocale(LC_COLLATE), <your_locale>)
import locale
locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
sorted_fruits = sorted(fruits, key=locale.strxfrm)
以上说到的排序方法,只能使用与linux中。
可以使用第三方库,pyuca模块,适用于mac os、linux、windows。
import pyuca
coll = pyuca.Collator()
sorted_fruits = sorted(fruits, key=coll.sort_key)
Unicode数据库
Unicode标准提供了一个完整数据库,不仅有码位与字符名称直接的映射,还有字符的元数据,②可以识别出2,½可以识别出0.5
unicodedata.numeric(char) 可以识别出各种数字符号对应的数值。
unicodedata.name(char) 可以展示字符在Unicode中的名称。
补充一下:
数字判断的两个函数:
isdigit() True: Unicode数字,byte数字(单字节),全角数字(双字节),罗马数字 False: 汉字数字 Error: 无
isnumeric() True: Unicode数字,全角数字(双字节),罗马数字,汉字数字 False: 无 Error: byte数字(单字节)
格式化:
%d 十进制整数
%o 八进制整数
%x 十六进制整数 (但是前面不带0x)
import unicodedata re_digit = re.compile(r'\d') sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285' for char in sample: print('U+%04x' % ord(char), # ord() 返回对应的 ASCII 数值,或者 Unicode 数值。%x 十六进制整数 char.center(6), # 长度为6的居中显示 're_digit' if re_digit.match(char) else '-', # 是否匹配正则'\d' 'is_digit' if char.isdigit() else '-', # 是否符合isdigit()函数。检测字符串是否只数字组成的 'is_isnumeric' if char.isnumeric() else '-', # 是否符合isnumeric()函数。 检测字符串是否是只包含数字字符。比如中文数字也是数字字符。 format(unicodedata.numeric(char), '5.2f'), # 显示数值。格式化为长度5,小数点后保留2位 unicodedata.name(char), # Unicode标准中字符的名称 sep='\t' ) 打印 U+0031 1 re_digit is_digit is_isnumeric 1.00 DIGIT ONE U+00bc ¼ - - is_isnumeric 0.25 VULGAR FRACTION ONE QUARTER U+00b2 ² - is_digit is_isnumeric 2.00 SUPERSCRIPT TWO U+0969 ३ re_digit is_digit is_isnumeric 3.00 DEVANAGARI DIGIT THREE U+136b ፫ - is_digit is_isnumeric 3.00 ETHIOPIC DIGIT THREE U+216b Ⅻ - - is_isnumeric 12.00 ROMAN NUMERAL TWELVE U+2466 ⑦ - is_digit is_isnumeric 7.00 CIRCLED DIGIT SEVEN U+2480 ⒀ - - is_isnumeric 13.00 PARENTHESIZED NUMBER THIRTEEN U+3285 ㊅ - - is_isnumeric 6.00 CIRCLED IDEOGRAPH SIX
支持字符串和字节序列的双模式函数
有一些模块中的函数,可以使用字符串或者字节序列作为参数,然后根据类型展现不同的行为。
re正则表达式模块
re.compile(r'\d+') 这种是字符串模式,能够匹配ASCII字符和之外的Unicode数字
re.compile(rb'\d+') 这种是字节序列模式,只能匹配ASCII字符
补充,字符串正则表达式有个re.ASCII标志,可以只匹配ASCII字符。
os模块
Linux内存对Unicode的支持并不完善,使用字节序列作为文件名的文件,在不同操作系统中,可能无法利用最合理的方法解码成字符串。
这种情况,可以把字节序列直接传给python来做处理,通过os模块的函数,直接取得字节序列的值。
os.listdir('.') 这种是通过系统解码后,把文件名传递给python。 如 digits-π.txt
os.listdir(b'.') 参数是字节序列,listdir函数返回的也是字节序列。由我们自行处理成字符串。 如digits-\xcf\x80.txt
针对系统文件名的处理,os模块特殊提供了编码和解码函数:
fsencode(filename): 如果参数filename是str类型,就使用sys.getfilesystemencoding()得到的编码把filename编码成字节序列返回。否则,直接返回不处理的filename
fsdecode(filename):如果参数是bytes类型,就使用sys.getfilesystemencoding()得到的编码把filename解码成字节序列返回。否则,直接返回不处理的filename
错误处理方式surrogateescape
在Python3中编解码器的错误处理,除了ignore、replace等,还多了一个surrogateescape:当遇到解码的字节会替换成Unicode中的U+DC00到U+DCFF之间的码位,这些码位都是保留的,用于应用程序内部使用。
b = b'digits-\xcf\x80'
b_s = b.decode('ascii', errors='surrogateescape')
b_s结果:digits-\udccf\udc80
b_s.encode('ascii', errors='surrogateescape')
结果b'digits-\xcf\x80'
也就是说,当无法解码时,会在Unicode找一个预留不用的码位,来映射这个字节,当编码时再映射回去。不过这样也没啥可读性。
以上代码我在Python3.7中测试无效,会报错:UnicodeEncodeError: 'utf-8' codec can't encode characters in position 7-8: surrogates not allowed
暂时不知道原因。。