前言
接触regular expression是从一个python的方法string.find()所引申出来的。对于一些普通的字符串元素查找和替换,感觉普通的find, replace等方法就已经足够了。可是在一些牵涉到复杂的应用里,还是非用regular expression不可。另外,一些web框架里url mapping的手段也大量的用到了regular expression。regular expression本质上就是字符串的模式搜索和匹配。这里有必要对最近常使用的一些方法做一个总结。虽然示例都是采用了python代码,但是使用其他的语言也有类似的方法,因为对于我们需要匹配的模式对所有语言来说都是一样的。
几个最常用的方法
在python里,和regular expression打交道的主要就是re包。这里有最常用的几个方法,在我们做一些模式匹配的时候要用到。这里,几个常用的方法如下表:
方法名 | 说明 |
search | 扫描整个字符串找到匹配的位置,并返回MatchObject对象。 如果没有找到匹配的则返回None. |
match | 如果0个或者多个在目标字符串开头的地方和表达式匹配,则返回对应的MatchObject对象。 否则返回None. |
findall | 按照从左到右的方式,查找字符串中所有不重叠的模式匹配。 |
finditer | 和findall类似,不过返回一个所有匹配字符模式的迭代器, 可以通过访问迭代器的方式来访问所有匹配结果。 |
sub | 将匹配的模式字符串替换成目标字符串。类似于replace方法。 |
这里列出来的说明看起来有点抽象。那么我们就先来看一个简单的示例。
search
>>>import re
>>>m = re.search('foo', 'seafood')
>>> m.group()
'foo'
>>> m.start()
3
>>> m.end()
6
我们先看search方法,这里前面的'foo'就是我们需要查找匹配的字符串。在目标字符串中,'seafood'中是中间部分和'foo'匹配。于是返回的MatchObject不是None。通过start, end方法我们可以分别找到匹配的字符串在目标串中的起始匹配下标和结束下标+1。这里有意思的地方就是'foo'在'seafood'中最后匹配的字符index是5,而这里m.end返回却是6。因为这里的结束索引6用在m[s:e]的时候,后面的结束索引是不包含在结果里的。如果我们这里取如下的方法:
>>> a = 'seafood'
>>> a[3:6]
'foo'
用一个数学符号来概括的话,这里的[s:e]表达的是一个半开半闭区间,比如说这里应该是这样: [3, 6)。
match
前面search的方法我们好理解,就是在一个字符串里找匹配的串。那么match方法表示什么意思呢?我们看如下的代码:
>>> import re
>>> match = re.match('foo', 'seafood')
>>> match is None
True
很奇怪,这里和前面search方法传的参数一样,结果却显示match为None,也就是说根本没有找到匹配的。如果我们再修改一下:
>>> match = re.match('foo', 'food')
>>> match.group()
'foo'
>>> match.start()
0
>>> match.end()
3
通过这部分代码,我们也就知道了,match要能找到匹配,必须要有一个条件就是它必须是和给定的目标串的开头匹配才行,如果开头没有匹配,就认为是没有匹配。
findall, finditer
findall方法返回的主要是一个list,里面是匹配的串内容:
>>> match = re.findall('ab', 'abaabbaaabbb')
>>> type(match)
<type 'list'>
>>> for item in match:
... print item
...
ab
ab
ab
这种方式返回的结果就是匹配的串。当然,在一些我们需要更多信息的地方,比如匹配的结果在目标串的某个具体index时,finditer是一个更好的选择。我们用finditer来实现类似的功能代码如下:
>>> match = re.finditer('ab', 'abaabbaaabbb')
>>> for item in match:
... s = item.start()
... e = item.end()
... print 'Found "%s" at %d:%d' % (item.group(), s, e)
...
Found "ab" at 0:2
Found "ab" at 3:5
Found "ab" at 8:10
这里,finditer可以说是一个更加强大的findall。它在我们要找到所有或者多个匹配结果的地方使用很合适。
compile
和前面几个方法不一样,前面的几个方法描述的是各种不同的匹配方式。这里,compile方法则是用来提高模式匹配的效率。我们前面的示例里,基本上是指定一个模式串和一个目标串之后就执行匹配方法。这种方式很简单直接,但是在某些情况下如果要考虑到效率的时候,我们可以考虑用compile方法来做一些优化。比如有的时候我们的这个模式串的匹配要使用的比较频繁或者我们有多个串要做匹配。我们来看看如下的代码:
import re
# generate list of expression objects
regexes = [re.compile(p) for p in ['this', 'that']]
text = 'Does this text match the pattern?'
print 'Text: %r\n' % text
for regex in regexes:
print 'Seeking "%s" ->' % regex.pattern,
match = regex.search(text)
if match:
print 'match! %s %s %s' % (match.string, match.start(), match.end())
else:
print 'no match'
这里有意思的是我们的re.compile返回了一个expression object,然后我们再用这个expression object再执行search方法。执行的结果如下:
Text: 'Does this text match the pattern?'
Seeking "this" -> match! Does this text match the pattern? 5 9
Seeking "that" -> no match
regular expression符号说明
前面我们举的例子实际上只是一个简单的字符串匹配,而且还是完整的匹配。从运用的角度来说,我们用一些简单的string.find等方法也可以达到同样的目的。regular expression和那些看似简单的方法的区别在于它有更强的表达能力。在很多需要满足各种灵活要求的时候,他们就可以派上用场。比如说我们需要匹配某些字符还要包含大小写,或者我们指定匹配多少个等等,这里我们就可以看到regular expression的强大了。我们分成几个部分来讨论。
基本符号
常见的一些符号如下表:
符号 | 描述 | 示例 |
字符串常量 | 匹配完整的制定字符串 | re.search('foo', 'foobar') |
re1|re2 | 匹配表达式1或者表达式2 | foo|bar |
. | 匹配任何字符,除了\n | b.b |
^ | 匹配字符串的开头 | ^Start |
$ | 匹配字符串的结尾 | end$ |
* | 匹配前面模式0到多次 | [a-z]* |
+ | 匹配前面模式1到多次 | [a-z]+ |
? | 匹配前面模式0到1次 | ab? |
{N} | 匹配前面模式N次 | [0-9]{3} |
{M, N} | 匹配前面模式M到N次 | [0-9]{2, 5} |
[...] | 匹配里面的单个字符 | [acde] |
[x-y] | 匹配里面从x到y这个范围内的单个字符 | [0-9] |
[^...] | 不匹配指定范围内的字符 | [^abc], [^0-9] |
我们一个一个的看过来:
>>> match = re.search('foo|bar', 'foodbar')
>>> match.group()
'foo'
>>> match.start()
0
>>> match.end()
3
>>> match = re.search('foo|bar', 'bargine')
>>> match.group()
'bar'
>>> match.start()
0
>>> match.end()
3
这里相当于一个表达或关系的符号,我们提供的一个或者多个模式,只要有一个匹配就可以了。match.group返回被匹配的那个模式。
>>> match = re.search('ab.', 'abcdefg')
>>> match.group()
'abc'
>>> match.start()
0
>>> match.end()
3
这里的“.”符号可以匹配几乎任意的符号,所以当我们提供一个'abcdefg'这样的串时,表示匹配上了'abc'。
我们再看看字符串的开始和结束的匹配,如果用过django做过web开发的话,会发现里面路径映射的时候要用到很多这样的符号。
>>> match = re.search('^goo', 'goodbye')
>>> match.group()
'goo'
>>> match = re.search('good$', 'sounds good')
>>> match.group()
'good'
从前面我们可以看出,对于指定开头的search方法,从某种程度来说就相当于一个match方法。
另外,对于那些指定匹配次数的应用示例如下:
>>> match = re.findall('[0-9]*', '123abc456')
>>> match
['123', '', '', '', '456', '']
>>> match = re.findall('[0-9]+', '123abc456')
>>> match
['123', '456']
>>> match = re.findall('[0-9]?', '123abc456')
>>> match
['1', '2', '3', '', '', '', '4', '5', '6', '']
>>> match = re.findall('[0-9]{2}', '123abc456')
>>> match
['12', '45']
>>> match = re.findall('[^aeiou]', 'abcdefghijk')
>>> match
['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k']
除了以上这些基本的符号应用之外,还有一些特殊的符号应用可以保证我们写出更加精简的匹配模式。
一些特殊字符
我们前面列举的一些字符可以满足很大一部分的要求,在某些情况下,一些特殊字符能够提供更加强大的支持。
符号 | 描述 | 示例 |
\d | 匹配单个数字,和[0-9]一样(\D和其意思相反,表示不匹配任何数字) | data\d+.txt |
\w | 匹配数字和字母,包括下划线,和[A-Za-z0-9_]表示一样(\W与此意相反) | [A-Za-z_]\w+ |
\s | 匹配任何空格类型的字符和[\n\t\r\v\f]一样 | This\sis |
\b | 匹配每个词语的边界(指每个词语的开头和结尾两个部分) | \bThe\b |
\A\Z | 匹配字符串的开头和结尾,与^$相同 | \AMonth |
这些特殊字符的表示方式更简洁,我们看如下的示例:
>>> match = re.findall(r'\d+', '123abc234')
>>> match
['123', '234']
>>> match = re.findall(r'\w+', '123 abc, result 12.34 _123')
>>> match
['123', 'abc', 'result', '12', '34', '_123']
这里要特别说明一下\b的用法,它和字符串的开始和结束容易混淆。实际上对于一个字符串来说,里面可能有多个单独的词语。在\b的用法里,每个单独的词语作为考察的边界。而对于字符的开始和结束符号^$来说,就是整个字符串的开始和结束。中间出现空格和其他词语都不在考察的范围。
>>> match = re.findall(r'\bt\w+', 'This is a test string, that contains 2 matches.')
>>> match
['test', 'that']
>>> match = re.findall(r'\w+t\b', 'at that mat t')
>>> match
['at', 'that', 'mat']
>>> match = re.findall(r't$', 'at that mat t')
>>> match
['t']
>>> match = re.findall(r'^t', 'this is a test string, that contains 2 matches.')
>>> match
['t']
相信看完了上面这些比较代码之后,我们就知道该如何使用这两者了。
greedy和nongreedy
应用regular expression的时候还有一个需要考虑的就是greedy和nongreedy的属性。该怎么理解这个东西呢?官方的说明是,regular expression的模式匹配默认是贪婪的。就是说如果我们一个目标串和我们的模式匹配,则我们的表达式尽量匹配尽可能多的字符串。在某些情况下,我们需要调整一下。让我们先看一下示例:
>>> match = re.finditer('[ab]', 'abbaabbba')
>>> for item in match:
... print item.group(), item.start(), item.end()
...
a 0 1
b 1 2
b 2 3
a 3 4
a 4 5
b 5 6
b 6 7
b 7 8
a 8 9
这里是我们要匹配字符a或者b,每次打印匹配的字符和匹配的范围。如果我们把前面的代码修改一下:
>>> match = re.finditer('a[ab]+', 'abbaabbba')
>>> for item in match:
... print item.group(), item.start(), item.end()
...
abbaabbba 0 9
我们这里的模式是一个字母a,后面接1到多个字母a或者b。那么,ab就是一个符合我们匹配要求的。但是这里却返回了整个串。这就是greedy的意思。只返回匹配最长的那个串。那么,如果这里我们要返回最小的匹配呢?比如说前面示例里我只要返回一个尽量小的ab就可以了。那该怎么修改呢?
>>> match = re.finditer('a[ab]+?', 'abbaabbba')
>>> for item in match:
... print item.group(), item.start(), item.end()
...
ab 0 2
aa 3 5
这里找到了索引在0和3开始的两个字串,而且都只是找到恰好匹配的。取消greedy特性的方法很简单,就是在设置多个元素属性的地方加一个问号(?)。
总结
regular expression主要用于解决字符串匹配相关的问题,它本身很灵活,相关的匹配问题更多的归结为如何描述需要匹配的目的。这些更多的是和具体使用语言无关的。这里总结了几个基本的regular expression特性和用法。在后续文章里会讨论它的一些更加特殊的用法。