Free Will

python编程系列(1):正则表达式

对文本进行处理在数据科学实践中必不可少的一环,业界的文本数据往往杂乱无章,而且数量及其庞大,当我们需要对文本进行片段匹配时,就要求我们利用计算机来批量地在文本中检索某种模式。正则表达式(Regular Expression)就是可以进行文本匹配的一种高级模式,它是一些由字符和特殊符号组成的字符串,它可以按照某种模式匹配一系列有相似特征的字符串。

最简单的正则表达式就是普通字符串,它仅仅可以匹配其自身。比如正则表达式“python”只可以匹配字符串“python”。正则表达式的强大之处在于特殊符号的应用,特殊符号定义了字符集合、子组匹配以及模式的重复次数。正是这些特殊符号使得一个正则表达式可以匹配字符串集合而不只是一个字符串。

一、元字符

下图列出了Python支持的正则表达式元字符和语法:

1.1 择一匹配符号“|”

表示从多个模式中选择一个,用于分割不同的正则表达式,可以匹配不止一个字符串,等同于逻辑“或”。例如

1
bat | bet | bit # 可以匹配字符串bat或者bet或者bit

1.2 任意字符匹配符号“.”

“.”号可以匹配除了换行符以为的任何字符(Python正则表达式有一个编译标记[S或DOTALL]能够使“.”匹配换行符),要匹配“.”号自身,必须使用反斜线转译符号“.”。例如:

1
2
f.o #能够匹配f和o之间加上任意一个字符的样式,如:fao、f9o、f#o
.. #能够匹配任意两个字符

1.3 匹配字符串开始“^”或结尾“$”

匹配字符串以什么开始的,可以使用脱字符“^”或\A;
匹配字符串以什么结束的,可以使用美元符“$”或\Z;
例如:

1
2
3
^From #任何以From开始的字符串
tcsh$ #任何以tcsh结尾的字符串
^subject:hi$ #任何由单独的字符串subject:hi构成的字符串

1.4 匹配单词边界:“\b”、“\B”

\b:匹配单词的边界(单词前或后),而不在乎单词中间的字符
\B:匹配单词中间的字符,而不在乎单词边界的字符
例如:

1
2
3
er\b #可以匹配“never”中的“er”,但不能匹配“verb”中的“er”,只关心后边
er\B #能匹配“verb”中的“er”,但不能匹配“never”中的“er”,只关心中间
\bthe #匹配任何以the开头的字符串

1.5 字符集“[ ]”

当想要匹配指定的某些字符的时候,使用字符集是很方便的。
注意:字符集只适用于单字符的情况。也就是说[ab]表示只从ab中选择一个

1
2
b[ae]t #匹配bat、或bet
[01][ab] #匹配0a、0b、1a、1b

1.6 字符集中的范围“-”和否定“

-:表示一个字符的范围
^:不匹配指定字符集里的任意字符

1
2
3
4
z.[0-9] #字母z后面跟着任何一个字符,然后跟着一个数字
[^aeiou] #一个非元音字符
[^\t\n] #不匹配制表符或\n
["-a] #在一个ASCII系统中,位于“"”和“a”之间的字符,即34-97之间的字符

1.7 特殊符号(*,+,?,{})

*:匹配其左边的正则表达式出现零次或多次的情况。
+:匹配一次或多次出现的正则表达式。
?:匹配零次或一次出现的正则表达式。
{N}、{M,N}: 匹配前面的正则表达式N次或M~N次

1
2
3
4
[dn]ot? #字母d或n后面跟一个o,然后后面最多再跟一个t。例如:do、no、dot、not
0?[1-9] #一个1到9的数字,前面跟或不跟一个0
[0-9]{15,16} #匹配15或16个数字。例如信用卡号码
</?[^>]+> #匹配全部有效的(和无效的)HTML标签

1.8 特殊字符

\d:十进制数字,相当于[0-9]
\D:非十进制数字的字符,相当于0-9
\w:全部字母数字,相当于[A-Za-z0-9]
\W:非字母数字的字符,相当于A-Za-z0-9
\s: 空格字符
\S: 非空格字符

1
2
3
4
\w+-\d+ #一个由字母数字组成的字符串和一串由连字符分隔的数字
[A-Za-z]\w* #第一个是字母,其余是字母或数字
\d{3}-\d{3}-\d{4} #美国电话号码格式,例如800-555-1212
\w+@\w+\.com #以xxx@yyy.com格式表示的简单电子邮件地址

1.9 圆括号指定分组

有时候除了进行匹配操作外,我们还想要提取所匹配的子组,例如:\w+-\d+,这个正则表达式想要分别保存第一部分的字母和第二部分的数字,该怎么实现?我们可能这样做的原因是对于任何成功的匹配,我们想要看到匹配的字符串究竟是什么。如果为两个子模块都加上圆括号,例如(\w+)-(\d),然后就能够分别访问每一个匹配的子组。

1
\d+(\.\d*)? #匹配浮点数的字符串,如:“5”、“5.”、“5.009”等

1.10 扩展表示法

可以参考上面表格的讲解结合下面的例子就能懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
Windows(?=95|98|NT|2000) #能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”
Windows(?!95|98|NT|2000) #能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”
(?<=95|98|NT|2000)Windows #能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”
(?<!95|98|NT|2000)Windows #能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”
industr(?:y|ies) #就是一个比“industry|industries”更简略的表达式
(?:\w+\.)* #以点结尾的字符串,如google.
(?#comment) #不做匹配,只做注释
(?=.com) #一个字符串后面跟着.com才做匹配
(?!.net) #一个字符串后面跟的不是.net才做匹配
(?<=800-) #字符串前面出现800-才做匹配
(?<!192\.168\.) # 字符串前面不是192.168。才做匹配,过滤掉一类ip地址
(?(1)y|x) #如果匹配组1(\1)存在,就与y匹配,否则就与x匹配

二、re模块

Python语言中使用re模块的方法支持正则表达式。这里列出re模块常见的函数以方便查询(后面会介绍主要的函数使用方法)

下面将分开解释上面的部分函数:

2.1 使用match()和search()匹配字符串,使用group()查看结果

re.match() :从字符串开始的位置匹配,成功返回匹配的对象,失败返回None
re.search(): 扫描整个字符串来进行匹配,成功返回匹配的对象,失败返回None

例1:比较match() 和 search()的区别

1
2
3
4
5
6
7
8
9
import re
m = re.match('foo', 'seafood')
if m is not None: print("match-" + m.group())
m = re.search('foo', 'seafood')
if m is not None: print("search-" + m.group())
#结果是:search-foo

例2: match()函数从起始位开始匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re
m = re.match('foo', 'foo')
if m is not None:
print("能匹配-" + m.group())
m = re.match('foo', 'bar')
if m is not None: print("不能匹配-" + m.group())
m = re.match('foo', 'food on the table')
if m is not None: print("从开始位置进行匹配-" + m.group())
#能匹配-foo
#从开始位置进行匹配-foo

例3: 匹配多个值(使用择一表达式”|”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import re
bt = 'bat|bet|bit'
m = re.match(bt, 'bat')
if m is not None:
print("1能匹配-" + m.group())
m = re.match(bt, 'blt')
if m is not None:
print("2能匹配-" + m.group())
m = re.match(bt, 'he bit me')
if m is not None:
print("3能匹配-" + m.group())
m = re.search(bt, 'he bit me')
if m is not None:
print("4能匹配-" + m.group())
#结果:
# 1能匹配-bat
# 4能匹配-bit

例4: 匹配任何单个字符。点号”.”除了换行符\n和非字符,都能匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import re
bt = ".end"
m = re.match(bt, 'bend')
if m is not None:
print("bend能匹配-" + m.group())
m = re.match(bt, 'end')
if m is not None:
print("end能匹配-" + m.group())
m = re.match(bt, '\nend')
if m is not None:
print("\nend能匹配-" + m.group())
m = re.search(bt, 'the end.')
if m is not None:
print("the end.能匹配-" + m.group())
#结果:
# bend能匹配-bend
# the end.能匹配- end

例5: 匹配小数点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re
bt = "3.14"
pi_bt = "3\.14" #表示字面量的点号 (dec.point)
m = re.match(bt, '3.14') #点号匹配
if m is not None:
print("3.14能匹配-" + m.group())
m = re.match(pi_bt, '3.14') #精确匹配
if m is not None:
print("精确匹配-" + m.group())
m = re.match(bt, '3014') #点号匹配0
if m is not None:
print("3014能匹配-" + m.group())
#结果:
# 3.14能匹配-3.14
# 精确匹配-3.14
# 3014能匹配-3014

例6: 使用字符集”[ ]”

1
2
3
4
5
6
7
8
9
10
11
import re
bt = "[cr][23][dp][o2]"
m = re.match(bt, 'c3po') #点号匹配
if m is not None:
print("c3po能匹配-" + m.group())
#结果:
# c3po能匹配-c3po

例7: 重复、特殊字符

正则表达式: \w+@\w+.com可以匹配类似nobody@xxx.com的邮箱地址,但是类似nobody@xxx.yyy.aaa.com的地址就不能匹配了。这时候我们可以使用 操作符来表示该模式出现零次或者多次:\w+@(\w+.)\w+.com

例8: 分组

group()可以访问每个独立的子组
groups()获取一个包含所有匹配子组的元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import re
>>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
>>> m.group()
'abc-123'
>>> m.group(1)
'abc'
>>> m.group(2)
'123'
>>> m.groups()
('abc', '123')
>>> m = re.match('ab', 'ab')
>>> m.group()
'ab'
>>> m.groups()
( )

例9: 匹配字符串起始和结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m = re.search('^the','the end.')
>>> m.group()
'the'
>>> m = re.search('^the','sthe end.')
>>> m.group()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
>>> m = re.search(r'\bthe','bite the dog')
>>> m.group()
'the'
>>> m = re.search(r'\bthe','bitethe dog')
>>> m.group()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
>>> m = re.search(r'\Bthe','bitethe dog')
>>> m.group()
'the'

2.2 使用findall()、finditer()查找每一次出现的位置

final() 以列表的形式返回所有能匹配的结果

1
2
3
>>> import re
>>> re.findall('car', 'car sscare')
['car', 'car']

finaliter()返回一个顺序访问每一个匹配结果(Match对象)的迭代器

1
2
3
4
>>> re.finditer(r'(th\w+) and (th\w+)',s, re.I).next().group(1)
'This'
>>> re.finditer(r'(th\w+) and (th\w+)',s, re.I).next().group(2)
'That'

2.3 使用sub()和subn()搜索和替换

两个函数都可以实现搜索和替换功能,将某字符串中所有匹配正则表达式的部分进行某种形式的替换。不同点是subn()还返回一个表示替换了多少次的总数,和返回结果一起以元组的形式返回。

1
2
3
4
>>> re.sub('[ae]','X','abcdef')
'XbcdXf'
>>> re.subn('[ae]','X','abcdef')
('XbcdXf', 2)

进行替换的时候,还可以指定替换的顺序,原理是使用匹配对象的group()方法除了能够获取匹配分组编号外,还可以使用\N,其中N表示要替换字符串中的分组的编号,通过编号就能指定替换的顺序。
例如:将美式日期MM/DD/YY{,YY}格式转换成DD/MM/YY{,YY}格式

1
2
3
4
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})',r'\2/\1/\3','2/20/91')
'20/2/91'
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})',r'\2/\1/\3','2/20/1991')
'20/2/1991'

2.4 在限定模式上使用split()分隔字符串

re模块的split()可以基于正则表达式的模式分隔字符串。但是当处理的不是特殊符号匹配多重模式的正则表达式时,re.split()和str.split()的工作方式相同,如下所示:

1
2
3
4
>>> re.split(':', 'str1:str2')
['str1', 'str2']
>>> 'str1:str2'.split(':')
['str1', 'str2']

但当处理复杂的分隔时,就需要比普通字符串分隔更强大的处理方式,例如下面匹配复杂情况:

1
2
3
4
5
6
7
8
>>> DATA = ('Mountation View, CA 94040', 'sunnyvale, CA', 'Los Altos, 94023', 'Palo Alto CA','Cupertino 95014')
>>> for datum in DATA: print(re.split(', |(?= (?:\d{5}|[A-Z]{2})) ',datum))
...
['Mountation View', 'CA', '94040']
['sunnyvale', 'CA']
['Los Altos', '94023']
['Palo Alto', 'CA']
['Cupertino', '95014']

上述的正则表达式:当一个空格紧跟在5个数字或2个字母后面时就用split语句分隔。当遇到“,”也用split函数分隔。

2.5 扩展符号

通过使用(?iLmsux)系列选项,可以直接在正则表达式里面指定一个活着多个标记。以下是使用re.I/IGNORECASE的示例,第二个是使用re.M/MULTILINE实现多行混合。

1
2
3
4
5
6
7
8
9
10
>>> re.findall(r'(?i)yes','yes? Yes. YES!!!')
['yes', 'Yes', 'YES']
>>> re.findall(r'(?i)th\w+','The quickest way is through this tunnel.')
['The', 'through', 'this']
>>> re.findall(r'(?im)(^th[\w ]+)', """
... This is the first,
... another line,
... that line,it's the best
... """)
['This is the first', 'that line']

通过使用“多行”,能够在目标字符串中实现跨行搜索,而不必将整个字符串视为单个实体。

下一个例子用来演示re.S/DOTALL,该标记表示点号(.)能够用来表示\n符号。

1
2
3
4
5
6
7
8
9
10
11
12
>>> re.findall(r'th.+',"""
... The first line
... the second line
... the third line
... """)
['the second line', 'the third line']
>>> re.findall(r'(?s)th.+',"""
... The first line
... the second line
... the third line
... """)
['the second line\nthe third line\n']

re.X/VERBOSE标记允许用户通过抑制在正则表达式中使用空白符来创建更易读的正则表达式。

1
2
3
4
5
6
7
8
>>> re.search(r'''(?x)
... \((\d{3})\) #区号
... [ ] #空白符
... (\d{3}) #前缀
... - #横线
... (\d{4}) #终点数字
... ''','(800) 555-1212').groups()
('800', '555', '1212')

(?:…)符号可以对部分正则表达式进行分组,但是不会保存该分组用于后续的检索或应用。

1
2
3
4
5
6
>>> re.findall(r'http://(?:\w+\.)*(\w+\.com)',
... 'http://google.com http://www.google.com http://code.google.com')
['google.com', 'google.com', 'google.com']
>>> re.search(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',
... '(800) 555-1212').groupdict()
{'areacode': '800', 'prefix': '555'}

可以同时使用(?P)和(?P=name)符号。前者通过使用一个名称标识符而不是使用从1开始增加到N的增量数字来保存匹配,如果使用数字来保存匹配结果,我们就可以通过使用\1、\2、…,\N来索引,如下所示,可以使用一个类似风格的\g来检索它们。

1
2
3
>>> re.sub(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',
... '(\g<areacode>) \g<prefix>-xxxx', '(800) 555-1212')
'(800) 555-xxxx'

使用后者,可以在同一个正则表达式中重用模式。例如,验证一些电话号码的规范化。

1
2
bool(re.match(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?P<number>\d{4}) (?P=areacode)-(?P=prefix)-(?P=number) 1(?P=areacode)(?P=prefix)(?P=number)', '(800) 555-1212 800-555-1212 18005551212'))
True

使用(?x)使代码更易读:

1
2
3
4
5
6
7
8
>>> bool(re.match(r'''(?x)
... \((?P<areacode>\d{3})\)[ ](?P<prefix>\d{3})-(?P<number>\d{4})
... [ ]
... (?P=areacode)-(?P=prefix)-(?P=number)
... [ ]
... 1(?P=areacode)(?P=prefix)(?P=number)
... ''','(800) 555-1212 800-555-1212 18005551212'))
True

可以使用(?=…)和(?!…)符号在目标字符串中实现一个前视匹配:

(?=…)字符串后面跟着…才适配

1
2
3
4
5
6
7
8
9
>>> re.findall(r'\w+(?= van Rossum)',
... '''
... Guido van Rossum
... Tim Peters
... Alex Martelli
... Just van Rossum
... Raymond Hettinger
... ''')
['Guido', 'Just']

(?!…)字符串后面不跟着…才适配:

1
2
3
4
5
6
7
8
9
>>> re.findall(r'(?m)^\s+(?!noreply|postmaster)(\w+)',
... '''
... sales@phptr.com
... postmaster@phptr.com
... eng@phptr.com
... noreply@phptr.com
... admin@phptr.com
... ''')
['sales', 'eng', 'admin']

比较re.findall()和re.finditer()

1
2
3
4
5
6
7
8
9
>>> ['%s@awcom' % e.group(1) for e in re.finditer(r'(?m)^\s+(?!noreply|postmaster)(\w+)',
... '''
... postmaster@phptr.com
... noreply@phptr.com
... admin@phptr.com
... eng@phptr.com
... sales@phptr.com
... ''')]
['admin@awcom', 'eng@awcom', 'sales@awcom']

条件正则表达式匹配,假定拥有一个特殊字符,它仅仅包含字母x和y,两个字母必须由一个跟着另外一个,不能同时拥有相同的两个字母:

1
2
3
4
>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xy'))
True
>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xx'))
False

三、实例

在UNIX系统中,who命令会展示登录的用户信息。例如:

1
2
3
4
5
➜ ~ who
sl console Nov 21 08:59
sl ttys000 Nov 21 09:09
sl ttys001 Nov 21 10:30
➜ ~

如果想按照空格(多个,数量不确定)分隔的话,可以使用\s\s+,下面创建一个程序,将保存在文件whodata.txt中的数据读出来:
先将who的数据保存在whodata.txt文件中:

1
➜ ~ who > /Users/sl/Desktop/whodata.txt

然后执行下面的程序:

1
2
3
4
5
6
import re
f = open('whodata.txt','r')
for eachLine in f:
print(re.split(r'\s\s+', eachLine))
f.close()

执行结果:

1
2
3
['sl', 'console', 'Nov 21 08:59', '']
['sl', 'ttys000', 'Nov 21 09:09', '']
['sl', 'ttys001', 'Nov 21 10:30', '']

优化上面的程序:

上面的程序,who命令是在脚本外部执行的,每次手动重复做这件事让人很厌倦,我们可以通过调用os.popen()命令(现在已经被subprocess模块替代)将这个命令的执行在脚本内部实现。另外我们使用str.rstrip()去除尾部的\n,程序如下:

1
2
3
4
5
6
7
8
9
10
11
import re
import os
f = os.popen('who', 'r')
for eachLine in f:
print(re.split(r'\s\s+|\t', eachLine.rstrip()))
f.close()
#结果:
['sl', 'console', 'Nov 21 08:59']
['sl', 'ttys000', 'Nov 21 09:09']
['sl', 'ttys001', 'Nov 21 10:30']

还可以使用with语句,可以使上下文管理对象变得更简易:

1
2
3
4
5
6
import re
import os
with os.popen('who', 'r') as f:
for eachLine in f:
print(re.split(r'\s\s+|\t', eachLine.rstrip()))

如果要适配python2和python3的话,可以避免使用print(),而使用两个版本中都有的函数distutils.log.warn(),并将其转换成printf名来使用。

1
2
3
4
5
6
import re
import os
from distutils.log import warn as printf
with os.popen('who', 'r') as f:
for eachLine in f:
printf(re.split(r'\s\s+|\t', eachLine.rstrip()))

生成随机数的例子,用于希望练习从中匹配、搜索正则表达式使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from random import randrange, choice
from string import ascii_lowercase as lc
from sys import maxsize
from time import ctime
tlds = ('com', 'edo', 'net', 'org', 'gov')
for i in range(randrange(5, 11)):
dtint = randrange(maxsize) / 3000000000
dtstr = ctime(dtint)
llen = randrange(4, 8)
login = ''.join(choice(lc) for j in range(llen))
dlen = randrange(llen, 13)
dom = ''.join(choice(lc) for j in range(dlen))
print('%s::%s@%s.%s::%d-%d-%d' % (dtstr, login, dom, choice(tlds), dtint, llen, dlen))
#随机产生的结果
Mon Jun 24 08:07:21 2024::nunzkre@iqdhccpw.gov::1719187641-7-8
Sun May 21 12:23:33 2062::dxlyq@kupbixskweqj.edo::2915411013-5-12
Thu Sep 2 18:27:12 1999::vuhihly@hdgaimdma.com::936268032-7-9
Mon Jul 30 16:45:03 2007::vygxw@diwdeqkq.net::1185785103-5-8
Thu Jul 8 01:50:54 1971::mjxs@dmcuo.com::47757054-4-5
Thu Sep 1 03:02:30 2005::djld@eohculuz.gov::1125514950-4-8
Sun Nov 27 01:23:35 2011::cjvf@atvmdgxupi.gov::1322328215-4-10
Thu Aug 1 18:36:36 2024::phasko@flcfkvb.org::1722508596-6-7


应统联盟


连接十万名应统专业同学


阿药算法


打通算法面试任督二脉