第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()等。

  1. 通用操作
    如 len('pcm')、 'c' in 'pcm'

  2. 与大小写相关的方法

  • upper: 转为大写
  • lower: 转为小写
  • isupper: 判断是否为大写
  • islower: 判断是否为小写
  • swapcase: 大写转小写,小写转大写
  • capitalize: 将首字母转换为大写
  • istitle: 判断字符串是不是标题
  1. 判断类方法
    除了前面介绍的几个is开头的方法,还有下面的这些也是很常见的:
  • isalpha: 是否只包含字母
  • isalnum: 是否只包含字符串字母和数字
  • isspace: 是否只包含空格、制表符、换行符
  • isdecimal: 是否自包含数字字符
  1. 字符串方法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)
  1. 查找类函数
  • find:查找子串在字符串中的位置,如果查找失败,返回-1
  • index: 与find函数类似,如果查找失败,抛出ValueError异常
  • rfind: 与find函数类似,区别在于rindex是从后向前查找
  • rindex: 与index函数类似,区别在于rindex是从后向前查找

这几个函数的用法和作用都差不多,这里介绍find的用法

s = 'wo you yi tou xiao mao lv.'
s.find('yi')
  1. 字符串操作方法
    字符串的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 正则表达式语法

正则表达式由普通文本和具有特殊意义的符号组成,工程师根据具体的需要,使用它们构造出合适的正则表达式来匹配文本。例如:

  1. 要匹配给定文本中的所有单词
#"?"用于匹配单词前后可能出现的空格,[a-zA-Z]+代表一个或多个英文字母
?[a-zA-Z]+
  1. 要匹配一个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方法

  1. 匹配类的函数

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函数。

  1. 修改类函数
    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("'"))
  1. 大小写不敏感
    在re模块中,要忽略大小写的差异,就如同前面的示例那样,使用flags=re.IGNORECASE。

  2. 非贪婪模式
    在正则表达式的字符串匹配中,有贪婪匹配和非贪婪匹配的区别。贪婪匹配总是匹配到最长的那个字符串,非贪婪模式正好相反。例如:我们要匹配以"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的语法。

  1. 语法块
    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作为结束标志。

  1. 变量
    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() }}
  1. 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 }}
  1. 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 %}
  1. Jinja2的for循环
    Jinja2中的for语句可以用于迭代Python的数据类型,包括列表、元组和字典。在Jinji中不存在while循环,这样符合了Jinja2的提供仅有的控制结构的设计原则。
    在Jinja2中迭代列表:

Members

{% for user in users %}
  • {{ user.username }}
  • {% endfor %}

    在Jinja2中迭代字典:

    
      {% for key,value in d.iteritems() %}
        
    {{ key }
    }
    {{ value }}
    {% endfor %}

    除了基本的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 在一串序列间取值的辅助函数

    有关宏和继承函数的内容,请看书籍。貌似一般的维护工作中也用不到的了。

    1. Jinja2的其他运算
      Jinja2可以定义变量,对变量进行操作,Jinja2提供了算数操作、比较操作和逻辑操作。使用Jinja2模板时,应该尽可能在Python代码中进行逻辑处理,在Jinja2中仅处理显示问题。因此,一般很少用到Jinja2的变量和变量的运算操作。常用的运算操作:
    • 算数操作 + - * / // % * **
    • 比较操作 == != > >= < <=
    • 逻辑运算 not and or

    你可能感兴趣的:(第4章 文本处理)