python: 文本处理

参考:re - Regular expression operations, Regular-Expressions in python



sys & os 模块

sys和os是python标准库中的两个模块,可以通过

>>> help(sys)
>>> help(os)
>>> dir(sys)

查看模块中的函数、变量等详细信息。

sys.argv[0] 当前脚本或程序名称
sys.argv[i](i>0) 命令行参数
os.name 当前操作系统
os.getcwd() 当前工作目录
os.listdir() 指定目录下的所有文件和目录名
os.remove() 删除文件
os.system() 运行shell命令
os.path.split() 返回路径的目录名和文件名
os.path.isfile() 检验路径是否为文件
os.path.isdir() 检验路径是否为目录
os.path.exists() 检验路径是否存在


[TOP]


文件操作

文件的读、写和关闭

首先来新建一个文件lesson1.txt:

>>> f = open('lesson1.txt', 'w')
>>> f.write('Good morning, class. \n')
>>> f.write('Godd morning, teacher.\n')
>>> f.write('Sit down, please.\n')
>>> f.write('My name is Han Meimei.')
>>> f.close()

这里的'w'表示写模式,常用的还有r读模式,a追加模式,b二进制模式,+读/写模式。

注意:如果不使用换行符\n,上面每次的输入会连接成一行。

读取文件:

>>> f = open('lesson1.txt', 'r')
>>> f.read(13)
'Good morning,'
>>> f.read()
' class.Good morning, teacher.Sit down, please.My name is Han Meimei.'
>>> f.close()

使用writelines()写入多行文本:

>>> f = open('lesson1.txt', 'w')
>>> f.writelines(['Good morning, class.\n','Good morning, teacher.\n','Sit down, please.\n','My name is Han Meimei.'])
>>> f.close()

使用readlines()读取全文:

>>> f = open('lesson1.txt', 'r')
>>> f.readlines()
['Good morning, class.\n', 'Good morning, teacher.\n', 'Sit down, please.\n', 'My name is Han Meimei.']
>>> f.close()

使用readline(),每调用一次只读取一行:

>>> f = open('hi.txt', 'r')
>>> f.readline()
'Good morning, class.\n'
>>> f.readline()
'Good morning, teacher.\n'
>>> f.close()

按行迭代

>>> for line in open(lesson1.txt):
...     process(line)

这种方式不需要再使用.close()关闭文件。标准输入也可以采用类似的方法:

>>> import sys
>>> for line in sys.stdin:
...     process(line)

[TOP]


字符串处理

>>> s = raw_input("Enter a string: ")
Enter a string: Good morning, class.
>>> s
'Good morning, class.'
>>> repr(s) #创建一个字符串,以合法的python表达式的形式来表示值
"'Good morning, class.'"
>>> len(s) #返回字符串长度
20
>>> s.split(',') #将字符串分隔,返回一个列表
['Good morning', ' class.']
>>> 'o' in s #判断s中是否包含'o',返回布尔值
True
>>> s.index('o') #从前向后查找指定字符串,返回第一个匹配项的位置
1
>>> s.find('o') #从前向后查找指定字符串,返回第一个匹配项的位置
1
>>> s.rfind('o') #从后向前查找指定字符串,返回第一个匹配项的位置
6
>>> s.count('o') #检索s中含有几个匹配项,返回匹配项数目
3
>>> s.replace(',', '-') #字符串的替换
'Good morning- class.'
>>> s.replace(',', '') #可以替换为空字符,即删除指定字符串
'Good morning class.'
>>> s.lower() #全部改成小写
'good morning, class.'
>>> s.upper() #全部改成大写
'GOOD MORNING, CLASS.'
>>> pieces = [x.strip() for x in s.split(',')] #忽略分隔后字符串首尾的空白字符
>>> pieces
['Good morning', 'class.']
>>> ','.join(pieces) #使用指定字符串连接列表中的元素
'Good morning,class.'
>>> n = '1024'
>>> int(n) #字符串转为整型
1024
>>> float(n) #字符串转为浮点型
1024.0

这些内置的字符串方法可以完成大部分的常用操作,更复杂的字符串处理需要借助于正则表达式模块re

[TOP]


正则表达式

python的正则表达式模块为re,与shell的正则相比,re更灵活,使用更方便,但语法也更复杂。

首先来学习一个简单的函数:

>>>  re.search('o', s)
<_sre.SRE_Match object at 0x7f45631b03d8>

这个表达式表示从字符串s中查找与’o’匹配的项,如果匹配成功,返回一个MatchObject,否则返回一个NoneType。

为了更直观地看到匹配是否成功,可以这样处理:

>>> if re.search('o', s):
...     print "Found 'o' in s."
Found 'o' in s.
>>> re.search('o', s) != None
True

原生字符串

有时候,字符串转义会给正则匹配带来不小的麻烦,python提供了raw前缀标记“原生字符串”用于避免此类问题:

>>> re.search(r'o', s) != None #注意r
True

python2默认未采用unicode,因此最好使用u标记,即ur'o',python3默认采用了unicode,只需使用r标记。

注意:python可能会提示脚本中出现的utf-8字符无法识别,此时应在.py文件首部加上:

# encoding = utf-8

[TOP]


基本规则

元字符

[ ]: 字符集,匹配括号内出现的任意单个字符

[ABCD]匹配A、B、C、D任一字母;[A-Z]匹配任意一个大写字母;[^A-D]匹配一个不是大写字母的字符(^[ ]内时表示取反)。

>>> re.search(r'^1[358][0-9]{9}', '13901390326') != None
True

这条命令可以匹配手机号码。

|: 或,用于连接两个表达式

>>> re.search(r'Han Meimei|Li Lei', 'My name is Han Meimei.') != None
True
>>> re.search(r'Han Meimei|Li lei', 'My name is Li Lei.') != None
True
>>> re.findall(r'My name is Han Meimei|Li Lei', 'My name is Han Meimei. My name is Li Lei')
['My name is Han Meimei', 'Li Lei']

函数findall()返回一个列表,包含所有匹配项。

这个例子表明|的有效范围是其两边的整个表达式,要避免这种情况,需要使用”无捕获组”,即:

>>> re.findall(r'My name is (?:Han Meimei|Li Lei)', 'My name is Han Meimei. My name is Li Lei')
['My name is Han Meimei', 'My name is Li Lei']

注意:[ ]中的|不表示或,而表示字符|

.(一个点): 匹配除换行符之外的任意单个字符

>>> re.findall(r'.+', 'hello\nworld')
['hello', 'world']

使用re.S(DOTALL)可以使.匹配包括换行在内的所有单个字符:

>>> re.findall(r'.+', 'hello\nworld', re.S)
['hello\nworld']

^$: 匹配行首和行尾字符串

python中与这两个元字符类似的还有转义字符\A\Z,不同之处在于,\A\Z并不会识别换行\n,也就是匹配整个字符串的开头和结尾,而^$则可以识别换行,可以匹配每一行的开头和结尾。

>>> re.findall(r'^wo', 'hello\nworld', re.M)
['wo']
>>> re.findall(r'\Awo', 'hello\nworld', re.M)
[ ]

类似\A\Z的预定义转义字符还有匹配数字的\d,补集为\D;匹配字母和数字的\w,补集为\W;匹配间隔符的\s,补集为\S\s等价于[ \t\r\n\f\v](注意最前面有个空格);匹配单词边界的\b等。

\b: 匹配单词边界

匹配单词的边界,可以看作是单词和间隔符、标点之间的位置,类似地,\B匹配非单词边界。

>>> re.findall(r'ei', 'My name is Han Meimei.')
['ei', 'ei']
>>> re.findall(r'ei\b', 'My name is Han Meimei.')
['ei']
>>> re.findall(r'ei\B', 'My name is Han Meimei.')
['ei']

(?:): “无捕获组”

对比这三个表达式:

>>> re.findall(r'My name is Han Meimei|Li Lei', 'My name is Han Meimei. My name is Li Lei')
['My name is Han Meimei', 'Li Lei']
>>> re.findall(r'My name is (Han Meimei|Li Lei)', 'My name is Han Meimei. My name is Li Lei')
['Han Meimei', 'Li Lei']
>>> re.findall(r'My name is (?:Han Meimei|Li Lei)', 'My name is Han Meimei. My name is Li Lei')
['My name is Han Meimei', 'My name is Li Lei']

可以发现如果要把一部分表达式作为一个整体操作,需要使用(?:re)将其括起来,而不能只使用( ),只使用( )会构造出一个“组”,为了避免构造成组,需要使用“无捕获组”表达式(?:re)。关于“组”的用法,请看这里

(?#): 注释

(?#comment)中的’comment’在程序解析表达式时将被忽略。

重复匹配次数的限定

*表示匹配0次、1次或多次,+表示匹配1次或多次,?表示匹配0次或1次。

精确匹配

{n,m}匹配至少n次,最多m次,{n}精确匹配n次。

最小匹配

*+?默认尽可能多的匹配字符,如提取c程序文件中的注释:

>>> s = r'/* comments1 */ code /* comments2 */'
>>> re.findall(r'/\*.*\*/', s)
['/* comments1 */ code /* comments2 */']

可以看到,该表达式把“code”也提取出来了,并不符合我们的本意,这时需要在*后加上?,表示“最小匹配”:

>>> re.findall(r'/\*.*?\*/', s)
['/* comments1 */', '/* comments2 */']

前向界定和后向界定

前向界定(?<=...)中的...用于指定匹配项前的字符,如:

>>> s = r'Han Meimei'
>>> re.findall(r'(?<=M)eim', s)
['eim']

后向界定(?=...)中的...用于指定匹配项后的字符,如:

>>> re.findall(r'Mei(?=m)', s)
['Mei']

上文提取c程序注释的表达式可以改写为:

>>> s = r'/* comments1 */ code /* comments2 */'
>>> re.findall(r'(?<=/\* ).*?(?= \*/)', s)
['comments1', 'comments2']

注意:(?<=...)(?=...)中的...不能包含正则式,必须是明确的字符串。

类似的,有前向非界定(?<!...)和后向非界定(?!...),用于指定匹配项之前或之后不希望出现的字符。复杂的界定表达式可读性会变差,这时更适合使用“组”来处理。

>>> re.findall(r'My name is (Han Meimei|Li Lei)', 'My name is Han Meimei. My name is Li Lei')
['Han Meimei', 'Li Lei']

这个例子的findall()只返回了( )中匹配的内容,这就是“组”的基本功能。

组还可以命名(?P<name>…)和引用(?P=name),如:

>>> s = r'Han Meimei'
>>> re.findall(r'(?P<p1>ei)m(?P=p1)', s) #提取中间为m的ei
['ei']

(?P<name>…)中的...可以是明确的字符,也可以是复杂的正则表达式。

还可以通过序号引用组表达式:

>>> re.findall(r'(ei)m\1', s)
['ei']
>>> re.findall(r'(ei)m(\1)', s)
[('ei', 'ei')]

[TOP]


函数

re.match()re.search()

这两个函数的用法是类似的,不同之处在于re.match()从字符串开头开始匹配,开头匹配失败则返回NoneType,而re.search()则会检索整个字符串。

re.match()re.search()包含三个参数,第一个是正则式,第二个是目标字符串,第三个是选项(修饰符),常用的修饰符有:

re.I(IGNORECASE) i 忽略大小写
re.X(VERBOSE) x 详细模式:忽略空白字符和注释 (注释以#开始,直到行尾)
re.M(MULTILINE) m 多行匹配,影响^$
re.S(DOTALL) s 使.匹配包括换行在内的所有字符
re.U(UNICODE) u unicode模式


修饰符的使用方法:

>>> s = r'Han Meimei'
>>> re.search('m', s) != None
>>> re.search('(?i)m', s) != None
>>> re.search('m', s, re.IGNORECASE) != None

re.findall()re.finditer()

这两个函数也包含正则式、目标字符串和修饰符三个参数。

re.findall()返回所有匹配成功的字符串,打印为一个列表,re.finditer()则以迭代的方式返回匹配成功的字符串:

>>> s = r'Han Meimei'
>>> re.findall(r'\w', s)
['H', 'a', 'n', 'M', 'e', 'i', 'm', 'e', 'i']
>>> for i in re.finditer(r'\w', s):
...     print i.group(), i.span()
H (0, 1)
... ...
i (9, 10)

re.sub()re.subn(): 替换

这两个函数用于将匹配的字符串替换为新的指定字符串,均包括正则式、新字符串、目标字符串、最多替换次数等参数。

re.sub()返回处理后的字符串,re.subn()返回一个元组,其中第一个元素是处理后的字符串,第二个元素是替换次数。

>>> re.subn(r'ei', r'a', s, 3)
('Han Mama', 2)

re.split(): “切片”

使用指定的正则式分隔目标字符串,包括正则式、目标字符串、切片次数三个参数,返回一个列表,如:

>>> s=r'Hello    ,    World.'
>>> re.split('\s*,\s*', s)
['Hello', 'World.']

re.escape(): 转义预处理

可用于字符串的预处理,为目标字符串中的特殊字符加上转义符:

>>> s=r'Hello , \tWorld.'
>>> re.escape(s)
'Hello\\ \\,\\ \\\\tWorld\\.'

re.compile(): 编译为pattern对象

该函数包含正则式和修饰符两个参数,返回pattern对象,如:

>>> s1 = r'Good morning, class.'
>>> s2 = r'Good morning, teacher.'
>>> rc = re.compile(r'morning', re.I)
>>> rc.findall(s1)
['morning']
>>> rc.findall(s2)
['morning']

注意:这里的findall()不只是函数了,而是pattern对象的方法

[TOP]


对象

pattern对象

pattern对象有很多方法,其中findall(),finditer(),match(),search()与同名函数的功能完全一样,不过其包含的参数有所不同,依次是目标字符串、指定起始位置、指定结束位置。如:

>>> s = r'Han Meimei'
>>> rc = re.compile(r'ei', re.I)
>>> rc.findall(s, 5, 10)
['ei', 'ei']
>>> rc.findall(s, 6, 10)
['ei']

pattern对象还有一些属性,如flags返回一个整数,代指编译时的修饰符,pattern返回编译时的正则式,groupindex返回一个字典,包含正则式中的组及其序号(不包含无命名组)。

>>> rc.flags
2

match对象

match对象用于提取正则匹配结果MatchObject中的信息。

>>> s = r'Han Meimei'
>>> rc = re.compile(r'(?P<p1>mei)', re.I)
>>> m = rc.search(s, 3, 10)
>>> m.string #目标字符串
'Han Meimei'
>>> m.re.pattern #re生成pattern对象
'(?P<p1>mei)'
>>> m.group()
'Mei'
>>> m.group(1)
'Mei'
>>> m.group('p1')
'Mei'

此外,groups()返回全部组,groupdict()返回一个以组名为key、匹配结果为values的字典,start(),end(),span()返回匹配成功的组的位置信息,pos,endpos返回正则方法输入的位置信息,lastindex,lastgroup返回最后匹配的组序号和组名。

最后,再介绍一个非常有用的expand(),用于生成并返回一个新的字符串,如:

>>> s1 = r'HanMeimei 15 girl'
>>> s2 = r'LiLei 16 boy'
>>> rc = re.compile(r'(?P<name>[a-z]+)\s+(?P<age>[0-9]+)\s+(?P<gender>[a-z]+)', re.I)
>>> m1 = rc.search(s1)
>>> m2 = rc.search(s2)
>>> m1.expand(r'My name is \g<1>. I am a \g<gender> and I am \g<age>.')
'My name is HanMeimei. I am a girl and I am 15.'
>>> m2.expand(r'My name is \g<1>. I am a \g<gender> and I am \g<age>.')
'My name is LiLei. I am a boy and I am 16.'

[TOP]