Python 中的字符串 & 字节和字节数组 (7)

时间:2023-01-10 15:36:34

本章将学到许多操作数据的方法,它们大多与下面这两种内置的Python数据类型有关。

  • 字符串
    Unicode字符组成的序列,用于存储文本数据
  • 字节和字节数组
    8比特整数组成的序列,用于存储二进制数据

关于文本文件与二进制文件

文本字符串

Unicode

Python3中的Unicode字符串

Python3中的字符串是Unicode字符串而不是字节数组(一个字符一个字节的意思)

这是与Python2相比最大的差别。在Python2中,我们需要区分普通的以字节为单位的字符串以及Unicode字符串

如果你知道某个字符的Unicode ID,可以直接在Python字符串中引用这个ID获得对应字符。下面是几个例子:
1. 用\u及4个十六进制的数字就可以从Unicode 256个基本多语言平面中指定某一特定字符。其中,

  • 前两个十六进制数字用于指定平面号(00到FF)
  • 后面两个数字用于指定该字符位于平面中的位置索引

00号平面即为原始的ASCII字符集,字符在该平面的位置索引与它的ASCII编码一致。例子\u01FF(平面号0x01,索引为0xFF)
2. 我们需要使用更多的比特位来存储那些位于更高平面的字符。Python为此而设计的转义序列以\U开头,后面紧跟着8个十六进制的数字其中最左一位需要为0。(前6位仍然为平面号,最后两位为索引号,因此’\u01aa’等价于’\U000001aa’)

  1. 你也可以通过\N{name}来应用某一字符,其中name为该字符的标准名称,这对所有平面的字符均使用。在Unicode字符名称索引页可以查到字符对应的标准名称。

Python中的unicodedata模块提供了下面两个方向的转换函数:

  • lookup() —— 接受不区分大小写的标准名称,返回一个Unicode字符;
  • name()——接受一个Unicode字符,返回大写形式的名称;
def unicode_test(value):
import unicodedata
name = unicodedata.name(value)
value2 = unicodedata.lookup(name)
print('value="%s", name="%s", value2="%s"' % (value, name, value2))

Python 中的字符串 & 字节和字节数组 (7)

注意,unicode_test(‘\u2603’)的结果,正常应该会显示一个雪人,但实际上什么都没有。问题主要是来源于使用的字体自身的限制。没有任何一种字体涵盖了所有Unicode字符,但缺失对应字符的图片时,会以占位符的形式显示。

字符串函数len可以计算字符串中Unicode字符的个数,而不是字节数:

>>> len('$')
1
>>> len('\U0001f47b')
1

注意

Unicode字符串只是一种符号,具体在计算机上如何存储是另外一回事。比如’\U0001f47b’不代表它占用了计算机的4个字节进行存储。具体如何存储是看编码和解码方式

使用UTF-8编码和解码

对字符串进行处理,并不需要在意Python中Unicode字符的存储细节。
但当需要与外界进行数据交互时则需要完成两件事情:
- 将Unicode字符串编码为字节
- 将字节解码为Unicode字符串

如果Unicode包含的字符种类不超过64000中,我们就可以将字符ID统一存储在2字节中。遗憾的是,Unicode所包含的字符串种类远不止如此。诚然,我们可以将字符ID统一编码在3或4字节中,但这会使空间开销(内存和硬盘)增加3到4倍

因此,UTF-8动态编码方案应运而生。这种方案会动态地为每一个Unicode字符分配1到4字节不等:
- 为ASCII字符分配1字节
- 为拉丁语系(除西里尔语)的语言分配2字节
- 为其他的位于基本多语言平面的字符分配3字节
- 为剩下的字符集分配4字节,这包括一些亚洲语言及符号

UTF-8shi Python、Linux以及HTML的标准文本编码格式。这种编码方式简单快速、字符覆盖面广、出错率低。

在代码中全都使用UTF-8编码会是一种非常棒的体验,你再也不需要不停地转换各种编码格式。如果你创建Python字符串时使用了从别的文本源(例如网页)复制粘贴过来的字符串,一定要确保文本源使用的是UTF-8编码。将Latin-1或者Windows 1252复制粘贴为Python字符串的错误及其常见,这样得到的字节序列是无效的,会产生许多后续隐患。

Unicode 和 UTF-8 有何区别?

举一个例子:It’s 知乎日报你看到的unicode字符集是这样的编码表:

字符 Unicode编码(16进制)
I 0049
t 0074
0027
s 0073
0020
77e5
4e4e
65e5
62a5

每一个字符对应一个十六进制数字。计算机只懂二进制,因此,严格按照unicode的方式(UCS-2),应该这样存储:

字符 Unicode编码(2进制)
I 00000000 01001001
t 00000000 01110100
00000000 00100111
s 00000000 01110011
00000000 00100000
01110111 11100101
01001110 01001110
01100101 11100101
01100010 10100101

这个字符串总共占用了18个字节,但是对比中英文的二进制码,可以发现,英文前9位都是0!浪费啊,浪费硬盘,浪费流量。怎么办?UTF。

UTF-8是这样做的:
1. 单字节的字符,字节的第一位设为0,对于英语文本,UTF-8码只占用一个字节,和ASCII码完全相同;
2. n个字节的字符(n>1),第一个字节的前n位设为1,第n+1位设为0,后面字节的前两位都设为10,这n个字节的其余空位填充该字符unicode码,高位用0补足。

于是,”It’s 知乎日报“就变成了:

字符 UTF-8
I 01001001
t 01110100
00100111
s 01110011
00100000
11100111 10011111 10100101
11100100 10111001 10001110
11100110 10010111 10100101
11100110 10001010 10100101

和上边的方案对比一下,英文短了,每个中文字符却多用了一个字节。但是整个字符串只用了17个字节,比上边的18个短了一点点。

下边是课后作业 (以后慢慢补上):
请将”It’s 知乎日报“的GB2312和GBK码(自行google)转成二进制。不考虑历史因素,从技术角度解释为什么在unicode和UTF-8大行其道的同时,GB2312和GBK仍在广泛使用。

剧透:一切都是为了节省你的硬盘和流量

编码

编码是将字符串转化为一些列字节的过程。字符串的encode()函数所接收的第一个参数是编码方式名。可选的编码方式列在下表:

编码 说明
‘ascii’ 经典的7比特ASCII编码
‘utf-8’ 最常用的以8比特为单位的变长编码
‘latin-1’ 也被成为 ISO 8859-1 编码
‘windows-1252’ Windows常用编码
‘unicode-escape’ Python中Unicode的转义文本格式,\uxxxx或者\Uxxxxxxxx

Python 中的字符串 & 字节和字节数组 (7)

Python 中的字符串 & 字节和字节数组 (7)

encode()函数可以接受额外的第二个参数来帮助你避免编码异常。它的默认值是’strict’,如上例所示,当函数检测到需要处理的字符串包含非ASCII字符时,会抛出UnicodeEncodeError异常。当然该参数还有别的可选值:

>>> # ignore会抛弃任何无法进行编码的字符
>>> snowman.encode('ascii', ignore')
b'
'
>>> # replace会将所有无法进行编码的字符替换为?:
>>> snowman.encode('
ascii', 'replace')
>>> b'
?'
>>> # backslashreplace则会创建一个和unicode-escape类似的Unicode字符串
>>> # 如果你需要一份Unicode转义符序列的可打印版本,见下
>>> snowman.encode('
ascii', 'backslashreplace')
b'
\\u2603'
>>> # 下面的代码可以用于创建网页中使用的字符实体串
>>> snowman.encode('
ascii', ‘xmlcharrefreplace’)
b'
☃'

关于字符实体串

解码

解码是将字节序列转化为Unicode字符串的过程。我们从外界文本源(文件、数据库、网站、网站API等)获得的所有文本都是经过编码的字节串。重要的是需要知道它是以何种方式编码的,这样才能逆转编码过程以获得Unicode字符串。

Python 中的字符串 & 字节和字节数组 (7)

格式化

Python有两种格式化字符串的方式,我们习惯简单地称之旧式(old style)和新式(new style)。

使用%的旧式格式化

% 意义
%s 字符串
%d 十进制整数
%x 十六进制整数
%o 八进制整数
%f 十进制浮点数
%e 以科学计数法表示的浮点数
%g 十进制或科学计数法表示的浮点数
%% 文本值%本身

Python 中的字符串 & 字节和字节数组 (7)

Python 中的字符串 & 字节和字节数组 (7)

使用{}和format的新式格式化

Python 中的字符串 & 字节和字节数组 (7)

Python 中的字符串 & 字节和字节数组 (7)

新式格式化与旧式格式化相比有一处明显的不同:精度(precision,小数点后面的数字)。旧式的对于浮点数而言仍然代表着小数点后数字的个数,对于字符串而言则代表着最大字符个数,但在新式格式化中你无法对证书设定精度
Python 中的字符串 & 字节和字节数组 (7)

使用正则表达式匹配

与正则表达式相关的功能都位于标准库模块re

你需要定义一个用于匹配的模式(pattern)字符串以及一个匹配对象:院(source)字符串

# 'You'是模式
# 'Young Frankenstein'是源
result = re.match('You', 'Young Frankenstein')

对于更加复杂的匹配,可以先对模式进行编译以加快匹配速度:

# youpattern是编译后的模式
youpattern = re.compile('You')
# 'Young Frankenstein'是源
result = youpattern.match('Young Frankenstein')

匹配方法

使用match()进行准确匹配

match()只能检测以模式串作为开头的源字符串
Python 中的字符串 & 字节和字节数组 (7)

使用search()寻找首次匹配

使用findall()寻找所有匹配

使用split()按匹配切分

Python 中的字符串 & 字节和字节数组 (7)

构造正则表达式

特殊字符

模式 匹配
\d 一个数字字符
\D 一个非数字字符
\w 一个字符或数字字符
\W 一个非字符非数字字符
\s 空白符
\S 非空白符
\b 单词边界(一个\w与\W之间的范围或者一个\W与\w之间的范围)
\B 非单词边界

Python的string模块中预先定义了一些可供我们测试用的字符串常量。我们将使用其中的printable字符串,它包含100个可打印的ASCII字符,包括大小写字母、数字、空格符以及标点符号
Python 中的字符串 & 字节和字节数组 (7)
Python 中的字符串 & 字节和字节数组 (7)

关于单词边界\b的应用

“\b”一般应用在需要匹配某一单词字符组成的子串,但这一字符不能包含在同样由单词字符组成的更长的子串中。

比如要替换掉一段英文中的单词“to”,而“today”显然不在替换的范围内,所以正则可以用“\bto\b”来限定。

用得比较多的场景是在HTML标签的匹配中,用以区分相互包含的标签,比如要过滤掉

正则表达式不仅仅适用于ASCII字符

Python 中的字符串 & 字节和字节数组 (7)

模式标识符

模式 匹配
abc 文本值abc
(expr) expr
expr1|expr2 expr1 或 expr2
. \n外的任何字符
^ 源字符串的开头
$ 源字符串的结尾
prev? 0个或1个prev
prev* 0个或多个prev,尽可能多地匹配
prev*? 0个或多个prev,尽可能少地匹配
prev+ 1个或多个prev,尽可能多地匹配
prev+? 1个或多个prev,尽可能少地匹配
prev{m} m个连续的prev
prev{m, n} m到n个连续的prev,尽可能多地匹配
prev{m, n}? m到n个连续的prev,尽可能少地匹配
[abc] a或b或c (和a
[^abc] 非(a或b或c)
prev(?=next) 如果后面为next,返回prev
prev(?!next) 如果后面非next,返回prev
(?<=prev)next 如果前面为prev,返回next
(?<!prev)next 如果前面非prev,返回next
尽可能多地匹配&尽可能少地匹配

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符

考虑这个表达式:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。

为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

测试

Python 中的字符串 & 字节和字节数组 (7)

Python 中的字符串 & 字节和字节数组 (7)

看最后一个例子,为什么没有匹配成功?Python字符串会使用一些特殊的转义符。例如上面的\b,它在字符串中代表退格,但在正则表达式中,它代表一个单词的开头位置。因此,把Python的普通字符串用作正则表达式的模式串时需要特别注意。

在任何使用正则表达式的地方都记着在模式串的前面添加字符r,这样可以告诉Python这是一个正则表达式,从而禁用字符串转义符。

>>> re.findall(r'\bfish', source)
['fish']

定义匹配的输出

当使用match()或search()时,所有的匹配会以m.group()的形式返回到对象mzhong .

如果你用括号将某一模式包裹起来,括号中模式匹配得到的结果归入自己的group中,而调用m.groups()可以得到包含这些匹配的元组,如下所示:

>>> m = re.search(r'(. dish\b).*(\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')

(?P< name >expr)这样的模式会匹配expr,并将匹配结果存储到名为name的组中

>>> m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')
>>> m.group('DISH')
'a dish'
>>> m.group('FISH')
'fish'

二进制数据

字节和字节数组

Python3引入了下面两种使用8比特序列存储小整数的方式
- 字节是不可变的,像字节数据组成的元组
- 字节数组是可变的,像字节数据组成的列表

Python 中的字符串 & 字节和字节数组 (7)

打印bytes或bytearray数据时,Python会以\x??的形式表示不可打印的字符,以ASCII字符的形式表示可打印字符(以及一些转移字符,例如\n而不是\x0a)。但是记住bytes或bytearray不是字符。

Python 中的字符串 & 字节和字节数组 (7)

注意:b’\x01abc\xff’一共包含5个字节

使用struct转换二进制数据

如你所见,Python中有许多文本处理工具(模块、函数等),然而处理二进制数据的工具则要少得多。

标准库中有一个struct模块,专门用于处理类似C和C++中结构体的数据。你可以使用struct模块的功能将二进制数据转换为Python中的数据结构

例子

>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
... width, height = struct.unpack('>LL', data[16:24])
... print('Valid PNG, width', width, 'height', height)
... else:
... print('Not a valid PNG')
...
Valide PNG, width 154 height 141

分析说明:
1. valid_png_header包含8字节序列,它标志着这是一个有效的PNG格式文件。
2. width值位于第16-20字节,height值则位于第21-24字节。
3. width, height = struct.unpack(‘>LL’, data[16:24])

- '>LL'中'>'指明以大端(big-endian)方案存储的。
- '>LL'中'LL'代表两个4字节的无符号长的整数,第一个L对应着width,第二个L对应着height

Python 中的字符串 & 字节和字节数组 (7)

关于大端模式和小端模式

a) Little-Endian就是低位字节排放在内存的低地址端高位字节排放在内存的高地址端
b) Big-Endian就是高位字节排放在内存的低地址端低位字节排放在内存的高地址端

如果我们有一个32位无符号整型0x12345678,那么高位是什么,低位又是什么呢?

在十进制中我们都说靠左边的是高位,靠右边的是低位,在其他进制也是如此。就拿 0x12345678来说,从高位到低位的字节依次是0x12、0x34、0x56和0x78。

以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:

a) Big-Endian: 低地址存放高位,如下图:

栈底 (高地址)
buf[3] (0x78) – 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) – 高位
栈顶 (低地址)

b) Little-Endian: 低地址存放低位,如下图:
栈底 (高地址)
buf[3] (0x12) – 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) – 低位
栈 顶 (低地址)

标识符

标识符 字节序
< 小端方案
> 大端方案
标识符 描述 字节
x 跳过一个字节 1
b 有符号字符(integer) 1
B 无符号字符(integer) 1
h 有符号短整数(integer) 2
H 无符号短整数(integer) 2
i 有符号整数(integer) 4
I 无符号整数(integer) 4
l 有符号长整数(integer) 4
L 无符号长整数(integer) 4
Q 无符号longlong型整数(integer) 8
f 单精度浮点数(float) 4
d 双精度浮点数(float) 8
p 数量和字符(bytes) 1 + 数量
s 字符(bytes) 数量

类型标识符紧跟在字节序标识符的后面。任何标识符的前面都可以添加数字用于指定需要匹配的数量,例如5B代表BBBBB。

Python 中的字符串 & 字节和字节数组 (7)

关于ps
以b编码的字节数组会多一个字节用于表明字节数组的长度,所以要编码/解码一个长度为2的字节数组我们需要3p。
Python 中的字符串 & 字节和字节数组 (7)

格式标识符的长度一定要与待转换数据对应。
Python 中的字符串 & 字节和字节数组 (7)

其它二进制数据工具

一些第三方开源包提供了下面这些更加直观地定义和提取二进制数据的方法:

使用binascii()转换字节/字符串

标准binascii模块提供了在二进制数据和多种字符串表示(十六进制、六十四进制、uuencoded,等等)之间的转换函数。

例如,下面的小李子将8-字节的PNG头打印为十六进制值的形式,而不是Python默认的打印bytes型变量的方式:混合使用ASCII和转义的\x??

Python 中的字符串 & 字节和字节数组 (7)

位运算符

a: 十进制5,二进制0b0101
b: 十进制1,二进制0b0001

位运算:
1. a & b (十进制1,二进制0b0001)
2. a | b (十进制5,二进制0b0101)
3. a ^ b (十进制4,二进制0b0100)
4. ~a (十进制-6| 因为相反数(-5)=取反 + 1|,二进制取决于int类型的大小)
5. a << 1 (十进制10,二进制0b1010)
6. a >> 1 (十进制2,二进制0b0010)