# ------------------------------------字符概要------------------------------------
"""
'字符串'是个相当简单的概念:一个字符串是一个字符序列.
'字符'的最佳定义是Unicode字符.从Python3的str对象中获取的元素是Unicode字符.相当于从Python2的unicode对象中获取的元素.
'字符的标识',即码位,是0-1114111的数字(十进制),在Unicode标准中以4-6个十六进制数字表示,而且加前缀'U+'
字符的具体表述取决于所用的编码.'编码'是在码位和字节序列之间转换时使用的算法.
把码位转换成字节序列的过程时编码;把字节序列转换成码位的过程时解码.
可以把字节序列想象成晦涩难懂的机器磁芯转储,把Unicode字符串想象成'人类可读'的文本.
那么.把字节序列变成人类可读的文本字符串就是解码;而把字符串变成用于存储或传输的字节序列就是编码.
"""
s = 'safé'
print(len(s)) # 4 'safé'字符串有4个Unicode字符
b = s.encode('utf-8') # 使用UTF-8把str对象编码成bytes对象
print(b) # b'saf\xc3\xa9' bytes字面量以b开头
print(len(b)) # 5 字节序列b有5个字节(UTF-8中,é的码位编码成2个字节)
print(b.decode('utf-8')) # safé 使用UTF-8把bytes对象解码成str对象
# ------------------------------------字节概要------------------------------------
"""
Python3的二进制序列类型在很多方面与Python2的str类型不同.首先要知道,Python内置了两种基本的二进制序列类型:
Python3引入的不可变bytes类型和Python2.6添加的可变bytearray类型.
bytes和bytearray对象的各个元素是介于0-255之间的整数,而不是像Python2的str对象那样是单个的字符.
然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为1的切片.
"""
safe = bytes(s, encoding='utf-8') # bytes对象可以从str对象使用给定的编码构建
print(safe) # b'saf\xc3\xa9'
print(safe[0]) # 115 各个元素是0-255的整数.
print(safe[:1]) # b's' bytes对象的切片还是bytes对象,即使是只有一个字节的切片.
safe_arr = bytearray(safe)
print(safe_arr) # bytearray对象没有字面量句法,而是以bytearray()和字节序列字面量参数的形式显示.
print(safe_arr[-1:]) # bytearray对象的切片还是bytearay对象.
"""
safe[0]获取的是一个整数,而safe[:1]返回的是一个长度为1的bytes对象----这一点应该不会让人意外.
safe[0] == safe[:1]只对str这个序列类型成立.不过str类型的这个行为十分罕见.
对其他各个序列类型来说,s[i]返回一个元素,而s[i:i+1]返回一个相同类型的序列,里面是s[i]元素.
"""
"""
虽然二进制序列其实是整数序列,但是它们的字面量表示法表面其中有ASCII文本,因此,各个字节的值可能会使用下列三种不同的方式显示:
1.可打印的ASCII范围内的字节(从空格到~),使用ASCII字符本身.
2.制表符,换行符,回车符和\对应的字节,使用转义序列\t,\n,\r和\\
3.其他字节的值,使用十六进制转义序列(例如,\x00是空字节)
因此'safé'.encode('utf-8')我们看到的是b'saf\xc3\xa9':前三个字节b'saf'在可打印的ASCII范围内,后两个字节则不然.
"""
"""
除了格式化方法(format和format_map)和几个处理Unicode数据方法(包括casefold,isdecimal,isidentifier,isnumeric,isprintable和encode)之外,
str类型的其他方法都支持bytes和bytearray类型.这意味着,我们可以使用熟悉的字符串方法处理二进制序列,如endwith,replace,strip,translate,upper等,
只有少数几个其他方法的参数是bytes对象,而不是str对象.
此外,如果正则表达式编译自二进制序列而不是字符串,re模块中的正则表达式函数也能处理二进制序列.
二进制序列有个类方法是str没有的,名为fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列.
"""
print(bytes.fromhex('31 48 CE A9')) # b'1H\xce\xa9'
# 构建bytes和bytearray还可以调用各自的构造方法,传入下述参数:
# 1.一个str对象和一个encoding关键字参数
print(bytes(s, encoding='utf-8')) # b'saf\xc3\xa9'
print(bytearray(s, encoding='utf-8')) # bytearray(b'saf\xc3\xa9')
# 2.一个可迭代对象,提供0-255之间的数值.
print(bytes([0, 125, 255])) # b'\x00}\xff'
print(bytearray([0, 125, 255])) # bytearray(b'\x00}\xff')
# 3.一个整数,使用空字节创建对应长度的二进制序列
print(bytes(10)) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
print(bytearray(10)) # bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
# 4.一个实现了缓冲协议的对象(如bytes,bytearray,memoryview,array.array);此时,把源对象中的字节序列复制到新键的二进制序列中.
# 使用缓冲类对象创建二进制序列是一种低层操作,可能涉及类型转换.
import array
numbers = array.array('h', [-2, -1, 0, 1, 2]) # 指定类型代码'h',创建了一个短整数(16位)数组
octets = bytes(numbers) # octets保存组成numbers的字节序列的副本
print(octets) # b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' 这些是表示那5个短整数的10个字节
# ------------------------------------结构体和内存视图------------------------------------
"""
struct模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列.
struct模块能处理bytes,bytearray,memoryview对象.
memoryview类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列,打包的数组和缓冲中的数据切片,而无需复制字节序列.例如PIL.
"""
# 如何使用memoryview和struct提取一个JPG图像的宽度和高度
import struct
fmt = '<3s3sHH' # 结构体的格式:<是小字节序,3s3s是两个3字节序列,HH是两个16位二进制整数.
with open('头像.gif', 'rb') as fp:
img = memoryview(fp.read()) # 使用内存中的文件创建一个memoryview对象
header = img[:10] # 然后使用它的切片再创建一个memoryview对象;这里不会复制字节序列
print(bytes(header)) # b'GIF89a\xa0\x011\x01' 转换成字节序列
print(struct.unpack(fmt, header)) # (b'GIF', b'89a', 416, 305) 拆包一个memoryview对象,得到一个元组,包含类型,版本,宽度和高度
del header # 删除引用,释放memoryview实例所占的内存
del img
"""
注意,memoryview对象的切片是一个新的memoryview对象,而且不会复制字节序列.
"""
# ------------------------------------基本的编解码器------------------------------------
"""
Python自带了超过100种编解码器,用于在文本和字节之间相互转换.每个编解码器都有一个名称,如utf_8,而且经常有以几个别名,如utf8,utf-8和U8.
这个些名称可以传给open(),str.encode(),bytes.decode()等函数的encoding参数.
"""
for codec in ['latin_1', 'utf_8', 'utf_16']:
print(codec, 'El Nino'.encode(codec), sep='\t')
"""
latin1 即iso8859_1
一种重要的编码,是其它编码的基础,例如cp1252和Unicode
cp1252
Microsoft制定的latin1超集,添加了有用的符号,例如弯引号和€.
cp437
IBM PC最初的字符集,包括框图符号.与后来出现的latin1不兼容.
gb1312
用于编码简体中文的陈旧标准;这是亚洲语言中使用较广泛的多字节编码之一.
utf-8
目前Web种最常见的8位编码;与ASCII兼容.
utf-16le
UTF-16的16位编码方案的一种形式;所有UTF-16支持通过转义序列表示超过U+FFFF的码位.
"""
# ------------------------------------了解编解码问题------------------------------------
"""
虽然有个一般性的UnicodeError异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时),
或UnicodeDecodeError(把二进制序列转换成字符串时).如果源码的编码与预期不符,加载Python模块时还可能抛出SyntaxError.
"""
"""
UnicodeEncodeError
非UTF编解码器只能处理Unicode字符的一小部分子集.把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError异常,
除非把errors参数传给编码方法或函数,对错误进行特殊处理.
"""
city = 'Sāo Paulo'
print(city.encode('utf_8')) # b'S\xc4\x81o Paulo' utf_?编码能处理任何字符串
print(city.encode('utf_16')) # b'S\xc4\x81o Paulo'
# print(city.encode('iso8859_1')) # UnicodeEncodeError: 'latin-1' codec can't encode character '\u0101' in position 1: ordinal not in range(256)
print(city.encode('iso8859_1', errors='ignore')) # b'So Paulo' 悄无声息的跳过无法编码的字符,这样做通常很是不妥
print(city.encode('iso8859_1', errors='replace')) # b'S?o Paulo' 把无法编码的字符替换成?;数据损坏了,但是用户知道出了问题.
print(city.encode('iso8859_1', errors='xmlcharrefreplace')) # b'Sāo Paulo' 把无法编码的字符替换成XML实体
"""
编解码器的错误处理方式是可拓展的.你可以为errors参数注册额外的字符串,方法是把一个名称和一个错误处理函数传给codes.register_error函数.
"""
"""
UnicodeDecodeError
不是每一个字节都包含有效的ASCII字符,也不是每一个字符序列都是有效的UTF-8或UTF-16.因此,把二进制序列转换成文本时,如果假设是这两个编码种的一个,
遇到无法转换的字节序列时会抛出UnicodeDecodeError.
另一方面,很多陈旧的8位编码----如cp1252,iso8859_1和koi8_r----能解码任何字节序列流而不抛出错误,例如随机噪声.
因此,如果程序使用错误的8位编码,解码过程悄无声息,而得到的是无用输出.
乱码字符称为鬼符.
"""
octets = b'Montr\xe9al' # 这些字节序列是使用latin1编码的Montréal;\xe9字节对应é
print(octets.decode('cp1252')) # Montréal 可以使用cp1252解码,因为它是latin1的超集
print(octets.decode('iso8859_7')) # Montrιal iso8859_7用于编码希腊文,因此无法正确解释\xe9字节,而且没有抛出错误
print(octets.decode('koi8_r')) # MontrИal koi8_r用于编码俄文;这里,\xe9表示西里尔字母И
# print(octets.decode('.utf_8')) # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
print(octets.decode('utf_8', errors='replace')) # Montr�al 使用replace错误处理方式,\xe9替换成了�(码位是U+FFFD),这是官方指定的替换字符,表示未知字符.
"""
SyntaxError
Python3默认使用UTF-8编码源码,Python2(从2.5开始)则默认使用ASCII.如果加载的.py模块中包含UTF-8之外的数据,并且没有声明编码,会抛出SyntaxError异常.
为了修正这个问题,可以在文件顶部添加一个神奇的coding注释:
# coding:cp1252
选择对团队而言易于阅读的人类语言,然后使用正确的字符拼写.
"""
# ------------------------------------如何找出字节序列的编码------------------------------------
"""
如何找出字节序列的编码?简单来说,不能,必须有人告诉你.
有些通信协议和文本格式,比如HTTP和XML,包含明确指明内容编码的首部.可以肯定的是,某些字节流不是ASCII,因为其中包含大于127的字节值,而且制定UTF-8和UTF-16
的方式也限制了可用的字节序列.不过即便如此,我们也不能根据特定的位模式来100%确定二进制文件的编码是ASCII或UTF-8.
然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码.
例如,如果b'\x00'字节经常出现,那么可能是16位或32位编码,而不是8位编码方案,因为纯文本中不能包含空字符.
统一字符编码侦测包Chardet就是这样工作的.
"""
import chardet
print(chardet.detect(b'Montr\xe9al')) # {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''}
print(b'Montr\xe9al'.decode('ISO-8859-1')) # Montréal
# ------------------------------------BOM: 有用的鬼符------------------------------------
u16 = 'El Nino'.encode('utf-16')
print(u16) # b'\xff\xfeE\x00l\x00 \x00N\x00i\x00n\x00o\x00'
print(list(u16)) # [255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 110, 0, 111, 0]
"""
二进制序列编码文本通常不会明确指明自己的编码,但是UFT格式可以在文本内容的开头添加一个字节序标记.
上面的例子中UTF-16编码的序列开头有一个额外的字节,b'\xff\xfe',这是BOM,即字节序标记,指明编码时使用Intel CPU的小字节序.
在小字节序设备中,各个码位的最低有效字节在前面:字母'E'的码位时U+0045,在字节偏移的第2位和第3位编码为69和0.
在大字节序CPU中编码顺序是相反的,'E'编码为0和69.
为了避免混淆,UTF-16编码在要编码的文本前面加上特殊的不可见字符ZERO WIDTH NO-BREAK SPACE(U+FEFF).
在小字节序系统中,这个字符编码为b'\xff\xfe'(244, 254).因为按照设计,U+FFFE字符不存在,在小字节序编码中,字节序列b'\xff\xfe'必定是ZERO WIDTH NO-BREAK SPACE,
所有编码器知道该用哪个字节序.
UTF-16有两个变种:UTF-16LE,显式指明使用小字节序;UTF-16BE,显示指明使用大字节序.如果使用这两个变种,不会生成BOM.
如果有BOM,UTF-16编码器会将其过滤掉,为你提供没有ZERO WIDTH NO-BREAK SPACE字符的真正文本.
根据标准,如果文件使用UTF-16编码,并且没有BOM,那么应该假定它使用的是UTF-16BE大字节序编码.然而,Inter x86架构用的是小字节序,
因此有很多文件用的是不带BOM的小字节序UTF-16编码.
UTF-8的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要BOM.
"""
# ------------------------------------处理文本文件------------------------------------
"""
处理文本的最佳实践是'Unicode三明治'.意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串.对输出来说,则要尽量晚的把字符串编码成字节序列.
多数Web框架都是这样做的,使用框架时很少接触字节序列.例如,在Django中,视图应该输出Unicode字符串;Django会负责把响应编码成字节序列,
而且默认使用UTF_8编码.
Python3能轻松的采纳Unicode三明治的建议,因为内置的open函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要的编码,
所以调用my_file.read()方法得到的以及传给my_file.write(text)方法的都是字符串对象.
"""
# 未完待续...