人类使用文本,计算机使用字节序列。
——Esther Nam 和 Travis Fischer
“Character Encoding and Unicode in Python”

文章目录

  • 4.1 字符问题
  • 4.2 字节概要
  • 4.3 基本的编解码器
  • 4.4 处理编解码错误
    • 4.4.1 处理UnicodeEncodeError(编码错误)
    • 4.4.2 处理 UnicodeDecodeError(解码错误)
    • 4.4.5 BOM 有用的鬼符
  • 4.5 处理文本文件
  • 4.6 规范化Unicode字符串
    • 4.6.1 大小写折叠
    • 4.6.3 极端的规范化:从Unicode到ASCII
  • 4.7 对Unicode排序
  • 4.8 Unicode数据库
  • 声明

4.1 字符问题

字符是什么?

在 2015 年,“字符”的最佳定义是 Unicode 字符。因此,从 Python 3 的 str 对象中获取的元素是 Unicode 字符。这相对于python 2的 str 对象中获取的原始字节序列来说无异于更加方便——你看见几个字符就能获取到几个对象。

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

  • 字符的标识,即码位,是 0-1 114 111 的数字(十进制),在 Unicode 标准中以 4-6个十六进制数字表示,而且加前缀“U+”。
  • 字符的具体表述取决于所用的编码。编码是在码位和字节序列之间转换时使用的算法。

把码位转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码。

>>> s = '流畅的python'
>>> len(s)
9
>>> b = s.encode('utf8')
>>> b
b'\xe6\xb5\x81\xe7\x95\x85\xe7\x9a\x84python'
>>> len(b)
15
>>> b.decode('utf8')
'流畅的python'

4.2 字节概要

Python 内置了两种基本的二进制序列类型:不可变 bytes 类型和可变 bytearray 类型。bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整数,而非单个字符(这与ASCII码十分相似)。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为 1 的切片。这说明在处理其时务必要将其当成整体来看待。

>>> s = bytes('流畅的python', encoding='utf8')
>>> s
b'\xe6\xb5\x81\xe7\x95\x85\xe7\x9a\x84python'
>>> s[0]
230
>>> s[:1]
b'\xe6'
>>> s_arr = bytearray(s)
>>> s_arr
bytearray(b'\xe6\xb5\x81\xe7\x95\x85\xe7\x9a\x84python')
>>> s_arr[-1:]
bytearray(b'n')

除了格式化方法(format 和 format_map)和几个处理 Unicode 数据的方法(包括casefold、isdecimal、isidentifier、isnumeric、isprintable 和 encode)之外,str 类型的其他方法都支持 bytes 和 bytearray 类型。这意味着,我们可以使用熟悉的字符串方法处理二进制序列,如 endswith、replace、strip、translate、upper等,只有少数几个其他方法的参数是 bytes 对象,而不是 str 对象。

二进制序列有一个特有的方法,fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列:
>>> bytes.fromhex('31 4B CE A9') b'1K\xce\xa9'

二进制序列对象还有其本身的构造方法,传入:

  • 一个 str 对象和一个 encoding 关键字参数。

  • 一个可迭代对象,提供 0~255 之间的数值。

  • 一个实现了缓冲协议的对象(如bytes、bytearray、memoryview、array.array);此时,把源对象中的字节序列复制到新建的二进制序列中。

4.3 基本的编解码器

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

>>> 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'

4.4 处理编解码错误

4.4.1 处理UnicodeEncodeError(编码错误)

我们通过传入errors参数来改变处理错误的方法

>>> msg = '北京2022'
>>> msg.encode('utf8')  # utf?格式可以处理任意字符
b'\xe5\x8c\x97\xe4\xba\xac2022'
>>> msg.encode('utf16')
b'\xff\xfe\x17S\xacN2\x000\x002\x002\x00'
>>> msg.encode('cp437')  # 'cp437'无法编码中文,默认处理方式'strict'抛出UnicodeEncodeError
Traceback (most recent call last):File "<input>", line 1, in <module>File "D:\py\Anaconda\lib\encodings\cp437.py", line 12, in encodereturn codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-1: character maps to <undefined>
>>> msg.encode('cp437', errors='ignore')  # errors='ignore'跳过无法编码的字符
b'2022'
>>> msg.encode('cp437', errors='replace')  # errors='replace'将无法编码的字符替换成'?'
b'??2022'
>>> msg.encode('cp437', errors='xmlcharrefreplace')  # errors='xmlcharrefreplace'将无法编码的字符替换成 XML 实体。
b'北京2022'

这种方法是可扩展的,详见:参考文档

4.4.2 处理 UnicodeDecodeError(解码错误)

不同的编码方式所得的同一编码,其解码肯定不相同。

乱码字符称为鬼符(gremlin)或 mojibake(文字化け,“变形文本”的日文)。演示了使用错误的编解码器可能出现鬼符或抛出UnicodeDecodeError。

>>> octets = b'Montr\xe9al'   # 使用 latin1 编码的“Montréal”
>>> octets.decode('cp1252')   # 'cp1252'能正确处理
'Montréal'
>>> octets.decode('iso8859_7')  # iso8559_7用以编码希腊文,无法正确处理
'Montrιal'
>>> octets.decode('koi8_r')  # koi8_r编码俄文
'MontrИal'
>>> octets.decode('utf_8')  # 抛出UnicodeDecodeError错误
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace')  # 将无法处理的编码替换成�(码位是 U+FFFD)
'Montr�al'

4.4.5 BOM 有用的鬼符

UTF-16编码在小端设备和大端设备中,编码的储存顺序不同。为了区分,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。

UTF-8的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要 BOM。尽管如此,有些应用依旧会在UTF-8文件开头添加BOM。

4.5 处理文本文件

目前处理文本的最佳实践是“Unicode三明治”。

在不同的系统中使用的默认编码(博主的电脑是cp936)不一样,而python如果读取文件时未指定默认编码。这就是说输出可能不如你意。

open('cafe.txt', 'w', encoding='utf_8').write('café')
file = open('cafe.txt')
print(file)
# <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
print(file.read())
# caf茅
file.close()
file = open('cafe.txt', encoding='utf8')
print(file.read())
# café

如果你的代码只运行在你自己的电脑中,那肯定不需要注意编码问题,然而需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开文件时始终应该明确传入 encoding= 参数,因为不同的设备使用的默认编码可能不同,有时隔一天也会发生变化。

4.6 规范化Unicode字符串

由于Unicode中组合字符的存在,相同的Unicode字面量可能有不同的编码,如

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

可以看到,s1, s2打印的结果是一样的,但是表达式s1 == s2却是不成立的。在 Unicode 标准中,'é’和 ‘e\u0301’ 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。

此时应该使用unicodedata.normalize函数提供的Unicode规范化。其有四种模式。

  • NFC 使用最少的码位构成等价的字符串
  • NFD 把组合字符分解成基字符和单独的组合字符
  • NFKC 和 NFKD 形式中,各个兼容字符会被替换成一个或多个“兼容分解”字符,即便这样有些格式损失,但仍是“首选”表述。二分之一’½’(U+00BD)经过兼容分解后得到的是
    三个字符序列 ‘1/2’。

需要注意的是除非特殊情况,否则不用后两种规范化进行储存,原因是其可能会出现数据丢失,而仅将其用于搜索和索引。

4.6.1 大小写折叠

py3新增的str.casefold()方法将会将所有的文本转换为小写。与str.lower()不同,str.casefold()是py对Unicode的专门的解决方案,其进行转换的结果将于前者有些许不同。但是这只占Unicode字符的0.11%(116/110 122)。

4.6.3 极端的规范化:从Unicode到ASCII

利用上面提到的一些函数和方法,我们可以自定义实现一些函数,使得字符串被极端的规范化——甚至使其与最简单的字符集之一,ASCII码对应。

4.7 对Unicode排序

原生python下虽然能对非ASCII码进行排序,但是其设置比较复杂,且容易更改全局的字符编码方案,条件比较苛刻。

对Unicode排序存在一个第三方库:PyUCA。其能比较方便的实现排序。

>>> 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']

4.8 Unicode数据库

import unicodedata
import rere_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')

参考链接


声明

本文来自《流畅的python》以及笔者自己的思考,如有错误,欢迎指正。

流畅的python读书笔记④:文本和字节序列相关推荐

  1. 流畅的Python读书笔记

    流畅的Python 说明 我发现流畅的python更适合我现在看,因为它写的很详细.而effective python知识点不是很连贯,我先看完这本书,再去过一遍effective python吧! ...

  2. 流畅的python读书笔记-第一章Python 数据模型

    第一章 python数据类型 1 隐式方法 利用collections.namedtuple 快速生成类 import collectionsCard = collections.namedtuple ...

  3. python读书笔记2000_流畅的Python读书笔记

    特殊方法的存在是为了Python解释器调用的,你自己并不需要去调用他们,比如说my_object.len()这种写法是没有的,应该使用len(my_object).在使用len(my_object)的 ...

  4. python vector_[流畅的Python]读书笔记之十三运算符重载

    运算符重载 Python 关于运算符重载的规则: 不能重载内置类型的运算符 不能新建,只能重载 某些运算符不能重载--is.and.or 和 not 一元运算符 __neg__ __pos__ __i ...

  5. python用可变参数求积_流畅的python读书笔记-第八章-对象引用、可变性和垃圾回收...

    对象不是个盒子 class Gizmo: def __init__(self): print('Gizmo id: %d' % id(self)) x = Gizmo() print(x) y = G ...

  6. 流畅的Python读书笔记-第八章-对象引用、可变性和垃圾回收

    第8章:对象引用,可变性和垃圾回收 在Python里面变量不是盒子,而是便利贴,类似于Java中的引用变量,因此最好把它们理解为附加在对象上的标注. 因为变量不过是标注,因此无法阻止为对象贴上多个标注 ...

  7. 读书笔记:《流畅的Python》第4章 文本和字节序列

    # 第四章 文本和字节序列"""内容提要:1.Unicode字符串2.二进制序列3.在二者之间转换使用的编码4.字符/码位/字节表述5.bytes/bytearray/m ...

  8. 流畅的python学习笔记(三):数据结构(3:文本和字节序列)

    文本和字节序列 大纲 1. 字符问题 2. 字节概要 2.1 结构体和内存视图 3. 基本的编解码器 4. 了解编解码问题 4.1 处理UnicodeEncodeError 4.2 处理Unicode ...

  9. [流畅的Python][4][文本和字节序列]

    第4章 文本和字节序列 人类使用文本,计算机使用字节序列.--------Esther Nam和Travis Fischer Python3明确地区分了人类可读的文本字符串和原始的字节序列 4.1 字 ...

最新文章

  1. 嵌入式系统之温限使用
  2. 支付宝人脸数据被共享?李开复道歉
  3. 中文微博与社会化媒体的区域距离
  4. vscode可以打开jupyternotebook吗_刚刚,官方宣布 VS Code 支持 Python 全开发了!
  5. leetcode 1006. 笨阶乘
  6. 打造高效前端工作环境-tmuxinator
  7. 实现一个简易版的微博,包含 client 和 server 两部分,并实现四个基础功能:关注、取关、发微博、获取用户微博列表
  8. 实现添加商品信息功能
  9. Python bcrypt 加密验证密码
  10. 标签系统 -- 用户画像
  11. 如何为Java面试准备项目经验
  12. 令人啼笑皆非的不靠谱产品是如何诞生的?
  13. python群发邮件 不进垃圾箱_邮件群发不进垃圾箱
  14. 【五校联考2015 8.20】宝藏
  15. 挂代理后git依然无法XXX
  16. 马王堆汉墓帛书‧老子——乙本释文(道经)
  17. 使用git拉取远程仓库代码
  18. 【GNURadio实验报告】实验2-使用GNURadio仿真OOK信号
  19. Pragma section
  20. Cocos2dx 之 cocosbuilder的使用

热门文章

  1. 进程学习:进程间通信(传统通信方式)1.无名管道
  2. Error - 使用statsmodels报错ModuleNotFoundError: No module named 'pandas.tseries.tools'
  3. web端腾讯PAG初体验
  4. 一次项目开发中,收获的经验和教训
  5. 如何在前端实现甘特图?SpreadJS两招搞定
  6. 实验1 OpenGL初识
  7. Linux系统中如何查找大文件或目录文件夹的方法
  8. CAD打开多个文档只启动一个程序
  9. 苹果6s登录id显示无法连接服务器,iPhone6s appleID无法登入
  10. 比较好用的两款免费音频(视频)格式转换、处理软件