第4章 文本处理
文本是软件工程师日常工作中处理最多的数据类型,几乎无时无刻不在与文本打交道。Linux下有很多的文本处理工具,但是有很多的高级特性都依赖于正则表达式,学习曲线较为陡峭。
Python语言内嵌的字符类型包含大量的文本处理函数,Python的标准库对文本处理提供了很好的支持。所以,我们也能使用Python来处理文本的需求。
4.1 字符串常量
4.1.1 定义字符串
在Python中,所有的字符都是字符串。因为Python不区分字符和字符串,所以Python可以使用单引号或者双引号来定义字符串。如: name = "super man"
。
在遇到特殊字符的时候,我们需要转义字符""对字符进行转移。场景的转移字符有
转义字符 | 含义 | 转义字符 | 含义 |
---|---|---|---|
\n | 换行 | \t | 水平制表 |
\r | 回车 | \|代表一个反斜线字符'' |
如我们在处理windows的路径的时候就需要转义字符的帮忙了
#这个会报错的
path= "c:\next"
#这样才行
path="c:\\next"
除了使用转义字符以外,还可以使用原始字符串(raw string)。原始自负床的使用非常简单,就是在字符串定义前面加上一个"r",如: r"Hello world"。原始字符串会抑制所有的转移,打印字符中的所有反斜杠。所以上面的例子也可以这样写:
path=r"c:\\next"
在Python中,如果你定义的字符串较长,可以使用三引号来定义字符串。使用三引号定义的字符串一般称为多行字符串。多行字符串不受代码缩进规则的限制,因为它本身就不是代码,而是字符串,此外也不会被转义,
Python字符串还有一个容易被忽略的小特性,就是两个字符串会自动主从一个新的字符串。如下:
#s的结果是 helloworld
s="hello" "world"
4.1.2 字符串是不可变的有序集合
Python的字符串有两个特点: 1是不可变的,2是有序的集合。
由于Python字符串是不可变的,所以不能直接对字符串进行修改。但是可以通过字符运算、切片操作、格式化表达式和字符串方法调用等方式创建新的字符串。然后把结果赋值给最初的变量名,达到修改字符串的目的。此外,正式因为不可变的特性。所以对字符串进行操作的时候都会产生一个新的字符串,新的字符串会占用一块独立的内存。因此,操作字符串时需要避免产生太多的中间结果。下面的反面例子:
fruits = ['apple1','apple2','apple3','apple4']
statement = fruits[0]
for item in fruits[1:]:
statement = statement + ', ' + item
print(statement)
在这个例子中,产生了大量的中间结果。产生了临时字符串,它们已产生就被销毁了,白白浪费了程序的运行时间。示例的正确做法是使用join方法,如下:
', '.join(fruits)
Python字符串的第二个特点就是通过下标和切片进行访问。在Python语言中,元组、列表和字符串都是元素的有序集合,都可以使用下标和切片进行访问。
4.1.3 字符串函数
Python提供与字符串处理相关的方法可以分为两大类。一类是可以用于多种类型的通用操作,以内置函数或表达式的方式提供。如len(s)、'x' in s等。另一类是只作用于字符串的特定类型操作,以方法调用的形式提供,如str.split()和str.upper()等。
-
通用操作
如 len('pcm')、 'c' in 'pcm' -
与大小写相关的方法
- upper: 转为大写
- lower: 转为小写
- isupper: 判断是否为大写
- islower: 判断是否为小写
- swapcase: 大写转小写,小写转大写
- capitalize: 将首字母转换为大写
- istitle: 判断字符串是不是标题
- 判断类方法
除了前面介绍的几个is开头的方法,还有下面的这些也是很常见的:
- isalpha: 是否只包含字母
- isalnum: 是否只包含字符串字母和数字
- isspace: 是否只包含空格、制表符、换行符
- isdecimal: 是否自包含数字字符
- 字符串方法startswith和endswith
这两个判断类方法用来判断字符串的前缀或后缀。下面示例一个常见的场景:统计出MongoDB日志文件占用磁盘的大小。
import os
mongod_logs = [item for item in os.listdir('/var/mongo/log') if item.startswith('mongod.log')]
sum_size=sum(os.path.getsize(os.path.join('/var/mongo/log'),item)) for item in mongod_logs)
- 查找类函数
- find:查找子串在字符串中的位置,如果查找失败,返回-1
- index: 与find函数类似,如果查找失败,抛出ValueError异常
- rfind: 与find函数类似,区别在于rindex是从后向前查找
- rindex: 与index函数类似,区别在于rindex是从后向前查找
这几个函数的用法和作用都差不多,这里介绍find的用法
s = 'wo you yi tou xiao mao lv.'
s.find('yi')
- 字符串操作方法
字符串的join函数用来连接字符串列表,组成一个新的、更大的字符串。join是一个字符串处理函数,需要先有字符串,再调用这个函数。因此,如果仅仅需要将几个字符串连接起来,并且不需要插入任何额外的字符,则可以使用空字符串调用join方法,如下所示:
#结果为abc
"".join(['a','b','c'])
#结果为a,b,c
",".join(['a','b','c'])
join函数比前面介绍的更加通用,join函数只有一个参数,并且是iterable而不是列表。也就是说,join接受任何可迭代的对象。因此,如果我们需要将文件中的内容拼接起来组成一个更大的字符串,我们自需要将文件对象传递给join函数即可,因为文件对象本身就是一个可迭代的对象。
with open('/etc/passwd') as f:
print("###".join(f))
join函数最容易被滥用的地方是打印字符串列表时,print函数本身可以通过sep参数指定分隔符:
print('root','/root','/bin/bash',seq=':')
如果使用join函数先组成字符串然后再打印,就很容易出错,并且性能也变差。如join的列表中存在数字,join函数不会自动将数字转换为字符串,然后会抛出异常信息,这样就不能做打印处理了。
接下来看一下与join函数起反作用的split函数。join函数用以将字符串列表(更准确地说,是可迭代对象)拼接成更大的字符串,而split函数正好相反,它用以将一个字符串拆分成一个字符串列表。
split默认使用空白字符作为分隔符,当然也可以指定分隔符。如下:
'super:man'.split(':')
strip、rstrip和lstrip这几个函数用来多字符串进行剪裁,最常用的场景就是去除两端的空白字符。当然也可以传递参数,去除特定的字符,如下:
"##Hello,world####.strip('#')"
replace函数非常简单,顾名思义就是将字符串中的子串替换成一个新的子串。
4.1.4 案例:使用Python分析Apache的访问日志
下面的代码可以统计网站访问的PV和UV。
#!/usr/bin/python
from __future__ import print_function
ips = []
with open('access.log') as f:
for line in f:
ips.append(line.split()[0])
print("PV is {0}".format(len(ips)))
print("UV is {0}".format(len(set(ips))))
下面我们接触collections.Counter保存资源的热度,Count是dict的子类,使用方式与字典类似。对于普通的计数功能,Count比字典更加好用。如下所示:
#变量c的结果是 Count({'a':2,'b':2,'c':1})
c=Counter('abcba')
Counter作为一个计数器,还提供了一个名为most_common的函数,用来显示Counter中取值最大的几个元素。下面的代码使用Counter统计网站中最热门的十项资源:
#!/usr/bin/python
#-*- coding: UTF-8 -*-
from __future__ import print_function
from collections import Counter
c = Counter()
with open('access.log') as f:
for line in f:
c[line.split()[6]] += 1
print("Popular resources : {0}".format(c.most_common(10)))
4.1.5 字符串格式化
在Python中,存在两种格式化字符串的方法,即%表达式和format函数。%表达式从Python诞生之日就开始存在了,是基于C语言的printf模型,目前还广泛使用。format函数是Python2.6和Python3.0新增加的技术,是Python独有的方法,并且和字符串格式表达式的功能有不少的重叠。虽然%表达式目前广泛使用,但是,format函数才是字符串格式化的未来,%表达式在Python未来的版本可能会弃用。
最简单的format函数使用应该是通过参数的位置访问参数。如下所示,通过{}来表示一个占位符,Python会自动将format函数的参数依次传递给{}占位符。
#需要按顺序来填写参数
"{} is better than {}.".format('A','B')
#通过下标来访问
"{0} is better than {1}.".format('A','B')
4.2 正则表达式
正则表达式是个处理文本的强大工具,在文本处理程序中广泛使用,如Offic world、vim等。正则表达式在Linux命令行中使用更加广泛,大部分文本处理工具都支持正则表达式,如grep、awk、sed等。正则表达式,最大的问题就是学习的难度较大,很容易学完就忘了。
4.2.1 正则表达式语法
正则表达式由普通文本和具有特殊意义的符号组成,工程师根据具体的需要,使用它们构造出合适的正则表达式来匹配文本。例如:
- 要匹配给定文本中的所有单词
#"?"用于匹配单词前后可能出现的空格,[a-zA-Z]+代表一个或多个英文字母
?[a-zA-Z]+
- 要匹配一个IP地址,可以使用下面的正则表达式:
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
下表给出正则表达式的基本组成部分
正则表达式 | 描述 | 示例 |
---|---|---|
^ | 行起标识记 | ^imap匹配以imap起始的行 |
$ | 行尾标记 | import$匹配以import结尾的行 |
. | 匹配任意一个字符 | 它只能匹配单个字符,但是可以匹配任意字符,如linu.,可以匹配到linux与linus |
[] | 匹配包含在[字符]之中的任意字符 | coo[kl]能够匹配cook或cool |
[^] | 匹配包含在^字符]之外的任意字符 | 9[^01]可以匹配92、93,但是不匹配91或90 |
[-] | 匹配[]中指定范围内的任意一个字符 | [1-5]匹配1-5的任意一个数字,[a-z]匹配任意一个小写字母 |
? | 匹配之前项1次或0次 | hel?o匹配hello或helo,但是不能匹配helllo |
+ | 匹配之前项1次或多次 | hel+匹配hel或hell,但是不能匹配he |
* | 匹配之前项0次或多次 | hel*匹配he、hel、hell |
{n} | 匹配之前的项n次 | [0-9]{3}匹配任意一个三位数 |
{n,} | 之前的项至少匹配n次 | [0-9]{3,}匹配任意一个三位数或更多的数字 |
{n,m} | 指定之前的项所必须匹配的最小次数和最大次数 | [0-9]{2,5}匹配从两位数到五位数之前的任意一个数字} |
4.2.2 利用re库处理正则表达式
正则表达式虽然本身比较负暂,但是,在Python中使用正则表达式却出奇地简单。在Python中,标准库re模块用来处理正则表达式,它能够顺利处理Unicode和普通字符串。这个模块包含了与正则表达式相关的函数、标志和一个异常。
下面的示例使用re模块下的findall函数来匹配符合并输出符合模式的子串
import re
data = "waht is the difference between python 2.7.13 and Python 3.6.0 ?"
re.findall('python [0-9]\.[0-9]\.[0-9]',data)
如果希望re模块在模式匹配的时候忽略字符的大小写,可以通过传递标志的形式告诉re模块忽略大小写这个需求,如:
re.findall('python [0-9]\.[0-9]\.[0-9]',data,flags=re.IGNORECASE)
除了直接使用re模块中的函数之外,还有另外一种使用正则表达式的方法,那就是创建一个特定模式编译的正则表达式对象,然后使用这个对象中方法。
什么事编译的正则表达式呢?它是一个简单的对象,通过传递模式给re.compile函数创建。编译与非编译方式使用正则表达式的区别,除了使用方法之外,主要是性能方面的差异。编译的性能明显好于非编译的。
import re
data = "waht is the difference between python 2.7.13 and Python 3.6.0 ?"
re_obj = re.compile('python [0-9]\.[0-9]\.[0-9]')
re_obj.findall(data)
4.2.3 常用的re方法
- 匹配类的函数
re模块中最简单的辨识findall函数,该函数在字符串中查找模式匹配,将所有的匹配字符串以列表的形式返回。如果文本中没有任何字符串匹配模式,则返回一个空的列表。如果有一个子串匹配模式,则返回一个元素的列表。所以,不管怎么匹配,都不会出错。这对工程师编写程序来说,减少异常情况的处理,代码逻辑更加整洁。
match函数类似于字符串中startswith函数,只是match更强大。match函数用以匹配字符串的开始部分,如果匹配成功,返回一个SRE_Match类型的对象,如果失败,则返回一个None。例如,我们判断data字符串是否以"What"和"Not What"开头:
re.match('What',data)
re.match('Not What',data)
下面示例匹配一个字符串是否以数字开头:
re.match('\d+',"123 is one hundred and twenty-three")
re模块中的search函数模式匹配成功时,也会返回一个SRE_Match对象。其与match函数的用法几乎一样,区别在于前者在字符串的任意位置进行匹配,后缀仅对字符串开始部分进行匹配。要注意的是search仅仅查找第一次匹配,如果要返回多个匹配的结果,最简单的做法是使用findall函数。
- 修改类函数
re模块的sub函数类似于字符串的replace函数,只是sub函数支持使用正则表达式,所以更加强大。例如,下面的正则表达式模式,可以同时匹配"2.7.13"和"3.6.0",并将他们都替换为"x.x.x"。
import re
data = "waht is the difference between python 2.7.13 and Python 3.6.0 ?"
re.sub('[0-9]+\.[0-9]+\.[0-9]+','x.x.x',data)
re模块的split函数与Python字符串的split函数功能一样,都是将一个字符串拆分为子串的列表,只是re模块的能够使用正则表达式。例如,对于下面这一段包含了冒号,逗号,单引号和若干空格的文本,我们希望拆分出每一个单次。
text = "MySQL slave binlog position: master host '10.1.1.1',filename 'mysql-bin.000002',position '524994060'"
re.split(r"[':,\s]+",text.strip("'"))
-
大小写不敏感
在re模块中,要忽略大小写的差异,就如同前面的示例那样,使用flags=re.IGNORECASE。 -
非贪婪模式
在正则表达式的字符串匹配中,有贪婪匹配和非贪婪匹配的区别。贪婪匹配总是匹配到最长的那个字符串,非贪婪模式正好相反。例如:我们要匹配以"Beautiful"开头并且以点好结尾的字符串,默认情况下正则表达式使用贪婪匹配,如果要使用非贪婪匹配,只需要在匹配字符串时加上一个"?"。如下所示:
text = "Beautiful is better than ugly.Explicit is better than implicit."
re.findall('Beautiful.*\.',text)
re.findall('Beautiful.*?\.',text)
4.3 字符集编码
在Python编程中,如果不使用Unicode,处理中文时将会遇到一些令人困惑的地方。需要注意的是,Python2.7默认使用的不是Unicode,Python3默认使用的是Unicode。
In [18]: name='超人'
In [19]: print(name)
超人
In [20]: print(name[0:1])
#输出没有结果
In [21]: name=u'超人'
In [22]: print(name)
超人
In [23]: print(name[0:1])
超
上面的示例可以看到使用中文的话,使用分片的时候得不到我们想要的结果。解决的方法也很简单,只要在前面加个"u"来定义Unicode字符串就行了。
4.3.4 Python2和Python3中的Unicode
前面说到Python2中如果要使用Unicode编码,则必须在字符串前面显式地加上一个"u"前缀。其实,Python2也可以默认使用Unicode的字符串的,只需要执行下面的导入即可:
from __future__ import unicode_literals
Python的字符串具有encode方法和decode方法。我们可以使用这两个方法对字符串进行编码或者解码,下面是一个在Python2下运行的例子:
name='超人'
new_name=name.decode('utf8')
#能正常输出"超"
print(new_name[:1])
#使用encode进行编码
new_name.encode('utf-8')
new_name.encode('utf-16')
我们既然已经知道使用encode对Unicode进行编码,使用decode对字符进行解码,那么,如果我们要存储中文的话,可以这样操作:
name=u'超人'
#使用encode进行编码
with open('/tmp/data.txt', 'w') as f:
f.write(name.encode('utf-8'))
#使用decode解码
with open('/tm/data.txt', 'r') as f:
data = f.read()
data.decode('utf-8')
如果需要写入的字符串比较多,而每次都需要进行编码,程序将会变得非常抵消。在Python2中可以使用codecs模块,在Python3中内置的open函数就已经支持指定编码格式。指定编码格式以后,当我们写入时会自动将Unicode转换为特定的编码,读取文件时,自动以特定的UTF进行编码。
在python2中,使用codecs模块进行编码和解码
import codecs
name=u'超人'
with open('/tmp/data.txt', 'w',encoding='utf-8') as f:
f.write(name)
with open('/tm/data.txt', 'r',encoding='utf-8') as f:
data = f.read()
在Python3中,内置的open函数可以指定字符集编码:
name='超人'
with open('/tmp/data.txt', 'w',encoding='utf-8') as f:
f.write(name)
...
4.4 Jinja2模版
4.4.1 模版介绍
模版在Python的web开发中广泛使用,它能够有效地将业务逻辑和页面逻辑分离,使得工程师编写出可读性更好、更加容易维护的代码。
试想一下,要为一个大型的表格构建HTML代码,表格中的数据由数据库中读取的数据以及必要的HTML字符串连接在一起。这个时候,最简单也就是最直接的方式就是Python代码中使用字符串拼接的方式生成HTML代码。如果真的这么做了,对工程师来说将是个噩梦,而且代码无法维护。
此时,可以使用模块将业务逻辑与页面逻辑分隔开来。模块包含的是一个响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道。使用真实的值替代变量,再返回最终得到的响应字符串,这一过程成为渲染。
web开发是最需要使用模版的地方,但是,并不是唯一可以使用模版的地方。模版使用范围比大多数工程师接触的都要广泛,因为模版使用所有基于文本的格式,如HTML,XML,CSV等。使用模版能够编写出可读性更好、更容易理解和维护的代码,并且使用范围非常广泛,因此怎么使用模版主要取决于工程师的想象力和创造力。比如,我们在使用Ansible就会用到。作为工程师,我们也可以使用Jinja2管理工作中的配置文件。一旦学会使用模版管理配置文件,就可以拜托无数繁琐的文本替换工作。
Python的标准库自带了一个简单的模版,下面的代码便是一个模版使用的例子。模版包含的是一个响应信息,其中包含用占位变量表示的动态部分,动态部分的取值取决于具体的应用,并且只有在请求的上下文才能知道。渲染就是使用真实的值替换变量,再返回最终得到的响应字符串。
from string import Template
s=Template('$who is a $role')
s.substitute(who='bob',role='teacher')
s.substitute(who='lily',role='student')
Python自带的模版功能非常有限,例如无法在模版中使用控制语句和表达式,不支持继承和重用等操作。这对于web开发来说是远远不够,因此,出现了第三方的模版系统。目前市面上有非常多的模版系统,其中最知名的是Jinja2和Mako。
4.42 Jinja2语法入门
Jinja2模版引擎之所以使用广泛,是因为它具有以下优点:
- 相对于Template,Jinja2更加灵活,它提供了控制结构、表达式和继承等,工程师可以在模版中做更多的事情。
- 相对于Mako,Jinja2提供了仅有的控制结构,不允许在模版中编写太多的业务逻辑,避免了工程师的乱用行文。
- 相对于Django模版,Jinja2的性能更好
- Jinja2模版的可读性很好。
Jinja2是Flask的一个依赖,如果已经安装了Flask,Jinja2也会随之安装。当然,也可以单独安装Jinja2.
pip install jinja2
接下来将详细介绍Jinja2的语法。
- 语法块
Jinja2可以应用于任何基于文本的格式,如HTML、XML等。Jinja2使用大括号的方式表示Jinja2的语法。在Jinja2中,存在3中语法:
- 控制结构 {%%}
- 变量取值 {{}}
- 注释{##}
下面是使用Jinja控制结构的和注释的一个例子:
{# note: disabled template because we no longer use this
{% for user in users %}
...
{% endfor %}
#}
可以看到,for循环的使用和Python比较类似,但是,没有了复合语句末尾的冒号。此外需要使用endfor作为结束标志。Jinja2中的if语句也一样,没有复合语句末尾的冒号,需要使用endif作为结束标志。
- 变量
Jinja2模版中使用的{{ }}语法表示一个变量,他是一种特殊的占位符,告诉模块引擎这个位置的值在渲染模版时获取。Jinja2识别所有的Python数据类型,甚至是一些复杂的类型,如列表、字典和对象等。如下:
A value from a dictionary: {{ mydict['key'] }}
A value from a list: {{ mydict[3] }}
A value from a list,with a variable index: {{ mydict[myintvar] }}
A value from a object's method: {{ myobj.somemethod() }}
- Jinja2中的过滤器
变量可以通过“过滤器”进行修改,过滤器可以理解为Jinja2里面的内置函数和字符串处理函数。例如,存在一个名为lower的过滤器,它的作用与字符串对象的lower方法一模一样。下面列表是常见的Jinja2过滤器。
过滤器名 | 说明 |
---|---|
safe | 渲染值时不转义 |
capitalize | 把值的首字母转换为大写,其他字母转换为小写 |
lower | 把值转换为小写 |
upper | 把值转换为大写 |
title | 把值中每个单词首字母都转换为大写 |
trim | 把值的首尾空格去掉 |
striptags | 渲染之前把值中所有的HTML标签都去掉 |
join | 拼接多个值为字符串 |
replace | 替换字符串的值 |
round | 默认对数字进行四舍五入,也可以用参数进行控制 |
int | 把值转换为整型 |
在Jinja2中,变量可以通过"过滤器"修改,过滤器与变量用管道(|)分割。多个过滤器可以链式调用,前一个过滤器的输出作为后一个过滤器的输入,如下所示:
{{ "Helle World" | replace("Hello","Goodbye") }}
{{ "Helle World" | replace("Hello","Goodbye") | upper}}
{{ 42.55 | round }}
{{ 42.55 | round |int }}
- Jinja2的控制结构
Jinja2中的if语句类似于Python中的if语句,但是,需要使用endif语句作为条件判断的结束。我们可以使用if语句判断一个变量是否定义是否为空,是否为真值。与Python中的if语句一样,也可以使用elif和else构建多个分支,如下所示:
{% if kenny.sick %}
kenny is sick.
{% elif kenny.dead %}
You killed Kenny!
{% else %}
Kenny looks okay
{% endif %}
- Jinja2的for循环
Jinja2中的for语句可以用于迭代Python的数据类型,包括列表、元组和字典。在Jinji中不存在while循环,这样符合了Jinja2的提供仅有的控制结构的设计原则。
在Jinja2中迭代列表:
<h1>Members</h1>
<u1>
{% for user in users %}
<li>{{ user.username }}</li>
{% endfor %}
</u1>
在Jinja2中迭代字典:
<d1>
{% for key,value in d.iteritems() %}
<dt>{{ key }</dt>}
<dd>{{ value }}</dd>
{% endfor %}
</d1>
除了基本的for循环使用以外,Jinja2还提供了一些特殊的变量,我们不用定义就可以直接使用这些变量。
变量 | 描述 |
---|---|
loop.index | 当前循环迭代的次数(从1开始) |
loop.index0 | 当前循环迭代的次数(从0开始) |
loop.revindex | 到循环结束的次数(从1开始) |
loop.revindex0 | 到循环结束的次数(从0开始) |
loop.first | 如果是第一次迭代,为True,否则为False |
loop.last | 如果是最后次迭代,为True,否则为False |
loop.length | 序列中的项目数 |
loop.cycle | 在一串序列间取值的辅助函数 |
有关宏和继承函数的内容,请看书籍。貌似一般的维护工作中也用不到的了。
- Jinja2的其他运算
Jinja2可以定义变量,对变量进行操作,Jinja2提供了算数操作、比较操作和逻辑操作。使用Jinja2模板时,应该尽可能在Python代码中进行逻辑处理,在Jinja2中仅处理显示问题。因此,一般很少用到Jinja2的变量和变量的运算操作。常用的运算操作:
- 算数操作 + - * / // % * **
- 比较操作 == != > >= < <=
- 逻辑运算 not and or