第7章 像高手一样玩转数据
- 字符串
Unicode字符组成的序列,用于存储文本数据。 - 字节和字节数组
8比特整数组成的序列,用于存储二进制数据。
7.1 文本字符串
7.1.1 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))
>>> unicode_test('A')
value="A", name="LATIN CAPITAL LETTER A", value2="A"
>>> unicode_test('$')
value="$", name="DOLLAR SIGN", value2="$"
>>> unicode_test('\u00a2')
value="¢", name="CENT SIGN", value2="¢"
>>> unicode_test('\u20ac')
value="€", name="EURO SIGN", value2="€"
2.使用UTF-8编码和解码
对字符串进行处理时,并不需要在意 Python 中 Unicode 字符的存储细节。
但当需要与外界进行数据交互时则需要完成两件事情:
- 将字符串编码为字节;
- 将字节解码为字符串。
3.编码
4.解码
解码是将字节序列转化为Unicode字符串的过程。
7.1.2 格式化
Python 有两种格式化字符串的方式, 我们习惯简单地称之为旧式(old style)和新式(new style)。 这两种方式在 Python 2 和 Python 3 中都适用(新式格式化方法适用于 Python 2.6 及以上)。
1. 使用%的旧式格式化
旧式格式化的形式为 string % data。其中 string 包含的是待插值的序列。表 7-2 展示了最简单的插值序列,它仅由 % 以及一个用于指定数据类型的字母组成。
例子:
>>> "Our cat %s weighs %s pounds" % (cat, weight)
'Our cat Chester weighs 28 pounds'
字符串内的 %s 意味着需要插入一个字符串。字符串中出现 % 的次数需要与 % 之后所提供的数据项个数相同。如果只需插入一个数据,例如前面的 actor, 直接将需要插入的数据置于% 后即可。如果需要插入多个数据,则需要将它们封装进一个元组(以圆括号为界,逗号分开),例如上例中的 (cat, weight)。
2. 使用{}和format的新式格式化
旧式格式化方式现在仍然兼容。 Python 2(将永远停止在 2.7 版本)会永远提供对旧式格式化的支持。然而,如果你在使用 Python 3,新式格式化更值得推荐。
新式格式化最简单的用法如下所示:
>>> '{} {} {}'.format(n, f, s)
'42 7.03 string cheese'
旧式格式化中传入参数的顺序需要与 % 占位符出现的顺序完全一致,但在新式格式化里,可以自己指定插入的顺序:
>>> '{2} {0} {1}'.format(f, s, n)
'42 7.03 string cheese'
0 代表第一个参数 f; 1 代表字符串 s; 2 代表最后一个参数,整数 n。
参数可以是字典或者命名变量,格式串中的标识符可以引用这些名称:
>>> '{n} {f} {s}'.format(n=42, f=7.03, s='string cheese')
'42 7.03 string cheese
详细见书上。
7.1.3 使用正则表达式匹配
你可能已经会在命令行里使用一些简单的“通配符”模式了,例如 ls*.py, 这条命令的意思是“列出当前目录下所有以 .py结尾的文件名”。
是时候使用正则表达式(regular expression)探索一些复杂模式匹配的方法了。与之相关
的功能都位于标准库模块 re 中,因此首先需要引用它。你需要定义一个用于匹配的模式(pattern)字符串以及一个匹配的对象: 源(source)字符串。简单的匹配,如下所示:
result = re.match(‘You’, ‘Young Frankenstein’)
这里, ‘You’ 是模式, ‘Young Frankenstein’ 是源——你想要检查的字符串。 match() 函数用于查看源是否以模式开头。
对于更加复杂的匹配,可以先对模式进行编译以加快匹配速度:
youpattern = re.compile(‘You’)
然后就可以直接使用编译好的模式进行匹配了:
result = youpattern.match(‘Young Frankenstein’)
match() 并不是比较 source 和 pattern 的唯一方法。下面列出了另外一些可用的方法:
- search() 会返回第一次成功匹配,如果存在的话;
- findall() 会返回所有不重叠的匹配,如果存在的话;
- split() 会根据 pattern 将 source 切分成若干段,返回由这些片段组成的列表;
- sub() 还需一个额外的参数 replacement,它会把 source 中所有匹配的 pattern 改成replacement。
1.使用match()进行准确匹配
match()只能检测以模式串作为开头的源字符串。search() 可以检测任何位置的匹配:
>>> m = re.search('Frank', source)
>>> if m:
... print(m.group())
...
Frank
改变一下匹配的模式:
>>> m = re.match('.*Frank', source)
>>> if m: # match返回对象
... print(m.group())
...
Young Frank
以下是对新模式能够匹配成功的简单解释:
- . 代表任何单一字符;
- * 代表任意一个它之前的字符, .* 代表任意多个字符(包括 0 个);
- Frank 是我们想要在源字符串中某处进行匹配的短语。
match() 返回了匹配 .*Frank 的字符串: ‘Young Frank’。
2.使用search()寻找首次匹配
3.使用findall()寻找所有匹配
之前的例子都是查找到一个匹配即停止。 但如果想要知道一个字符串中出现了多少次字母’n’ 应该怎么办?
>>> m = re.findall('n', source)
>>> m # findall返回了一个列表
['n', 'n', 'n', 'n']
>>> print('Found', len(m), 'matches')
Found 4 matches
将模式改成 ‘n’,紧跟着任意一个字符,结果又如何?
>>> m = re.findall('n.', source)
>>> m
['ng', 'nk', 'ns']
注意,上面例子中最后一个 ‘n’ 并没有匹配成功,需要通过 ? 说明 ‘n’ 后面的字符是可选的:
>>> m = re.findall('n.?', source)
>>> m
['ng', 'nk', 'ns', 'n']
4.使用split()按匹配切分
示例展示了如何依据模式而不是简单的字符串(就像普通的 split() 方法做的)将一个字符串切分成由一系列子串组成的列表:
>>> m = re.split('n', source)
>>> m # split返回的列表
['You', 'g Fra', 'ke', 'stei', '']
5.使用sub()替换匹配
这和字符串 replace() 方法有些类似,只不过使用的是模式而不是文本串:
>>> m = re.sub('n', '?', source)
>>> m # sub返回的字符串
'You?g Fra?ke?stei?
6.模式:特殊的字符
一些基本模式:
- 普通的文本值代表自身,用于匹配非特殊字符;
- 使用 . 代表任意除 \n 外的字符;
- 使用 * 表示任意多个字符(包括 0 个);
- 使用 ? 表示可选字符(0 个或 1 个)。
Python 的 string 模块中预先定义了一些可供我们测试用的字符串常量。我们将使用其中的 printable 字符串,它包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格符以及标点符号:
>>> import string
>>> printable = string.printable
>>> len(printable)
100
>>> printable[0:50]
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
>>> printable[50:]
'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
printable 中哪些字符是数字?
>>> re.findall('\d', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
哪些字符是数字、字符或下划线?
>>> re.findall('\w', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '_']
哪些属于空格符?
>>> re.findall('\s', printable)
[' ', '\t', '\n', '\r', '\x0b', '\x0c']
7.模式:使用标识符
表7-4:模式标识符
(表格太大,详见书上)
在看下面的例子时,你可能需要时不时地查阅上面的表格。先来定义我们使用的源字符串:
>>> source = '''I wish I may, I wish I might
... Have a dish of fish tonight.'''
首先,在源字符串中检索 wish:
>>> re.findall('wish', source)
['wish', 'wish']
接着,对源字符串任意位置查询 wish 或者 fish:
>>> re.findall('wish|fish', source)
['wish', 'wish', 'fish']
从字符串开头开始匹配 wish:
>>> re.findall('^wish', source)
[]
从字符串开头开始匹配 I wish:
>>> re.findall('^I wish', source)
['I wish']
从字符串结尾开始匹配 fish:
>>> re.findall('fish$', source)
[]
最后,从字符串结尾开始匹配 fish tonight.:
>>> re.findall('fish tonight.$', source)
['fish tonight.']
^ 和 叫作锚点(anchor):将搜索域定位到源字符串的开头, 则定位到末尾。上面例子中的 .$ 可以匹配末尾的任意字符,包括句号,因此能成功匹配。但更准确地说,上面的例子应该使用转义符将 . 转义为句号,这才是我们真正想示意的纯文本值匹配:
>>> re.findall('fish tonight\.$', source)
['fish tonight.']
接下来查询以 w 或 f 开头,后面紧接着 ish 的匹配:
>>> re.findall('[wf]ish', source)
['wish', 'wish', 'fish']
查询以若干个 w、 s 或 h 组合的匹配:
>>> re.findall('[wsh]+', source)
['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']
查询以 ght 开头,后面紧跟一个非数字非字母字符的匹配:
>>> re.findall('ght\W', source)
['ght\n', 'ght.']
查询以 I 开头,后面跟着 wish 的匹配(wish 出现次数尽量少):
>>> re.findall('I (?=wish)', source)
['I ', 'I ']
最后查询以 wish 结尾,前面为 I 的匹配(I 出现的次数尽量少):
>>> re.findall('(?<=I) wish', source)
[' wish', ' wish']
有时,正则表达式的语法可能会与 Python 本身的语法冲突。例如,我们期望下面例子中的模式能匹配任何以 fish 开头的词:
>>> re.findall('\bfish', source)
[]
为什么没有匹配成功?第 2 章曾提到, Python 字符串会使用一些特殊的转义符。例如上面的 \b,它在字符串中代表退格,但在正则表达式中,它代表一个单词的开头位置。因此,把 Python 的普通字符串用作正则表达式的模式串时需要特别注意, 不要像上面一样与转义符产生冲突。 或者在任何使用正则表达式的地方都记着在模式串的前面添加字符 r,这样可以告诉 Python 这是一个正则表达式,从而禁用字符串转义符,如下所示:
>>> re.findall(r'\bfish', source)
['fish']
8.模式:定义匹配的输出
当使用 match() 或 search() 时,所有的匹配会以 m.group() 的形式返回到对象 m 中。如果你用括号将某一模式包裹起来, 括号中模式匹配得到的结果归入自己的 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
7.2 二进制数据
处理二进制数据有趣多了。你需要了解像字节序(endianness,电脑处理器是如何将数据组织存储为字节的)以及整数的符号位(sign bit)之类的概念。你可能需要研究二进制文件格式、网络包等内容,从而对其中的数据进行提取甚至修改。本节将了解到 Python 中有关二进制数据的一些基本操作。
7.2.1 字节和字节数组
Python 3 引入了下面两种使用 8 比特序列存储小整数的方式,每 8 比特可以存储从 0~255的值:
- 字节是不可变的,像字节数据组成的元组;
- 字节数组是可变的,像字节数据组成的列表。
7.2.2 使用struct转换二进制数据
7.2.3 其他二进制数据工具
7.2.4 使用binascii()转换字节/字符串
7.2.5 位运算符
参考文献:
1. 《Python语言及其应用》。