# 第四章 文本和字节序列
"""内容提要:
1.Unicode字符串
2.二进制序列
3.在二者之间转换使用的编码
4.字符/码位/字节表述
5.bytes/bytearray/memoryview等二进制序列的独特特性
6.全部Unicode和陈旧字符集的编解码器
7.避免和处理编码错误
8.处理文本文件的最佳实践
9.默认编码的陷阱和标准I/O的问题
10.规范化Unicode文本,进行安全的比较
11.规范化/大小写折叠/暴力移除音调符号的实用函数
12.使用locate模块和PyUCA库正确第排序Unicode文本
13.Unicode数据库中的字符元数据
14.能处理字符串和字节序列的双模式API
"""
# 4.1 字符问题
"""
字符的标识:即码位,是0~1114111的数字(十进制),在Unicode标准中以4~6个十六进制数字表示
加前缀'U+'如
A : U+0041
欧元符号: U+20AC
高音谱号:U+D11E
字符的表述:取决于具体的编码
编码是在码位和字节序列之间转化时使用的算法
在utf-8中 A 编码为单个字节\x41
utf-16LE中 A 编码为两个字节\x41\x00
编码:把码位转换为字节序列
解码:把字节序列转换为码位"""
# 编码和解码
# s = 'café'
# print(len(s)) # 'café'有4个Unicode字符
# b = s.encode('utf8') # 编码
# print(b) # b'caf\xc3\xa9' byte的字面量以b开头
# print(len(b)) # 字节序列有5个字节(在utf8中é的码位编码成2个字节)
# print(b.decode('utf8')) # 解码
# 4.2 字节概要
"""
字节类型:
1.bytes 不可变
2.bytearray 可变
bytes和bytearray对象的各个元素是介于0~255(含)之间的整数
二进制序列的切片始终是同一类型的二进制序列
二进制序列的字面量表示:
1.可打印的ASCII范围内的字节用ASCII字符表示
2.制表符 换行符 回车符和\对应的字节用转义序列表示 \t \n \r \\
3.其他字节的值,使用时十六进制转义序列 \x00 表示空字节
除了格式化方法(format和format_map)和
几个处理Unicode数据的方法
casefold isdecimal isdentifier isnumeric isprintable encode外
str类型的其他方法都支持bytes和bytearray
re模块也支持处理二进制序列
"""
# 使用数组中的原始数据初始化bytes对象
# 例 包含5个字节的bytes和bytearray对象
"""cafe = bytes('café',encoding='utf8') # bytes对象从str使用指定的编码构建
print(cafe) # b'caf\xc3\xa9'
print(cafe[0]) # 99 # 各个元素是0~255的整数
print(cafe[:1]) # b'c' # 切片还是bytes对象,即使是只有一个字节的切片
cafe_arr = bytearray(cafe)
print(cafe_arr) # bytearray(b'caf\xc3\xa9') bytearray没有字面量表示
print(cafe_arr[-1:]) # bytearray(b'\xa9') bytearray切片还是bytearray"""
# fromhex:解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列
# print(bytes.fromhex('31 4B CE A9'))
"""
构建bytes和bytearray实例海可以调用各自的构造方法,传入以下参数:
1.一个str对象和一个encoding关键紫参数
2.一个可迭代对象,提供0~255之间的整数
3.一个整数,使用空字节创建对应长度的二进制序列
4.一个实现了缓冲协议的对象
如: bytes,bytearray,memoryview,array.array
"""
# 例
# 4-3使用数组中的原始数据初始化bytes对象
"""import array
numbers = array.array('h',[-2,-1,0,1,2]) # 指定类型代码h,创建一个短整型数组
octets = bytes(numbers) # 保存组成numbers的字节序列的副本
print(octets) # b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'是表示那五个短整数的10个字节"""
# 结构题和内存视图
"""struct模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组
示例:memoryview和struct处理gif图像.py"""
# 4.3 基本的编解码器
# 4-5使用3个编解码器编码字符串
# for codec in ['latin_1','utf-8','utf-16']:
# print(codec,'El Niño'.encode(codec),sep='\t')
# print(b'El Ni\xc3\xb1o'.decode('utf8'))
"""
latin1 : 一种重要的编码,是其他编码的基础
cp1252 : Microsoft制定的latin1的超集
cp437 : IBM PC最初的字符集
gb2312 : 编码简体中文的陈旧字符集
utf-8 : 目前web中最常用的编码,与ASCII兼容
utf-16le : 16位编码方案的一种
"""
# 4.4了解编解码问题
# 4.4.1处理UnicodeEncodeError
"""多数非UTF编码只能处理Unicode的一个子集
把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError异常
除非把errors参数传给编码方法或者函数,对错误进行特殊处理"""
# 4-6 编码成字节序列:成功和错误处理
# city = b'S\xc3\xa3o paulo'.decode(encoding='utf8')
# print(city)
"""city = 'São paulo'
print(city.encode('utf8'))
print(city.encode('utf16')) # b'\xff\xfeS\x00\xe3\x00o\x00 \x00p\x00a\x00u\x00l\x00o\x00'
print(city.encode('iso8859_1')) # b'S\xe3o paulo'
# print(city.encode('cp437')) # 报错 UnicodeEncodeError
print(city.encode('cp437',errors='ignore'))
# b'So paulo' 悄无身息地跳过无法处理的字符
print(city.encode('cp437',errors='replace'))
# b'S?o paulo' 把无法编码的字符替换成'?'
print(city.encode('cp437',errors='xmlcharrefreplace'))
# b'São paulo' 把无法编码的字符替换成xmlsh实体"""
# 4.4.2 处理UnicodeDecodeError
"""把二进制序列转换成文本时,遇到无法转换的字节序列时抛出UnicodeDecodeError"""
# 4-7 解码字节序列:成功和错误处理
"""
octets = b'Montr\xe9al'
print(octets.decode('cp1252')) # Montréal 正常解码
# iso8859_7用于编码希腊文
print(octets.decode('iso8859_7')) # Montrιal 没有抛出错误
# koi8_r用于编码俄文
print(octets.decode('koi8_r')) # MontrИal
# print(octets.decode('utf8')) # 报错 不是有效的utf8字节序列
print(octets.decode('utf8',errors='replace')) # Montr�al
"""
# 4.4.3使用预期之外的编码加载模块时抛出SyntaxError
"""
python3的源码默认使用utf8编码
python2使用的是ASCII
在文件顶部添加
# coding:cp1252
可以指定编码方式
"""
# 4.4.4如何找出字节序列的编码
"""Chardet能识别所支持的30种编码"""
#4.4.5 BOM:有用的鬼符
"""BOM(byte-order mark)指的是字节序标记
小字节序:各个码位的最低有效字节在前面
'E' 的 码位时U+0045(十进制69),在字节偏移的第二位和第三位编码为69和0
大字节序:
'E' 的编码为0和69"""
# u16 = 'São paulo'
# print(u16.encode('utf16')) # b'\xff\xfeS\x00\xe3\x00o\x00 \x00p\x00a\x00u\x00l\x00o\x00'
# # 即这里的b'\xff\xfe,指明编码时使用intel CPU的小字节序
#
# print(list(u16))
# 4.5处理文本文件
"""Unicode三明治
1.对于输入来说,尽可能早地把输入的字节序列解码成字符串
2.三明治中的肉片时程序的业务逻辑,在这里只能处理字符串对象,在其他处理过程钟一定不能编码或解码
3.对于输出来说,要尽可能晚地把字符串编码成字节序列"""
# open('cafe.txt','w',encoding='utf8').write('café')
# open('cafe.txt').read() # 'caf茅' 本机
# 4.6 为了正确比较而规范化Unicode字符串
# s1 = 'café'
# s2 = 'cafe\u0301'
# print(s1, s2)
# # >>>café café
# print(len(s1), len(s2))
# # >>>4 5
# print(s1 == s2) # False
"""é和e\u0301在Unicode标准中叫做标准等价物
但python看到的是不同的码位序列,因此判定不相等
这个问题可以使用Unicodedata.normalize()提供的Unicode规范化来解决
Unicodedata.normalize()
第一个参数:
'NFC' 使用最少的码位来构成等价字符串
'NFD' 把组合字符分解成基字符和单独的组合字符
'NFKC' K表示兼容性 是较为严格的规范化形式
μ(希腊字母) U+03BC 和 微符号 μ U+00B5
'NFKD' 'NFKC''NFKD'可能会在规范化的过程中损失或者曲解意思
不能用于持久存贮
"""
# normalize函数示例
"""from unicodedata import normalize
s1 = 'café' # 作为组合字符
s2 = 'cafe\u0301' # 分解为基字符和重音符
print(len(s1), len(s2))
# >>>4 5
print(len(normalize('NFC', s1)), len(normalize('NFC', s2)))
# >>> 4 4
print(len(normalize('NFD', s1)), len(normalize('NFD', s2)))
# >>> 5 5
print(normalize('NFC',s1) ==normalize('NFC',s2)) # True
print(normalize('NFD',s1) ==normalize('NFD',s2)) # True"""
# NFC的特殊情况
"""from unicodedata import normalize ,name
ohm = '\u2126'
print(ohm)
print(name(ohm)) # OHM SIGN 电阻单位欧姆Ω
ohm_c = normalize('NFC',ohm)
print(name(ohm_c)) # GREEK CAPITAL LETTER OMEGA 希腊小写字母omega
print(ohm == ohm_c) # False
print(normalize('NFC', ohm) == normalize('NFC', ohm_c))# True"""
# NFKC示例
"""from unicodedata import normalize ,name
half = '\u00BD'
print(half)
print(normalize('NFKC', half)) # 1⁄2 分解成3个字符
four_squared = '4²'
print(normalize('NFKC', four_squared)) # 42 分解成4 2
micro = '\u00B5'
micro_kc = normalize('NFKC',micro)
print(micro,micro_kc) # μ μ
print(ord(micro), ord(micro_kc)) # 181 956
print(name(micro), name(micro_kc))
# >>>MICRO SIGN GREEK SMALL LETTER MU"""
# 4.6.1大小写折叠
# 把所有文本变成小写,再做其他转换 这个功能由str.casefold()支持
# 一般情况下 str.casefold()和lower()结果一样
# 特殊情况:
# μ变成小写的希腊字母μ,但在大多数字体显示效果一样
# 德语的Eszett会变成ss
# 4.6.2规范化文本实用函数
# normeq.py
# 4.6.3 极端规划化 去掉变音符号
# sanitize.py
# 4.7 Unicode文本排序
# 使用locale.strxfrm函数做排序的键
# import locale
# locale.setlocale(locale.LC_COLLATE,'pt_BR.UTF-8')
# locale.Error: unsupported locale setting 操作系统不支持
# 在sorted中使用 key= locale.strxfrm 就可以使用区域设置中的区域规则来作为排序的依据
# 4.8 Unicode数据库
"""Unicode标准提供了一个完整的数据库:
包括码位和字符名称之间的映射
各个字符的元数据
字符之间的关系"""
# 元数据示例
"""import unicodedata
import re
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),
char.center(6),
're_dig' if re_digit.match(char) else '-',
'isdig' if char.isdigit() else '-',
'isnum' if char.isnumeric() else '-',
format(unicodedata.numeric(char),'5.2f'),
unicodedata.name(char),
sep = '\t')"""
# 4.9 支持字符串和字节序列的双模式API
# 提供的函数能接受字符串或者字节序列为参数,然后根据类型进行特殊处理
# re和os模块有这样的函数
# 4.9.1正则表达式中的字符串和字节序列
# ramanujan.py
# 4.9.2 os函数中的字符串和字节序列
探索编码默认值 default_encoding.py
# 探索编码默认值
import sys,locale
expressions = """
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()"""
my_file = open('dummy','w')
for expression in expressions.split():
value = eval(expression)
print(expression.rjust(30),'->',repr(value))
"""结果:
locale.getpreferredencoding() -> 'cp936' # 打开文件的默认编码
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp936'
sys.stdout.isatty() -> False
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> False
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> False
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'"""
# 结论:不要依赖默认值
4-4使用memoryview和struct查看gif图像的首部
# 4-4使用memoryview和struct查看gif图像的首部
import struct
fmt = '<3s3sHH' # '<' 小字节序,'3s3s'两个3字节序列,'HH'两个16位二进制整数
with open('sda.gif','rb') as fp:
img = memoryview(fp.read()) # 使用内存中的文件创建了一个memoryview对象
header = img[:10] # memoryview对象的一个切片
tmp = bytes(header) # 转换成字节序列以显示
print(header)
print(tmp)
print(struct.unpack(fmt,header)) # 拆包memoryview对象,得到一个元组,包含类型,版本,宽度和高度
del header
del img # 删除引用释放memoryview实例所占用的内存
# 4.6.2规范化文本实用函数 # normeq.py
# Utility Function for normalized Unicode string C oparoson
from unicodedata import normalize
def nfc_equal(str1,str2):
return normalize('NFC',str1) == normalize('NFC',str2)
def fold_equal(str1,str2):
return (normalize('NFC',str1).casefold() ==
normalize('NFC',str2).casefold())
if __name__ == '__main__':
s1 = 'café' # 作为组合字符
s2 = 'cafe\u0301' # 分解为基字符和重音符
print(s1 == s2)
print(nfc_equal(s1, s2))
fold_equal('A','a')
# 4.6.3 极端规划化 去掉变音符号 # sanitize.py
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)
if __name__ == '__main__':
t = '4² \u00BD café'
print(shave_marks(t))
# 4.9.1正则表达式中的字符串和字节序列 # ramanujan.py
import re
re_numbers_str = re.compile(r'\d+')
re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+')
re_words_bytes = re.compile(rb'\w+')
text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef"
"as 1729 = 1³+12³ = 9³ + 10³.") # 在字符串编译时与前一个拼接起来
text_bytes = text_str.encode('utf8') # 字节序列只能用于字节序列的正则表达式搜索
print('Text',repr(text_str),sep='\n')
print('Numbers')
print(' str :',re_numbers_str.findall(text_str)) # 能匹配泰米尔数字和ASCII数字
print(' bytes :',re_numbers_bytes.findall(text_bytes)) # 只能匹配ASCii数字
print('words')
print(' str :',re_words_str.findall(text_str)) # 能匹配字母\上标\泰米尔数字\和sacii数字
print(' Bytes :',re_words_bytes.findall(text_bytes)) # 只能匹配ascii的字母和数字
"""
Text
'Ramanujan saw ௧௭௨௯as 1729 = 1³+12³ = 9³ + 10³.'
Numbers
str : ['௧௭௨௯', '1729', '1', '12', '9', '10']
bytes : [b'1729', b'1', b'12', b'9', b'10']
words
str : ['Ramanujan', 'saw', '௧௭௨௯as', '1729', '1³', '12³', '9³', '10³']
Bytes : [b'Ramanujan', b'saw', b'as', b'1729', b'1', b'12', b'9', b'10']"""
35岁学Python,也不知道为了啥?