正则表达式字符串:编程中不可或缺的字符串匹配利器

编程时涉及最多的一种数据结构是字符串,对字符串进行操作的需求几乎无处不在。例如要判断一个字符串是否为合法的 Email 地址,即便可以编程提取@前后的子串,然后分别判断是否为单词和域名,然而这样做既麻烦,代码又难以复用。
正则表达式是一种强有力的工具,可用于匹配字符串。其设计思想是通过一种描述性语言为字符串定义规则,若字符串符合该规则,我们就认为它“匹配”了;若不符合,该字符串就是不合法的。
所以我们判断一个字符串是否是合法的Email的方法是:
创建一个匹配Email的正则表达式;
用该正则表达式去匹配用户的输入来判断是否合法。
正则表达式是用字符串表示的,所以我们首先要了解怎样用字符去描述字符。
在正则表达式里,要是直接给出字符,那就意味着是精确匹配。用 \d 能够匹配一个数字,用 \w 能够匹配一个字母或者一个数字,因此:
.可以匹配任意字符,所以:
在正则表达式中,要匹配变长的字符,用*来表示任意个字符(包含 0 个);用+来表示至少一个字符;用?来表示 0 个或 1 个字符;用{n}来表示 n 个字符;用{n,m}来表示 n 到 m 个字符。
来看一个复杂的例子:\d{3}\s+\d{3,8}。
我们来从左到右解读一下:
“\(d{3}\)的意思是能够匹配到 3 个数字,就像‘010’那样;”
\(s\)能够匹配一个空格,这里的空格包括 Tab 等空白符。因此,\(s +\)表示至少有一个空格,比如可以匹配“ ”,也可以匹配“ ”等。
3 到 8 个数字可以用 \d{3,8} 来表示,像“1234567”这样的就是 3 到 8 个数字的例子。
综合来看,上述正则表达式能够匹配这样的电话号码:电话号码带有区号,并且这些区号和电话号码之间用任意个空格隔开。
如果要匹配“010-12345”这样的号码呢?因为“-”属于特殊字符,在正则表达式里,需要用“\”进行转义,所以,上述的正则是\d{3}\-\d{3,8}。
但是,无法匹配“010 - 12345”,原因是其中带有空格。因此,我们需要更复杂的匹配方式。
进阶
要做更精确地匹配,可以用[]表示范围,比如:
A|B 能够匹配 A 或者 B,因此[P|p]ython 能够匹配 'Python' 或者 'python'。
^表示行的开头,^\d表示必须以数字开头。
$表示行的结束,\d$表示必须以数字结束。
你可能留意到了,py 能够匹配到“python”。然而,当加上“^py$”之后,就变成了整行匹配的情况,此时就只能匹配到“py”了。
re模块
我们有了准备知识后,就能够在 Python 里使用正则表达式了。Python 为我们提供了 re 模块,这个模块包含了所有正则表达式的功能。因为 Python 的字符串本身也是用\进行转义的,所以我们必须要特别留意这一点:
s 等于 "ABC\\-001" ,这是 Python 的字符串
# 对应的正则表达式字符串变成:
# 'ABC\-001'
因此我们强烈建议使用 Python 的 r 前缀,这样就无需考虑转义的问题了。
s 等于 'ABC\-001' ,这是 Python 的字符串
# 对应的正则表达式字符串不变:
# 'ABC\-001'
先看看如何判断正则表达式是否匹配:
>>> import re
re.match 匹配的是一个字符串,该字符串的开头是三位数字,后面接着一个短横线,接着是三位到八位的数字。例如,"010-12345" 这个字符串就符合这个模式。
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
re.match 一个正则表达式 r'^\d{3}\-\d{3,8}$',用于匹配一个字符串,这个字符串以三位数字开头,接着是一个短横线,然后是三到八位数字。现在要匹配的字符串是 '010 12345' 。
>>>
match()方法用于判断是否匹配。若匹配成功,就会返回一个 Match 对象;若匹配失败,就会返回 None。常见的判断方式如下:
test = '用户输入的字符串'
如果测试内容与正则表达式相匹配,那么就满足条件。如果测试内容与正则表达式不匹配,那么就不满足条件。
print('ok')
else:
print('failed')
切分字符串
正则表达式切分字符串比固定字符切分更具灵活性。请看以下正常的切分代码:
将字符串 'a b c' 按照空格 ' ' 进行分割。
嗯,无法识别连续的空格,用正则表达式试试:
使用空格进行分割,对字符串 "a b c" 进行分割操作,分割后的结果为 ["a", "b", "c"] 。
包含 'a'、'b' 和 'c'。
无论多少个空格都可以正常分割。加入,试试:
最终得到的分割结果为 ['a', 'b', 'c', 'd']
包含 'a'、'b'、'c'、'd' 。
再加入;试试:
使用正则表达式 re.split 来分割字符串 "a,b;; c d",按照空格、逗号或分号进行分割。具体来说,就是将字符串按照这些分隔符拆分成多个子字符串,然后返回一个包含这些子字符串的列表。例如,["a", "b", "c", "d"] 就是分割后的结果。
['a', 'b', 'c', 'd']
如果用户输入了一组标签,那么下次要记得采用正则表达式,将不规范的输入转化为正确的数组。
分组
正则表达式除了能简单地判断是否匹配之外,还有着强大的提取子串的功能。其中用()所表示的就是需要提取的分组(Group)。例如:
^(\d{3})-(\d{3,8})$这两个部分分别定义了组,能够直接从与之匹配的字符串里提取出区号以及本地号码:
m 对字符串 '010-12345' 进行匹配操作,匹配的模式是 '^(\d{3})-(\d{3,8})$' ,即匹配以三位数字开头,接着一个短横线,然后是三位到八位数字的字符串。
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'
如果正则表达式定义了组,那么就能够在 Match 对象上运用 group()方法把子串提取出来。
注意到 group(0)始终为原始字符串,而 group(1)、group(2)等分别表示第 1 个、第 2 个等子串。
提取子串非常有用。来看一个更凶残的例子:
>>> t = '19:05:30'
m 进行匹配操作,匹配的模式是 r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$',匹配的对象是 t
>>> m.groups()
这个正则表达式能够直接对合法的时间进行识别。然而,在某些情况下,即便使用正则表达式也无法实现完全的验证,例如对日期的识别:
0 到 12 之间的月份数字 - 0 到 31 之间的日期数字,格式为月份 - 日期,如 01 - 01 到 12 - 31 之间的数字组合,其中月份数字为 01 到 12 中的一个,日期数字为 01 到 31 中的一个,整体格式为 ^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$
对于“2 - 30”这样的非法日期,用正则无法识别,或者说要写出来非常困难。对于“4 - 31”这样的非法日期,同样用正则难以识别或写出困难。在这种情况下,就需要程序来配合进行识别。
贪婪匹配
特别需要指出的是,正则匹配默认采用贪婪匹配方式,即会匹配尽可能多的字符。例如,能够匹配出数字后面的 0,如下所示:
使用正则表达式 r'^(\d+)(0*)$' 去匹配字符串 '102300' ,然后获取匹配结果中的分组,即第一个分组是数字部分 '1023' ,第二个分组是末尾的 0 部分 '00' ,最后通过 groups() 方法获取这两个分组并返回。
('102300', '') 可改写为:('102300', '无具体内容') 或 ('102300', '空值') 或 ('102300', '未提及具体内容') 等多种形式,具体含义需根据上下文确定。
因为采用了贪婪匹配,所以直接将后面的 0 全部匹配了,这样一来,0*就只能匹配空字符串了。
要把后面的 0 匹配出来,就必须让\d+采用非贪婪匹配,也就是尽可能少地进行匹配。而加上一个?就能让\d+采用非贪婪匹配。
使用正则表达式 re.match 去匹配字符串 '102300',匹配的模式是 '^(\d+?)(0*)$'。其中,(\d+?) 表示匹配一个或多个数字,尽可能少地匹配;(0*) 表示匹配零个或多个 0。然后通过 groups 方法获取匹配到的分组结果。
('1023', '00') 这组数据包含两个元素,一个是 '1023',另一个是 '00'。
编译
当我们在 Python 里使用正则表达式的时候,re 模块自身会进行两件事:一是进行模式匹配,二是进行字符串的搜索与替换等操作。
编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
用编译后的正则表达式去匹配字符串。
如果一个正则表达式需重复使用几千次,从效率方面考虑,我们能够预编译这个正则表达式。之后在重复使用时,就无需再进行编译这个步骤,直接进行匹配。
>>> import re
# 编译:
使用正则表达式 re.compile 创建了一个模式对象 re_telephone,该模式匹配以三个数字开头,后面跟着三到八个数字的字符串,即匹配形如 ^(\d{3})-(\d{3,8})$ 的字符串。
# 使用:
re_telephone 与字符串 '010-12345' 进行匹配,然后调用 groups 方法获取匹配结果的分组。
('010', '12345') 分别代表两个不同的内容,其中 '010' 可能是一个特定的代码或标识,而 '12345' 则是另一个与之相关的内容。
re_telephone 对 '010-8086' 进行匹配,然后调用 groups 方法,获取匹配结果的分组。
('010' 这个编码,'8086' 这个标识)
编译后会生成 Regular Expression 对象,此对象自身包含了正则表达式,因此在调用对应的方法时,无需给出正则字符串。
小结
正则表达式有着很强的能力,在很短的一节时间内讲完它是做不到的。若要把正则的所有内容都讲解清楚,那得写成一本很厚的书。倘若你时常碰到正则表达式方面的问题,或许就需要一本关于正则表达式的参考书籍。
练习
尝试去写一个能够验证 Email 地址的正则表达式。其中版本一能够验证出诸如这样的 Email。
[email protected]
[email protected]
版本二可以验证并提取出带名字的Email地址:
[email protected]
参考源码
regex.py