Python 文本和字节序列

  Python 3 明确区分了人类可读的文本字符串和原始的字节序列。隐式地把字节序列转换成 Unicode 文本已成过去。

  深入理解 Unicode 对你可能十分重要,也可能无关紧要,这取决于Python 编程的场景。说到底,本章涵盖的问题对只处理 ASCII 文本的程序员没有影响。但是即便如此,也不能避而不谈字符串和字节序列的区别。此外,你会发现专门的二进制序列类型所提供的功能,有些是Python 2 中“全功能”的 str 类型不具有的。

 

字符问题

  在 2015 年,“字符”的最佳定义是 Unicode 字符。因此,从 Python 3 的str 对象中获取的元素是 Unicode 字符,这相当于从 Python 2 的unicode 对象中获取的元素,而不是从 Python 2 的 str 对象中获取的原始字节序列。

举个?  编码和解码

>>> s = 'café'
>>> len(s)
4
>>> b = s.encode('utf-8')
>>> b
b'caf\xc3\xa9'
>>> len(b)
5
>>> b.decode('utf-8')
'café'

  如果想帮助自己记住 .decode() 和 .encode() 的区别,可以把字节序列想成晦涩难懂的机器磁芯转储,把 Unicode 字符串想成“人类可读”的文本。那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。

 

字节概要

  新的二进制序列类型在很多方面与 Python 2 的 str 类型不同。首先要知道,Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变bytes 类型和 Python 2.6 添加的可变 bytearray 类型。(Python 2.6 也引入了 bytes 类型,但那只不过是 str 类型的别名,与 Python 3 的bytes 类型不同。)

  bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2 的 str 对象那样是单个的字符。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为 1 的切片,如示例:

>>> cafe = bytes('café', encoding='utf_8')
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0]
99
>>> cafe[:1]
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:]
bytearray(b'\xa9')

二进制序列有个类方法是 str 没有的,名为 fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列:

>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'
>>> bytes.fromhex('31 4B CE A9').decode('utf-8')
'1KΩ'

使用数组中的原始数据初始化 bytes 对象

>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> octets = bytes(numbers)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'

 

结构体和内存视图 

  struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理 bytes、bytearray 和 memoryview 对象。

使用 memoryview 和 struct 查看一个 GIF 图像的首部

>>> import struct
>>> fmt = '<3s3sHH' #
>>> with open('filter.gif', 'rb') as fp:
... img = memoryview(fp.read()) #
...
>>> header = img[:10] #
>>> bytes(header) #
b'GIF89a+\x02\xe6\x00'
>>> struct.unpack(fmt, header) #
(b'GIF', b'89a', 555, 230)
>>> del header #
>>> del img
  1. 结构体的格式:< 是小字节序,3s3s 是两个 3 字节序列,HH 是两个16 位二进制整数
  2. 使用内存中的文件内容创建一个memoryview对象
  3. 然后使用它的切片在创建一个memoryview对象,这里不会复制字节序列
  4. 转换成字节序列,这里只是为了显示,这里复制了是个字节
  5. 拆包memoryview对象,得到一个元祖,包含类型、版本、宽度和高度
  6. 删除引用,释放memoryview实例所占用的内存

 

处理UnicodeEncodeError

  多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。

举个?  编码成字节序列:成功和错误处理

 1 city = 'São Paulo'
 2 
 3 #'utf_?' 编码能处理任何字符串
 4 u8 = city.encode('utf_8')
 5 print('utf-8:', u8)
 6 
 7 u16 = city.encode('utf_16')
 8 print('utf-16:', u16)
 9 
10 #'iso8859_1' 编码也能处理字符串 'São Paulo
11 iso = city.encode('iso8859_1')
12 print('iso:', iso)
13 
14 #报错咯,'cp437' 无法编码 'ã'(带波形符的“a”)
15 #city.encode('cp437')
16 
17 #解决方法如下
18 cp_ig = city.encode('cp437', errors='ignore')
19 print('cp ignore:', cp_ig)
20 
21 cp_rp = city.encode('cp437', errors='replace')
22 print('cp replace:', cp_rp)

以上代码执行的结果为:

utf-8: b'S\xc3\xa3o Paulo'
utf-16: b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
iso: b'S\xe3o Paulo'
cp ignore: b'So Paulo'
cp replace: b'S?o Paulo'

注意:

  1. error='ignore' 处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥
  2. 编码时指定error='replace',把无法编码的字符替换成'?';数据损坏了,但是用户知道出现了问题

 

处理文本文件 

  处理文本的最佳实践是“Unicode 三明治”(如图下图所示)。 意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串;Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。

处理文本文件很简单。但是,如果依赖默认编码,你会遇到麻烦。举个?

 1 #打开一个文件cafe.txt并写入内容,w是对文件的模式操作(写操作), encoding是对文件操作的编码
 2 fp = open('cafe.txt', 'w', encoding='utf_8')
 3 fp_len = fp.write('café')
 4 print('fp的io信息:', fp)
 5 print('写入到文件中内容的长度:', fp_len)
 6 fp.close()
 7 
 8 #获取文件的内容
 9 fp2 = open('cafe.txt')
10 print('fp2的io信息:', fp2)
11 '''
12 因为和上面的写入的编码不同,所以直接以默认的编码打开,无法处理é而引发异常
13 '''
14 #print(fp2.read())
15 fp2.close()
16 
17 #解决fp2无法或许文件内容的方法指定打开的时候编码
18 fp3 = open('cafe.txt', encoding='utf-8')
19 print('fp3的io信息:', fp3)
20 print('fp3中的文件内容:', fp3.read())
21 fp3.close()
22 
23 fp4 = open('cafe.txt', 'rb')
24 print('fp4的io信息:', fp4)
25 print('fp4的文件内容:', fp4.read().decode('utf-8'))
26 fp4.close()
27 
28 #另外一种不太可取的解决方案, errors可以设置成replace或者ignore
29 fp5 = open('cafe.txt', 'r', errors='ignore')
30 print('fp5的io信息:', fp5)
31 print('fp5的文件内容:', fp5.read())

以上代码执行的结果为:

fp的io信息: <_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
写入到文件中内容的长度: 4
fp2的io信息: <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='US-ASCII'>
fp3的io信息: <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf-8'>
fp3中的文件内容: café
fp4的io信息: <_io.BufferedReader name='cafe.txt'>
fp4的文件内容: café
fp5的io信息: <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='US-ASCII'>
fp5的文件内容: caf

探索编码默认值

 1 import  sys, locale
 2 
 3 
 4 expressions = """
 5     locale.getpreferredencoding()       
 6     type(my_file)                       
 7     my_file.encoding                    
 8     sys.stdout.isatty()                 
 9     sys.stdout.encoding                 
10     sys.stdin.isatty()                  
11     sys.stdin.encoding
12     sys.stderr.isatty()
13     sys.stderr.encoding
14     sys.getdefaultencoding()
15     sys.getfilesystemencoding()
16 """
17 
18 with open('dummy', 'w') as my_file:
19     for expression in expressions.split():
20         value = eval(expression)
21         print('{:>30}'.format(expression), '->', repr(value))
22         
23 '''
24 locale.getpreferredencoding() 是最重要的设置
25 文本文件默认使用 locale.getpreferredencoding()
26 输出到控制台中,因此 sys.stdout.isatty() 返回 True
27 因此,sys.stdout.encoding 与控制台的编码相同
28 '''

以上代码执行的结果为(终端运行):

ocale.getpreferredencoding() -> 'UTF-8'
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'UTF-8'
           sys.stdout.isatty() -> True
           sys.stdout.encoding -> 'UTF-8'
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'UTF-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'UTF-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'

 

为了正确比较而规范化Unicode字符串

  因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。

?  例如,“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样:

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

  'é' 和 'e\u0301' 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。

     解决方案是使用 unicodedata.normalize 函数提供的Unicode 规范化。这个函数的第一个参数是这 4 个字符串中的一个:'NFC'、'NFD'、'NFKC' 和 'NFKD'。下面先说明前两个。

  NFC(Normalization Form C)使用最少的码位构成等价的字符串,而NFD 把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期:

 1 from unicodedata import normalize
 2 
 3 
 4 s1 = 'café' # 把"e"和重音符组合在一起
 5 s2 = 'cafe\u0301' # 分解成"e"和重音符
 6 print('s1和s2的长度:', len(s1), len(s2))
 7 
 8 print('NFC标准化处理以后的s1,s2的长度:', len(normalize('NFC', s1)), len(normalize('NFC', s2)))
 9 print('NFD标准化处理以后的s1,s2的长度:', len(normalize('NFD', s1)), len(normalize('NFD', s2)))
10 print(normalize('NFC', s1), normalize('NFC', s2))

以上代码执行的结果为:

s1和s2的长度: 4 5
NFC标准化处理以后的s1,s2的长度: 4 4
NFD标准化处理以后的s1,s2的长度: 5 5
café café

  在另外两个规范化形式(NFKC 和 NFKD)的首字母缩略词中,字母 K表示“compatibility”(兼容性)。这两种是较严格的规范化形式,对“兼容字符”有影响。虽然 Unicode 的目标是为各个字符提供“规范的”码位,但是为了兼容现有的标准,有些字符会出现多次。例如,虽然希腊字母表中有“μ”这个字母(码位是 U+03BC,GREEK SMALL LETTER MU),但是 Unicode 还是加入了微符号 'μ'(U+00B5),以便与 latin1 相互转换。因此,微符号是一个“兼容字符”。

NFC的具体应用?

>>> from unicodedata import normalize
>>> half = '½'
>>> normalize('NFKC', half)
'1⁄2'
>>> four_squared = ''
>>> normalize('NFKC', four_squared)
'42'
>>> micro = 'μ'
>>> micro_kc = normalize('NFKC', micro)
>>> micro, micro_kc
('μ', 'μ')
>>> ord(micro), ord(micro_kc)
(956, 956)

  使用 '1/2' 替代 '½' 可以接受,微符号也确实是小写的希腊字母'μ',但是把 '4²' 转换成 '42' 就改变原意了。某些应用程序可以把'4²' 保存为 '4<sup>2</sup>',但是 normalize 函数对格式一无所知。因此,NFKC 或 NFKD 可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述:用户搜索 '1 / 2 inch' 时,如果还能找到包含 '½ inch' 的文档,那么用户会感到满意。

注意:

  使用 NFKC 和 NFKD 规范化形式时要小心,而且只能在特殊情况中使用,例如搜索和索引,而不能用于持久存储,因为这两种转换会导致数据损失。

 

规范化文本匹配实用函数

  由前文可知,NFC 和 NFD 可以放心使用,而且能合理比较 Unicode 字符串。对大多数应用来说,NFC 是最好的规范化形式。不区分大小写的比较应该使用 str.casefold()。

  如果要处理多语言文本,工具箱中应用nfc_equal 和fold_equal 函数。

?  比较规范化 Unicode 字符串

 1 from unicodedata import normalize
 2 
 3 
 4 def nfc_equal(str1, str2):
 5     return normalize('NFC', str1) == normalize('NFC', str2)
 6 
 7 def fold_equal(str1, str2):
 8     return (normalize('NFC', str1).casefold() ==
 9             normalize('NFC', str2).casefold())
10 
11 s1 = 'café'
12 s2 = 'cafe\u0301'
13 print('s1 equal s2:',nfc_equal(s1, s2))
14 
15 print(nfc_equal('A', 'a'))
16 
17 s3 = 'Straße'
18 s4 = 'strasse'
19 
20 print('s3 equal s4', nfc_equal(s3, s4))
21 #转换字符成小写
22 print(fold_equal(s3, s4)) 

以上代码的执行结果为:

s1 equal s2: True
False
s3 equal s4 False
True

极端“规范化”:去掉变音符号

去掉变音符号还能让 URL 更易于阅读,至少对拉丁语系语言是如此。下面是维基百科中介绍圣保罗市(São Paulo)的文章的URL:

http://en.wikipedia.org/wiki/S%C3%A3o_Paulo

其中,“%C3%A3”是 UTF-8 编码“ã”字母(带有波形符的“a”)转义后得到的结果。下述形式更友好,尽管拼写是错误的:

http://en.wikipedia.org/wiki/Sao_Paulo

如果想把字符串中的所有变音符号都去掉,看 ?

 1 import unicodedata
 2 
 3 
 4 def shave_marks(txt):
 5     """去掉全部变音符号"""
 6 
 7     norm_txt = unicodedata.normalize('NFD', txt)        #把所有字符分解成基字符和组合记号
 8     shaved = ''.join(c for c in norm_txt
 9                      if not unicodedata.combining(c))   #过滤掉所有组合记号
10     return unicodedata.normalize('NFC', shaved)         #重组所有字符
11 
12 
13 order = '“Herr Voß: • ½ cup of OEtker™ caffè latte • bowl of açaí.”'
14 print(shave_marks(order))
15 
16 Greek = 'Zέφupoς, Zéfiro'
17 print(shave_marks(Greek))

以上代码执行的结果为:

“Herr Voß: • ½ cup of OEtker™ caffe latte • bowl of acai.”
Zεφupoς, Zefiro

 

Unicode文本排序

  Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽如人意。

? 来了~,对一个生长在 ?? 的水果排序

>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']

  不同的区域采用的排序规则有所不同,葡萄牙语等很多语言按照拉丁字母表排序,重音符号和下加符对排序几乎没什么影响。 因此,排序时“cajá”视作“caja”,必定排在“caju”前面。

  排序后的 fruits 列表应该是:

['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

  在 Python 中,非 ASCII 文本的标准排序方式是使用 locale.strxfrm函数,根据 locale 模块的文档(https://docs.python.org/3/library/locale.html?highlight=strxfrm#locale.strxfrm),这 个函数会“把字符串转换成适合所在区域进行比较的形式”。

   使用 locale.strxfrm 函数之前,必须先为应用设定合适的区域设置,还要祈祷操作系统支持这项设置。在区域设为 pt_BR 的GNU/Linux(Ubuntu 14.04)中,可以使用示例中的命令:

  使用 locale.strxfrm 函数做排序键

1 import locale
2 
3 #设置时区
4 print(locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8'))
5 
6 fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
7 fruits_sort = sorted(fruits, key=locale.strxfrm)
8 print('搞定:', fruits_sort)

以上代码的执行结果为:

pt_BR.UTF-8
搞定: ['acerola', 'atemoia', 'açaí', 'caju', 'cajá']

使用Unicode排序算法排序

?  使用 pyuca.Collator.sort_key 方法

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

 

支持字符串和字节序列的双模式API

  标准库中的一些函数能接受字符串或字节序列为参数,然后根据类型展现不同的行为。re 和 os 模块中就有这样的函数。

正则表达式中的字符串和字节序列

? ramanujan.py:比较简单的字符串正则表达式和字节序列正则表达式的行为

 1 import re
 2 
 3 
 4 re_numbers_str = re.compile(r'\d+')     #编译匹配字符串的数字的正则,连续数字,至少出现一次
 5 re_words_str = re.compile(r'\w+')
 6 re_numbers_bytes = re.compile(rb'\d+')  #编译匹字节序列配数字的正则,连续数字,至少出现一次
 7 re_words_bytes = re.compile(rb'\w+')
 8 
 9 text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef" 
10             " as 1729 = 1³ + 12³ = 9³ + 10³.")
11 
12 text_bytes = text_str.encode('utf_8')
13 
14 print('Text', repr(text_str), sep='\n ')
15 print('Numbers')
16 print(' str :', re_numbers_str.findall(text_str))
17 print(' bytes:', re_numbers_bytes.findall(text_bytes))
18 print('Words')
19 print(' str :', re_words_str.findall(text_str))
20 print(' bytes:', re_words_bytes.findall(text_bytes))

以上代码执行的结果为:

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', '', '12³', '', '10³']
 bytes: [b'Ramanujan', b'saw', b'as', b'1729', b'1', b'12', b'9', b'10']

 

 

 

转载于:https://www.cnblogs.com/demon89/p/7381537.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值