s = 'safé' # 一共有 4 个 Unicode 对象
len(s)

b = s.encode('utf8') # 使用 UTF-8 把 str 对象编码成 bytes 对象
b

len(b) # 在 UTF-8 中， é 编码成字节

b.decode('utf8') #使用 UTF-8 把 bytes 对象解码成 str 对象

"""
Explanation: 本章将讨论以下话题：

字符、码位和字节表述
bytes、bytearray 和 memoryview 等二进制序列的独特特性
全部 Unicode 和陈旧字符集的编解码器
避免和处理编码错误
处理文本文件的最佳实践
默认编码陷阱和标准 I/O 问题
规范化 Unicode 文本，进行安全的比较
规范化、大小写折叠和暴力移除音调符号的实用函数
实用 locale 模块和 PyUCA 库正确地排序 Unicode 文本
Unicode 数据库中的字符元数据
能处理字符串和字节序列的双模式 API

（有一些东西觉得用不到，就没有记，到时候用到可以对照目录看书）
字符问题
字符串是个简单的概念，一个字符序列，问题出现在 “字符” 的定义上。在 2015 年 “字符” 的最佳定义是 Unicode 字符，因此，从 Python 3 的 str 对象获得的元素是 Unicode 字符，这相当于从 Python 2 中的 unicode 对象中获取的元素，而不是从 Python 2 中的 str 对象获取原始字节序列。
把码位转成字节序列的过程叫编码，把字节序列转换成码位的过程是解码。下面展示了这一区分:
End of explanation
"""


cafe = bytes('café', encoding = 'utf_8')
cafe

cafe[0]  # 每个元素都是 range(256) 的整数

cafe[:1] # bytes 对象的切片还是 bytes，即使只有一个元素的切片

cafe_arr = bytearray(cafe) 
cafe_arr #bytearray 没有字面量语法，而是以 bytearray() 和字节序列字面量参数形式显示

cafe_arr[-1:] # bytearray 对象切片还是 bytearray 对象

"""
Explanation: 如果想帮自己记住 .decode() 和 .encode() 的区别，可以把字节序列想象成晦涩难懂的机器磁芯转储，把 Unicode 字符串想象成 “人类可读” 的文本，那么，把字节序列变成人类可读的文本字符串就是编码，把字符串变成用于存储或传输的字节序就是编码
虽然 Python 3 中的 str 类型相当于 Python 2 中的 unicode 类型，只不过换了个名称，不过 Python 3 中的 bytes 类型却不是把 str 类型换个名称那么简单，而且还有关系紧密的 bytearray 类型。因此，在讨论编码和解码问题之前，有必要来介绍一下二进制序列类型 
字节概要
新的二进制序列类型在很多方面与 Python 2 中的 str 类型不同， 首先要知道，Pyhon 内置了两种基本二进制序列类型，Python 3 引入的不可变 bytes 和 Python 2.6 添加的可变 bytearray 类型（Python 2.6 也有 bytes 类型，不过那是 str 类型的别名，与 Python 3 中的 bytes 类型不同）
bytes 或 bytearray 对象的各个元素是介于 0-255（含 255） 的整数，而不像 Python 2 的 str 对象那样是单个的字符。然而，二进制序列的切片始终是同一类型的二进制序列，包括长度为 1 的切片：
End of explanation
"""


bytes.fromhex('31 4b ce a9')

"""
Explanation: my_bytes[0] 获取的是一个整数，而 my_bytes[:1] 获取的是长度为 1 的 bytes 对象。 s[0] == s[:1] 只对 str 序列类型成立，不过 str 这个行为很罕见。对于其他各个序列类型来说，s[i] 返回的是一个元素，s[i:i+1] 返回的是一个相同类型的序列，里面是 s[i] 元素
虽然二进制序列其实是整数序列，但它们的字面量表示法表明其中有 ASCII 文本。因此，各个字节的值可能会用下面 3 种不同的方式显示

可打印的 ASCII 范围内的字节（从 空格 到 ~），使用 ASCII 字符本身
制表符、换行符、回车符和 \ 对应的字节，使用转义序列 \t、\n、\r、和 \
其它字节的值，使用十六进制转义序列（例如，\x00 是空字节）

因此在上面我们看到 b'caf\xc3\xa9' 前 3 个字符在可打印 ASCII 范围内，后面不在
除了格式化方法（format 和 format_map）和几个处理 Unicode 数据的方法（包括 casefold, isdecimal, isidentifier, isnumeric, isprintable, encode) 之外，str 类型的其它方法都支持 bytes 和 bytearray 类型。这意味着，我们可以使用熟悉的方式处理二进制序列，如 endswith, replace, strip, translate, upper 等，只有少数几个其它方法参数是 bytes 对象而不是 str 对象。此外如果正则表达式编译自二进制序列而不是字符串，re 模块中的正则表达式函数也能处理二进制序列。
二进制序列有个类方法是是 str 类型没有的，叫做 fromhex，它的作用是解析十六进制数字对（数字对之间的空格是可选的），构建二进制序列：
End of explanation
"""


import array
numbers = array.array('h', [-2, -1, 0, 1, 2]) # h 表示短整数（ 16 位）数组
octets = bytes(numbers)
octets # 这是表示 5 个短整数的 10 个字节

"""
Explanation: 构建 bytes 或 bytearray 实例还可以调用各自的构造方法，传入以下参数

一个 str 对象和一个 encodeing 关键字参数
一个可迭代对象，提供 0-255 之间的值
一个整数，使用空字节创建对应长度的二进制序列。[ Python 3.5 把这个方法标记为 过时的，不建议用 ]
一个实现了缓冲协议的对象（如 bytes, bytearray, memoryview, array.array）；此时，把源对象中的字节序列复制到新建的二进制序列中

使用缓冲类对象构建二进制序列是一种底层操作，可能涉及类型转换，下面做了演示：
End of explanation
"""


import struct 
fmt = '<3s3sHH'  # < 是小端字节序，3s3s 是两个 3 字节序列，HH 是两个 16 位二进制整数
with open("/home/kaka/Downloads/giphy.gif", "rb") as fp:
    img = memoryview(fp.read()) # 使用内存中的文件内容创建一个 memoryview 对象，这里不会复制字节序列
header = img[:10] #使用它的切片再创建一个 memoryview 对象，这里不会复制字节序列
bytes(header) # 转成字节序列，这里只是为了显示，这里只复制了 10 个字节

struct.unpack(fmt, header) # 拆包 memoryview 对象，得到一个元组，包含类型、版本、宽度和高度

del header  # 删除引用，释放 memoryview 实例所占的内存
del img

"""
Explanation: 使用缓冲类对象创建 bytes 或 bytearray 对象时，始终复制源对象中的字节序。与之相反，memoryview 对象允许在二进制数据结构之间共享内存。如果想从二进制序列中提取结构化信息，struct 模块是重要的工具。下面会用这个模块处理 bytes 和 memoryview 对象
结构体和内存视图
struct 模块提供了一些函数，把打包字节序列转换成不同类型字段组成的元组，或有一些函数用于执行反向转换，把元组转换成打包的字节序列。struct 模块能够处理 bytes、bytearray 和 memoryview 对象
第二章讲过 memoryview 类不是用于创建或存储字节序列的，而是共享内存，让你访问其他二进制序列、打包的数组和缓冲区中的数据切片，无需复制字节序列，例如 Python Imaging Library(PIL) 就是这样处理图像的
下面展示了提取一个 GIF 图像的宽度和高度：
End of explanation
"""


for codec in ['latin_1', 'utf_8', 'utf_16']:
    print(codec, 'El Niño'.encode(codec), sep = '\t') # sep 是分隔符，默认是空格

"""
Explanation: 注意，memoryview 对象的切片是一个新的 memoryview 对象，不会复制字节序列。如果使用 mmap 模块把图像打开为内存映射文件，那么会复制少量字节，这里不讨论。如果你经常读取修改二进制文件可以查一下资料。
基本的编解码器
Python 自带了超过 100 种编解码器，用于文本和字节之间的转换。每个编解码器都有一个名称，例如 'utf_8'，而且经常有几个别名，如 'utf8', 'utf-8' 和 'U8'。这些名称可以传给 open()、str.encode()、bytes.decode() 等函数的 encoding 参数。下面展示了使用 3 个编解码器把相同文本编码成不同的字节序列
End of explanation
"""


city = "SãoPaulo"
city.encode('utf_8') # utf_? 能处理任何字符串

 city.encode('utf_16')

city.encode('iso8859_1')

city.encode('cp437') # 无法编码 ã，默认的错误处理方式是 'strict' 抛出 UnicodeEncodeError

city.encode('cp437', errors = 'ignore') # 跳过无法编码的字符，这种做法通常要出大问题

city.encode('cp437', errors = 'replace') # 把无法编码的字符替换成 '?'，数据会损坏了，但是用户知道出了问题

city.encode('cp437', errors = 'xmlcharrefreplace') # 将无法编码的字符串换成 XML 实体

"""
Explanation: 了解编解码问题
虽然有个一般性的 UnicodeError 异常，但是报告错误时几乎都会指明具体的异常：UnicodeEnocdeError（把字符串转换成二进制序列时）或 UnicodeDecodeError（把二进制序列转换成字符串时）。如果源码的编码与预期不符，加载 Python 模块时还可能抛出 SyntaxError。接下来说明如何处理这几种错误。
处理 UnicodeEncodeError
多数非 UTF 编解码器只能处理 Unicode 字符的一部分子集。把文本转成字节序列时，如果目标编码中没有定义某个字符，就会抛出 UnicodeEncodeError 异常，除非把 errors 参数传给编码方法或函数，对错误进行特殊处理。
End of explanation
"""


octets = b'Montr\xe9al' # 这个字节序列使用 latin1 编码的 Montréal
octets.decode('cp1252') # 可以使用 cp1252，因为它是 latin1 的超集

octets.decode('iso8859_7') # iso8859_7 用于编码希腊文，因此无法正确解释 \xe9 字节，而且没抛出错误

octets.decode('koi8_r') # 编码俄文，同样无法正确解释 \xe9，没抛出错误

octets.decode('utf_8') # utf_8 检测到 这不是有效的 utf_8 编码，抛出 UnicodeDecodeError 错误

octets.decode('utf_8',  errors = 'replace') # 使用 replace 的错误处理方式

"""
Explanation: 编解码的错误处理方式是可扩展的，你可以为 errors 参数注册额外的字串，方法是把一个名称和一个错误处理函数传给 codecs.register_error 函数。
处理 UnicodeDecodeError
不是每一个字节都包含有效的 ASCII 字符，也不是每一个字符序列都是有效的 UTF_8 或者 UTF_16。因此，把二进制序列转换成文本时，如果假设这两个编码中的一个，遇到无法转换的字节序列会抛出 UnicodeDecodeError。另一方面，很多陈旧的 8 位编码 -- 如 'cp1252'、'iso8859_1' 和 'koi8_r' 能解码任何字节序列流而不抛出错误，例如随机噪声。因此如果程序使用错误的 8 位编码，解码过程悄无声息，得到的是无用输出（乱码称为 “鬼符”，gremlin 或 mojibake）。
下面展示了使用错误的编解码器可能出现鬼符或抛出 UnicodeDecodeError
End of explanation
"""


u16 = 'El Niño'.encode('utf_16')
u16

"""
Explanation: 使用预期之外的编码加载模块时抛出 SyntaxError
Python 3 默认使用 UTF-8 编码源码， Python 2（从 2.5 开始）默认使用 ASCII，如果加载的 .py 模块中包含 UTF-8 之外的数据，而且没有声明编码，会得到类似下面的信息:

SyntaxError: Non-UTF-8 code starting with '\xc1' in file C:...\xxx.py on line 8, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details  

为了修正这个问题，可以再在文件顶部添加一个神奇的 coding 注释，例如这样:
```
coding: utf-8
```
如何找出字节序的编码
简单的来说，不能，必须有人告诉你
有些通信协议的文件格式，例如 HTTP 和 XML，包含明确指定内容编码的首部。ASCII 编码不会有大于 127 的值，但是也不能以此作为判断是不是 ASCII 编码的依据。然而，就像人类语言也有规则和限制一样，只要假定字节流是人类可读的纯文本，就可能通过试探和分析找出编码。例如，如果 b'\x00' 经常出现，可能是 16 位或 32 位编码，而不是 8 位编码方案，因为纯文本中不能包含空字符。如果字节序列 b'\x20\x00' 经常出现，可能是 UTF-16LE 编码中的空格字符等等。
统一字符编码侦测包 Chardet 就是这样工作的，他能侦测识别所支持的 30 种编码。 Chardet 是一个 Python 库，可以在程序中使用，下面是它对本文源码的检测报告：

4_code.ipynb: utf-8 with confidence 0.99

有用的鬼符
在前面你可能能注意到了，UTF-16 编码的序列开头有几个额外的字节，如下所示
End of explanation
"""


list(u16)

"""
Explanation: 这里的 b'\xff\xfe' 是 BOM，即 字节序标记（byte-order mark），指明编码时使用 IntelCPU 的小端字节序
小端字节序是低位在前，高位在后。字母 'E' 的编码是 U+0045，十进制数 69，字节的偏移第二位和第三位编码是 69，0（两位代表一个字符）。
End of explanation
"""


u16 = 'El Niño'.encode('utf_16le')
list(u16)

u16 = 'El Niño'.encode('utf_16be')
list(u16)

"""
Explanation: 大端高位在前，低位在后。为了避免混淆，UTF-16编码要在编码文本前加上特殊的不可见字符 ZERO WIDTH NO-BREAK SPACE（U+FEFF）。在小端字节序，这个字符编码为 b'\xff\xfe'（十六进制数 255, 254）。因为按照设计，U+FEFF 字符不存在，在小字节序编码中，字节序 b'\xff\xfe' 必定是 ZERO WIDTH NO-BREAK SPACE，所以编码器知道要用哪个字节序
UTF-16 有两个变种：UTF-16LE，显式指定使用小字节序；UTF-16BE，显示指定使用大字节序，如果使用这两个变种，不会生成 BOM
End of explanation
"""


open('cafe.txt', 'w', encoding = 'utf_8').write('café')

open('cafe.txt').read() # 这个在 Windows 下可能会出现问题，因为 Windows 系统默认编码可能是 cp1252

"""
Explanation: 根据标准，如果文件使用 UTF-16 编码，而没有 BOM，假定它使用 UTF-16BE 编码，然而，Intel x86 架构使用小字节序，因此有很多文件用的是不带 BOM 的小字节序 UTF-16 编码
处理文本文件
处理文本文件最佳方法是 “Unicode 三明治” 方法，尽早的把输入（例如读取文件）的字节序列解码成字符串，业务逻辑处理的是字符串对象，输出要尽可能晚的将字符串编码成字节序列
Python 3 中可以轻松做到这点，因为内置的 open 函数会在读取文件时做必要的解码，以文本模式写入文件时还会做必要的编码，所以调用 my_file.read() 方法得到的以及传给 my_file.write(text) 方法的都是字符串对象
可以看到，处理文本文件很简单，但是如果依赖默认编码可能会遇到麻烦：
End of explanation
"""


s1 = 'café'
s2 = 'cafe\u0301'
s1, s2

len(s1), len(s2)

s1 == s2

"""
Explanation: 需要在多台设备或多种场合运行的代码，一定不能依赖默认编码，打开文件时候应该明确传入 encoding= 参数。
为了正确比较而规范化 Unicode 字符串
因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号，打印时作为一个整体)，所以字符串比较起来很复杂
例如 'café' 这个词可以使用两种方式构成，分别有 4 个和 5 个码位，但结果一样:
End of explanation
"""


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

len(normalize('NFC', s1)), len(normalize('NFC', s2))

len(normalize('NFD', s1)), len(normalize('NFD', s2))

normalize('NFC', s1) == normalize('NFC', s2)

normalize('NFD', s1) == normalize('NFD', s2)

"""
Explanation: U+0301 是 COMBINING ACUTE ACCENT，加在 “e“ 后面得到 ”é“。在 Unicode 标准中， 'é' 和 'e\u030' 这样的序列叫做 ”标准等价物“，应用程序应该把它们看做相同的字符，但是，Python 看到的是不同的码位序列，因此判定二者不等
这个问题的解决方案是使用 unicodedata.normalize 函数提供的 Unicode 规范化。这个函数的第一个参数是这 4 个字符串其中的一个: 'NFC', 'NFD', 'NFKC', 'NFKD'。下面说明前两个
NFC（Normalization Form C) 使用最少的码位构成等价的字符串，而 NFD 把组合字符分解成基字符和单独的组合字符，这两种规范化方式都可以得到预期的结果：
End of explanation
"""


from unicodedata import normalize, name
half = '½'
normalize('NFKC', half)

"""
Explanation: 西方键盘通常能输出组合字符，因此用户输入的文本默认是 NFC 形式，不过为了保险，保存文本之前，最好使用 normalize('NFC', user_text) 清洗字符串。
在另外两个规范化形式（NFKC 和 NFKD）的首字母缩略词中，字母 K 表示 ”compatibility“（兼容性）。这两种是较严格的规范化形式，对 “兼容性字符” 有影响，虽然 Unicode 是为各个字符提供 “规范化” 码位，但是为了兼容现有的标准，有些字符会出现多次，例如虽然希腊字母表中有 "μ" 这个字母（码位是 U+03BC, GREEK SMALL LETTER MU），但是 Unicode 还是加了微符号 µ (U+00B5) 为了和 latin1 相互转换，因此，微符号是一个　“兼容字符”
在 NFKC 和 NFKD 中，各个兼容字符会被替换成一个或多个 “兼容分解” 字符，即便这样有些格式损失，但仍是 “首选” 表述 -- 理想情况下，格式化是外部标记的职责，不应该由 Unicode 处理。下面举个例子。 二分之一 '½'（U+00BD）经过兼容分解后得到的是三个字符序列 '1/2'；微符号 μ 分解后是小写字母 μ (U+03BC)
下面是具体应用：
End of explanation
"""


import os
os.listdir('./test')

os.listdir(b'./test') # \xcf\x80 是 π 的 UTF-8 编码

"""
Explanation: 注意，在 NFKC 或 NFKD 中可能会损失或曲解信息，例如字符$4^{2}$ 就被转换成 42，损失了原意，但是可以为搜索和索引提供便利的中间表述，例如 用户搜索 '1 / 2 inch' 搜到了 '½ inch' 会非常满意
使用 NFKC 和 NFKD 要小心，而且只能在特殊情况使用，例如搜索和索引，而不能用于持久存储，因为这两种转换会造成数据损失
os 函数中的字符串和字节序列
GNU/Linux 内核不理解 Unicode，因此对于任何合理的编码方案来说，文件名中使用字节序列都是无效的，无法解码成字符串。在不同操作系统中使用各种客户端的文件服务器，在遇到这个问题更容易出错
为了规避这个问题，os 模块的所有函数，文件名或者路径名参数既能使用字符串又能使用字节序列。如果这样的函数使用字符串参数调用，该参数会使用 sys.getfilesystemencoding() 得到编解码器的自动编码，然后操作系统会使用相同的编解码器解码。这几乎就是我们想要的行为，与 Unicode 三明治最佳实践一致
如果必须处理（或者修正）黁写无法使用上述方式自动处理的文件名，可以把啊字节序列参数传给 os 模块中的函数，得到字节序列返回值。这一特性允许我们处理任何文件名或路径名，不管里面有多少鬼符。
End of explanation
"""


os.listdir('./test')

os.listdir(b'./test')

pi_name_bytes = os.listdir(b'./test')[1]
pi_name_str = pi_name_bytes.decode('ascii', 'surrogateescape') # 使用 ascii 和 surrogateescape 错误处理方式把它解码成字符串
pi_name_str

pi_name_str.encode('ascii', 'surrogateescape') # 再用同样的方式编码回原始值

"""
Explanation: 为了便于手动处理字符串或字节序列形式的文件名或路径名，os 模块提供了特殊的编码和解码函数 
fsencode(filename) 
如果 filename 是 str 类型（此外还可能是 bytes 类型），使用 sys.getfilesystemencoding() 返回的编解码器把 filename 编码成字节序列，否则，返回未经修改的 filename 字节序列
fsdecode(filename)
如果 filename 是 decode 类型（此外还可能是 str 类型），使用 sys.getfilesystemencoding() 返回的编解码器把 filename 解码成字符串，否则，返回未经修改的 filename 字符串序列
在 Unix 衍生平台，这些函数使用 surrogateescape 错误处理方式，避免遇到意外字节序列时候卡住，Windows 用的是 strict 方式
surrogateescape 会把每个无法解码的字节换成 Unicode 中的 U+DC00 到 U+DCFF 之间的码位，这些码位是保留的，没有分配支付，供程序内部使用。编码时，这些码位会转换成被替换的字节值，如下
End of explanation
"""


