接下来将学到很多操作数据的方法,它们大多与下面这两种内置的python数据类型有关。
- 字符串 Unicode字符组成的序列,用于存储文本数据
- 字节和字节数组 8比特整数组成的序列,用于存储二进制数据
文本字符串
格式化
我们可以使用不同的格式化方法将变量插值(interpolate)到字符串中,即将变量的值嵌入字符串中。Python 有两种格式化字符串的方式,我们习惯简单地称之为旧式(old style)和新式(new style)。这两种方式在 Python 2 和 Python 3 中都适用(新式格式化方法适用于 Python 2.6 及 以上)
使用%的旧式格式化
旧式格式化的形式为 string % data 。其中string包含的是待插值的序列。下面是一些
简单的插值序列,它仅由%以及一个用于指定数据类型的字母组成。
转换类型:
%s 字符串
%d 十进制整数
%x 十六进制整数
%o 八进制整数
%f 十进制浮点数
%e 以科学计数法表示的浮点数
%g 十进制或科学计数法表示的浮点数
%% 文本值%本身
首先格式化一个整数:
栗子们:
In [1]: '%s' % 42
Out[1]: '42'
In [2]: '%d' % 42
Out[2]: '42'
In [3]: '%x' % 42
Out[3]: '2a'
In [4]: '%o' % 42
Out[4]: '52'
接着格式化浮点数:
In [5]: '%s' % 7.03
Out[5]: '7.03'
In [6]: '%f' % 7.03
Out[6]: '7.030000'
In [7]: '%e' % 7.03
Out[7]: '7.030000e+00'
In [8]: '%g' % 7.03
Out[8]: '7.03'
整数和字面值%
In [9]: '%d%%' % 100
Out[9]: '100%'
关于字符串和整数的插值操作:
In [10]: actor = 'Richard Gere'
In [11]: cat = 'Chester'
In [12]: weight = 28
In [13]: "My wife's favorite actor is %s" % actor
Out[13]: "My wife's favorite actor is Richard Gere"
In [14]: "Our cat %s weighs %s pounds" % (cat,weight)
Out[14]: 'Our cat Chester weighs 28 pounds'
字符串内的 %s 意味着需要插入一个字符串。字符串中出现 % 的次数需要与 % 之后所提供的 数据项个数相同。如果只需插入一个数据,例如前面的 actor, 直接将需要插入的数据置于 % 后即可。如果需要插入多个数据,则需要将它们封装进一个元组(以圆括号为界,逗号 分开),例如上例中的 (cat, weight)。
尽管 weight 是一个整数,格式化串中的 %s 也会将它转化为字符串型。
我们可以在%和指定类型的字母之间设定最大和最小宽度,排版以及填充字符等。
我们来定义一个整数 n、一个浮点数 f 以及一个字符串 s:
n = 42
f = 7.03
s = 'string cheese'
使用默认宽度格式化它们:
In [6]: '%d %f %s' % (n,f,s)
Out[6]: '42 7.030000 string cheese'
为每个变量设定最小域宽为10个字符,右对齐,左侧不够用空格填:
In [7]: '%10d %10f %10s' % (n,f,s)
Out[7]: ' 42 7.030000 string cheese'
使用同样的宽度,改为左对齐:
In [8]: '%-10d %-10f %-10s' % (n,f,s)
Out[8]: '42 7.030000 string cheese'
使用同样同样的域宽,但是设定最大字符宽度为4,右对齐。这样的设置会截断超过长度限制的字符串,并且将浮点数的精度限制在小数点后4位:
In [9]: '%10.4d %10.4f %10.4s' % (n,f,s)
Out[9]: ' 0042 7.0300 stri'
去掉域宽为10的限制:
In [10]: '%.4d %.4f %.4s' % (n,f,s)
Out[10]: '0042 7.0300 stri'
改变硬编码方式,将域宽,字符宽度等设定作为参数:
In [11]: '%*.*d %*.*f %*.*s' % (10,4,n,10,4,f,10,4,s)
Out[11]: ' 0042 7.0300 stri'
使用{}和format的新式格式化
新式格式化最简单的用法如下所示:
In [12]: '{} {} {}' .format(n,f,s)
Out[12]: '42 7.03 string cheese'
旧式格式化中传入参数的顺序需要与%占位符出现的顺序完全一致,但在新式格式化里,可以自己指定插入的顺序:
In [13]: '{2} {0} {1}' .format(n,f,s)
Out[13]: 'string cheese 42 7.03'
0代表第一个参数f;1代表字符串s;2代表最后一个参数,整数n。
参数可以是字典或者命名变量,格式串中的标示符可以引用这些名称:
In [14]: '{n} {f} {s}' .format(n=42,f=7.03,s='string cheese')
Out[14]: '42 7.03 string cheese'
我们将之前作为参数的值存到一个字典中:
d = {'n': 42, 'f': 7.03, 's': 'string cheese'}
下面的栗子中,{0}代表整个字典,{1}则代表字典后面的字符串'other'
In [15]: d = {'n': 42, 'f': 7.03, 's': 'string cheese'}
In [16]: '{0[n]} {0[f]} {0[s]} {1}' .format(d,'other')
Out[16]: '42 7.03 string cheese other'
上面这些例子都是以默认格式打印结果的。旧式格式化允许在 % 后指定参数格式,但在新 式格式化里,将这些格式标识符放在 : 后。
首先使用位置参数的例子:
In [18]: '{0:d} {1:f} {2:s}' .format(n,f,s)
Out[18]: '42 7.030000 string cheese'
接着是将参数改为命名参数:
In [19]: '{n:n} {f:f} {s:s}' .format(n=42,f=7.03,s='string cheese')
Out[19]: '42 7.030000 string cheese'
新式格式化也支持其他各类设置(最小域宽、最大字符宽、排版,等等)
下面是最小域宽设为10,右对齐(默认)的栗子:
In [20]: '{0:10d} {1:10f} {2:10s}' .format(n,f,s)
Out[20]: ' 42 7.030000 string cheese'
使用 > 字符设定右对齐更为直观:
In [21]: '{0:>10d} {1:>10f} {2:>10s}' .format(n,f,s)
Out[21]: ' 42 7.030000 string cheese'
最小域宽10,左对齐:
In [22]: '{0:<10d} {1:<10f} {2:<10s}' .format(n,f,s)
Out[22]: '42 7.030000 string cheese'
最小域宽10,居中
In [23]: '{0:^10d} {1:^10f} {2:^10s}' .format(n,f,s)
Out[23]: ' 42 7.030000 string cheese'
新式格式化与旧式格式化相比有一处明显的不同:精度(precision,小数点后面的数字) 对于浮点数而言仍然代表着小数点后的数字个数,对于字符串而言则代表着最大字符个 数,但在新式格式化中你无法对整数设定精度:
In [24]: '{0:>10.4d} {1:>10.4f} {2:>10.4s}' .format(n,f,s)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-24-710b8b83231d> in <module>()
----> 1 '{0:>10.4d} {1:>10.4f} {2:>10.4s}' .format(n,f,s)
ValueError: Precision not allowed in integer format specifier
In [25]: '{0:>10d} {1:>10.4f} {2:>10.4s}' .format(n,f,s)
Out[25]: ' 42 7.0300 stri'
最后可以设定的值是填充字符。如果想要使用空格以外的字符进行填充,只需要把它放在 : 之后,其余任何排版符(<,>,^)和宽度标识符之前即可:
In [27]: '{0:!^20s}' .format('BIG SALE')
Out[27]: '!!!!!!BIG SALE!!!!!!'
使用正则表达式匹配
我们想要使用正则表达式(regular expression)必须先引用re模块。我们需要定义一个用于匹配的模式(pattern)字符串以及一个匹配的对象:源(source)字符串。简单的匹配,如下所示:
In [1]: import re
In [2]: result = re.match('You','Young Frankenstein')
这里。'You'是模式,'Young Frankenstein' 是源——你想要检查的字符串。match()函数是用于查看源是否以模式开头。
对于更加复杂的匹配,可以先对模式进行编译以加快匹配速度:
In [4]: youpattern = re.compile('You')
然后就可以之恶极使用编译好的模式进行匹配:
In [5]: result = youpattern.match('Young Frankenstein')
match()并不是比较source和pattern的唯一方法。下面列出了其他的方法:
- search()会返回第一次匹配成功,如果存在的话。
- findall()会返回多有不重叠的匹配,如果存在的话。
- split()会根据pattern将source切片分成若干段,返回由这些片段组成的列表
- sub()还需要一个额外的参数replacement,它会把source中所有匹配的pattern改成replacement。
1.使用match()进行准确匹配
字符串 'Young Frankenstein' 是以单词 'You' 开头的吗?以下是一些带注释的代码:
In [1]: import re
In [2]: source = 'Young Frankenstein'
In [3]: m = re.match('You',source) #从源字符串的开头开始匹配
In [4]: if m : # 匹配成功返回了对象,将它输出看看匹配得到的是什么
...: print(m.group())
...:
You
In [5]: m = re.match('^You',source) #起始锚点也能起到同样作用
In [6]: if m:
...: print(m.group())
...:
You
尝试匹配'Frank'会如何?
In [7]: m = re.match('Frank',source)
In [8]: if m:
...: print(m.group())
...:
这一次,match() 什么也没有返回,if 也没有执行内部的 print 语句。如前所述,match() 只能检测以模式串作为开头的源字符串。但是 search() 可以检测任何位置的匹配:
In [5]: m = re.search('Frank',source)
In [6]: if m:
...: print(m.group())
...:
Frank
改一下匹配模式:
In [7]: m = re.match('.*Frank',source)
In [8]: if m :
...: print(m.group())
...:
Young Frank
在上面的匹配模式中:
-
.
代表任意单一字符 -
*
代表任意一个它之前的字符,.*
代表任意多个字符(包括0个) - Frank是我们想要在源字符串中某处进行匹配的短语。
match() 返回了匹配 .*Frank 的字符串:'Young Frank'。
2.使用search()寻找首次匹配
我们可以使用search()在源字符'Young Frankenstein' 的任意位置寻找模式 'Frank',无 需通配符 .*:
In [11]: m = re.search('Frank',source)
In [12]: if m: # search返回对象
...: print(m.group())
...:
Frank
3.使用findall()寻找所有匹配
之前的栗子都是查找到一个匹配就停止。但是想要知道一个字符串中出现多少次字母'n'应该怎么办?
In [13]: m = re.findall('n',source)
In [14]: m # findall返回一个列表
Out[14]: ['n', 'n', 'n', 'n']
In [15]: print('Found',len(m),'matches')
Found 4 matches
将模式改为'n',紧跟着任意一个字符
In [16]: m = re.findall('n.',source)
In [17]: m
Out[17]: ['ng', 'nk', 'ns']
上面例子中最后一个 'n' 并没有匹配成功,需要通过 ? 说明 'n' 后面的字符是可 选的:
In [18]: m = re.findall('n.?',source)
In [19]: m
Out[19]: ['ng', 'nk', 'ns', 'n']
4.使用split()按匹配切分
下面的栗子展示了如何依据模式而不是简单的字符串(就像普通的split()方法做的)将一个字符串切分成由一系列子串组成的列表:
In [20]: m = re.split('n',source)
In [21]: m #split返回的列表
Out[21]: ['You', 'g Fra', 'ke', 'stei', '']
5.使用sub()替换匹配
这和字符串replace()方法有些类似,只不过使用的是模式而不是文本串
In [22]: m = re.sub('n','?',source)
In [23]: m #sub返回的字符串
Out[23]: 'You?g Fra?ke?stei?'
6.模式:特殊的字符
我们已经见过的一些基本模式:
- 普通的文本值代表自身,用于匹配非特殊字符
- 使用
*
表示任意多个字符串(包括0个) - 使用
?
表示可选字符(0个或1个) - 使用
.
代表任意除\n外的字符
下面是一些特殊字符
模式 匹配
\d 一个数字字符
\D 一个非数字字符
\w 一个字母或数字字符
\W 一个非字母或非数字字符
\s 空白符
\S 非空白符
\b 单词边界(一个\w与\W之间的范围,顺序可逆)
\B 非单词边界
Python 的 string 模块中预先定义了一些可供我们测试用的字符串常量。我们将使用其中 的 printable 字符串,它包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格 符以及标点符号,下面我们将这一百个字符打印出来:
In [24]: import string
In [25]: printable = string.printable
In [26]: len(printable)
Out[26]: 100
In [27]: printable[0:50]
Out[27]: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
In [28]: printable[50:]
Out[28]: 'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
#printable 中哪些字符是数字?
In [29]: re.findall('\d',printable)
Out[29]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
#哪些字符是数字、字符或下划线?
In [30]: re.findall('\w',printable)
Out[30]: ['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', '_']
#哪些属于空格符?
In [31]: re.findall('\s',printable)
Out[31]: [' ', '\t', '\n', '\r', '\x0b', '\x0c']
注意:正则表达式不仅仅适用于 ASCII 字符,例如 \d 还可以匹配 Unicode 的数字字符,并不 局 限 于 ASCII 中 的 '0' 到 '9'。
7.模式:使用标识符
下面的模式标识符中,expr和其他斜体的单词表示合法的正则表达式。
模式标识符:
模式 匹配
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|b|c 一样)
[^abc] 非(a 或 b 或 c)
prev (?=next) 如果后面为 next,返回 prev
prev (?!next) 如果后面非 next,返回 prev
(?<=prev) next 如果前面为 prev,返回 next
(?<!prev) next 如果前面非 prev,返回 next
先看一些小栗子:
In [1]: import re
首先定义我们使用的源字符串:
In [2]: source = '''I wish I may, I wish I might
...: Have a dish of fish tonight.'''
首先,在源字符串中检索 wish:
In [3]: re.findall('wish',source)
Out[3]: ['wish', 'wish']
接着,对源字符串任意位置查询 wish 或者 fish:
In [4]: re.findall('wish|fish',source)
Out[4]: ['wish', 'wish', 'fish']
从字符串开头开始匹配 wish:
In [5]: re.findall('^wish',source)
Out[5]: []
从字符串开头开始匹配 I wish:
In [6]: re.findall('^I wish',source)
Out[6]: ['I wish']
从字符串结尾开始匹配 fish:
In [7]: re.findall('wish$',source)
Out[7]: []
最后,从字符串结尾开始匹配 fish tonight.:
In [8]: re.findall('fish tonight.$',source)
Out[8]: ['fish tonight.']
^ 和 $ 叫作锚点(anchor):^ 将搜索域定位到源字符串的开头,$ 则定位到末尾。上面例 子中的 .$ 可以匹配末尾的任意字符,包括句号,因此能成功匹配。但更准确地说,上面 的例子应该使用转义符将 . 转义为句号,这才是我们真正想示意的纯文本值匹配:
In [10]: re.findall('fish tonight\.$',source)
Out[10]: ['fish tonight.']
查询以 w 或 f 开头,后面紧接着 ish 的匹配:
In [11]: re.findall('[wf]ish',source)
Out[11]: ['wish', 'wish', 'fish']
查询以若干个 w、s 或 h 组合的匹配:
In [12]: re.findall('[wsh]+',source)
Out[12]: ['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']
查询以 ght 开头,后面紧跟一个非数字非字母字符的匹配:
In [13]: re.findall('ght\W',source)
Out[13]: ['ght ', 'ght.']
查询以 I 开头,后面跟着 wish 的匹配(wish 出现次数尽量少):
In [15]: re.findall('I (?=wish)',source)
Out[15]: ['I ', 'I ']
最后查询以 wish 结尾,前面为 I 的匹配(I 出现的次数尽量少):
In [17]: re.findall('(?<=I) wish',source)
Out[17]: [' wish', ' wish']
有时,正则表达式的语法可能会与 Python 本身的语法冲突。例如,我们期望下面例子中的 模式能匹配任何以 fish 开头的词:
In [18]: re.findall('\bfish',source)
Out[18]: []
为什么没有匹配成功?之前提到,Python 字符串会使用一些特殊的转义符。例如上面 的 \b,它在字符串中代表退格,但在正则表达式中,它代表一个单词的开头位置。因此, 把 Python 的普通字符串用作正则表达式的模式串时需要特别注意,不要像上面一样与转义 符产生冲突。或者在任何使用正则表达式的地方都记着在模式串的前面添加字符r,这样可以告诉Python这是一个正则表达式,从而禁用转义符:
In [19]: re.findall(r'\bfish',source)
Out[19]: ['fish']
8.模式:定义匹配的输出
当使用match()或search()时,所有的匹配会以m.group()的形式返回到对象m中。如果你用括号将某一模式包裹起来,括号中模式匹配的结果归入到自己的group(无名称)中,而调用m.groups()可以得到包含这些匹配的元组,如下所示:
In [20]: m = re.search(r'(. dish\b).*(\bfish)',source)
In [21]: m.group()
Out[21]: 'a dish of fish'
In [22]: m.groups()
Out[22]: ('a dish', 'fish')
(?P< name >expr) 这样的模式会匹配 expr,并将匹配结果存储到名为 name 的组中:
In [23]: m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)',source)
In [24]: m.group()
Out[24]: 'a dish of fish'
In [25]: m.groups()
Out[25]: ('a dish', 'fish')
In [26]: m.group(DISH)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-26-e95cb461ed66> in <module>()
----> 1 m.group(DISH)
NameError: name 'DISH' is not defined
In [27]: m.group('DISH')
Out[27]: 'a dish'
In [28]: m.group('FISH')
Out[28]: 'fish'
二进制数据
处理二进制数据我们需要了解像字节序(endianness,电脑处理器是如何将数据组织存储为字节的)以及整数 的符号位(sign bit)之类的概念。我们可能需要研究二进制文件格式、网络包等内容,从而对 其中的数据进行提取甚至修改。本节将了解到 Python 中有关二进制数据的一些基本操作。
字节和字节数组
Python3引入了下面两种8比特序列存储小整数的方式,每8比特可以存储从0~255的值:
- 字节是不可变的,像字节数据组成的元组
- 字节数组是可变的,像字节数据组成的列表
我们的示例从创建列表 blist 开始。接着需使用这个列表创建一个 bytes 类型的变量 the_ bytes 以及一个 bytearray 类型的变量 the_byte_array:
In [1]: blist = [1,2,3,255]
In [2]: the_bytes = bytes(blist)
In [3]: the_bytes
Out[3]: b'\x01\x02\x03\xff'
In [4]: the_byte_array = bytearray(blist)
In [5]: the_byte_array
Out[5]: bytearray(b'\x01\x02\x03\xff')
bytes 类型值的表示形式比较特殊:以 b 开头,接着是一个单引号,后面跟着
由十六进制数(例如 \x02)或 ASCII码组成的序列,最后以配对的单引号结束。
Python会将这些十六进制数或者 ASCII码转换为整数,如果该字节的值为有效
ASCII 编码则会显示 ASCII 字符。
In [6]: b'\x61'
Out[6]: b'a'
In [7]: b'\x01abc\xff'
Out[7]: b'\x01abc\xff'
下面的栗子说明bytes类型的不可变性和bytearray类型的变量的可变性:
In [8]: the_bytes[1] = 127
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-8-8a3aebe74d04> in <module>()
----> 1 the_bytes[1] = 127
TypeError: 'bytes' object does not support item assignment
In [9]: the_byte_array[1] = 127
In [10]: the_byte_array
Out[10]: bytearray(b'\x01\x7f\x03\xff')
下面两行代码都会创建一个包含256个元素的结果,包含0~255的所有值:
In [11]: the_byte = bytes(range(0,256))
In [12]: the_byte_array = bytearray(range(0,256))
打印bytes或bytearray数据时,Python会以\xxx的形式表示不可打印的字符,以ASCII字符的形式表示可打印的字符(以及一些转义字符,例如\n而不是\x0a)。下面是the_byte的打印结果:
In [13]: the_byte
Out[13]: b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
看起来可能有些困惑,毕竟上面输出的数据是字节(小整数)而不是字符。