正则表达式是从信息中搜索特定的模式的一把瑞士军刀。它们是一个巨大的工具库,其中的一些功能经常被忽视或未被充分利用。今天我将向你们展示一些正则表达式的高级用法。
举个例子,这是一个我们可能用来检测电话美国电话号码的正则表达式:
1
|
r
'^(1[-\s.])?(\()?\d{3}(?(2)\))[-\s.]?\d{3}[-\s.]?\d{4}$'
|
我们可以加上一些注释和空格使得它更具有可读性。
1
2
3
4
5
6
7
8
9
|
r
'^'
r
'(1[-\s.])?'
# optional '1-', '1.' or '1'
r
'(\()?'
# optional opening parenthesis
r
'\d{3}'
# the area code
r
'(?(2)\))'
# if there was opening parenthesis, close it
r
'[-\s.]?'
# followed by '-' or '.' or space
r
'\d{3}'
# first 3 digits
r
'[-\s.]?'
# followed by '-' or '.' or space
r
'\d{4}$'
# last 4 digits
|
让我们把它放到一个代码片段里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import
re
numbers
=
[
"123 555 6789"
,
"1-(123)-555-6789"
,
"(123-555-6789"
,
"(123).555.6789"
,
"123 55 6789"
]
for
number
in
numbers:
pattern
=
re.match(r
'^'
r
'(1[-\s.])?'
# optional '1-', '1.' or '1'
r
'(\()?'
# optional opening parenthesis
r
'\d{3}'
# the area code
r
'(?(2)\))'
# if there was opening parenthesis, close it
r
'[-\s.]?'
# followed by '-' or '.' or space
r
'\d{3}'
# first 3 digits
r
'[-\s.]?'
# followed by '-' or '.' or space
r
'\d{4}$\s*'
,number)
# last 4 digits
if
pattern:
print
'{0} is valid'
.
format
(number)
else
:
print
'{0} is not valid'
.
format
(number)
|
输出,不带空格:
1
2
3
4
5
|
123
555
6789
is
valid
1
-
(
123
)
-
555
-
6789
is
valid
(
123
-
555
-
6789
is
not
valid
(
123
).
555.6789
is
valid
123
55
6789
is
not
valid
|
正则表达式是 python 的一个很好的功能,但是调试它们很艰难,而且正则表达式很容易就出错。
幸运的是,python 可以通过对 re.compile
或 re.match
设置 re.DEBUG
(实际上就是整数 128) 标志就可以输出正则表达式的解析树。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import
re
numbers
=
[
"123 555 6789"
,
"1-(123)-555-6789"
,
"(123-555-6789"
,
"(123).555.6789"
,
"123 55 6789"
]
for
number
in
numbers:
pattern
=
re.match(r
'^'
r
'(1[-\s.])?'
# optional '1-', '1.' or '1'
r
'(\()?'
# optional opening parenthesis
r
'\d{3}'
# the area code
r
'(?(2)\))'
# if there was opening parenthesis, close it
r
'[-\s.]?'
# followed by '-' or '.' or space
r
'\d{3}'
# first 3 digits
r
'[-\s.]?'
# followed by '-' or '.' or space
r
'\d{4}$'
, number, re.DEBUG)
# last 4 digits
if
pattern:
print
'{0} is valid'
.
format
(number)
else
:
print
'{0} is not valid'
.
format
(number)
|
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
at_beginning
max_repeat
0
1
subpattern
1
literal
49
in
literal
45
category category_space
literal
46
max_repeat
0
2147483648
in
category category_space
max_repeat
0
1
subpattern
2
literal
40
max_repeat
0
2147483648
in
category category_space
max_repeat
3
3
in
category category_digit
max_repeat
0
2147483648
in
category category_space
subpattern
None
groupref_exists
2
literal
41
None
max_repeat
0
2147483648
in
category category_space
max_repeat
0
1
in
literal
45
category category_space
literal
46
max_repeat
0
2147483648
in
category category_space
max_repeat
3
3
in
category category_digit
max_repeat
0
2147483648
in
category category_space
max_repeat
0
1
in
literal
45
category category_space
literal
46
max_repeat
0
2147483648
in
category category_space
max_repeat
4
4
in
category category_digit
at at_end
max_repeat
0
2147483648
in
category category_space
123
555
6789
is
valid
1
-
(
123
)
-
555
-
6789
is
valid
(
123
-
555
-
6789
is
not
valid
(
123
).
555.6789
is
valid
123
55
6789
is
not
valid
|
在我解释这个概念之前,我想先展示一个例子。我们要从一段 html 文本寻找锚标签:
1
2
3
4
5
6
7
|
结果将在意料之中:
1
|
|
我们改下输入,添加第二个锚标签:
结果看起来再次对了。但是不要上当了!如果我们在同一行遇到两个锚标签后,它将不再正确工作:
这次模式匹配了第一个开标签和最后一个闭标签以及在它们之间的所有的内容,成了一个匹配而不是两个 单独的匹配。这是因为默认的匹配模式是“贪婪的”。
当处于贪婪模式时,量词(比如
*
和+
)匹配尽可能多的字符。
当你加一个问号在后面时(.*?
)它将变为“非贪婪的”。
现在结果是正确的。
一个前向界定符搜索当前的匹配之后搜索匹配。通过一个例子比较好解释一点。
下面的模式首先匹配 foo
,然后检测是否接着匹配 bar
:
1
2
3
4
5
6
7
8
9
10
11
|
import
re
strings
=
[
"hello foo"
,
# returns False
"hello foobar"
]
# returns True
for
string
in
strings:
pattern
=
re.search(r
'foo(?=bar)'
, string)
if
pattern:
print
'True'
else
:
print
'False'
|
这看起来似乎没什么用,因为我们可以直接检测 foobar
不是更简单么。然而,它也可以用来前向否定界定。 下面的例子匹配foo
,当且仅当它的后面没有跟着 bar
。
1
2
3
4
5
6
7
8
9
10
11
12
|
import
re
strings
=
[
"hello foo"
,
# returns True
"hello foobar"
,
# returns False
"hello foobaz"
]
# returns True
for
string
in
strings:
pattern
=
re.search(r
'foo(?!bar)'
, string)
if
pattern:
print
'True'
else
:
print
'False'
|
后向界定符类似,但是它查看当前匹配的前面的模式。你可以使用 (?>
来表示肯定界定,(? 表示否定界定。
下面的模式匹配一个不是跟在 foo
后面的 bar
。
1
2
3
4
5
6
7
8
9
10
11
12
|
import
re
strings
=
[
"hello bar"
,
# returns True
"hello foobar"
,
# returns False
"hello bazbar"
]
# returns True
for
string
in
strings:
pattern
=
re.search(r
'(?
if
pattern:
print
'True'
else
:
print
'False'
|
正则表达式提供了条件检测的功能。格式如下:
(?(?=regex)then|else)
条件可以是一个数字。表示引用前面捕捉到的分组。
比如我们可以用这个正则表达式来检测打开和闭合的尖括号:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import
re
strings
=
[
"
,
# returns true
"
"bar>"
,
# returns false
"hello"
]
# returns true
for
string
in
strings:
pattern
=
re.search(r
'^(<)?[a-z]+(?(1)>)$'
, string)
if
pattern:
print
'True'
else
:
print
'False'
|
在上面的例子中,1
表示分组 (<)
,当然也可以为空因为后面跟着一个问号。当且仅当条件成立时它才匹配关闭的尖括号。
条件也可以是界定符。
分组,由圆括号括起来,将会捕获到一个数组,然后在后面要用的时候可以被引用。但是我们也可以不捕获它们。
我们先看一个非常简单的例子:
1
2
3
4
5
|
import
re
string
=
'Hello foobar'
pattern
=
re.search(r
'(f.*)(b.*)'
, string)
print
"f* => {0}"
.
format
(pattern.group(
1
))
# prints f* => foo
print
"b* => {0}"
.
format
(pattern.group(
2
))
# prints b* => bar
|
现在我们改动一点点,在前面加上另外一个分组 (H.*)
:
1
2
3
4
5
|
import
re
string
=
'Hello foobar'
pattern
=
re.search(r
'(H.*)(f.*)(b.*)'
, string)
print
"f* => {0}"
.
format
(pattern.group(
1
))
# prints f* => Hello
print
"b* => {0}"
.
format
(pattern.group(
2
))
# prints b* => bar
|
模式数组改变了,取决于我们在代码中怎么使用这些变量,这可能会使我们的脚本不能正常工作。 现在我们不得不找到代码中每一处出现了模式数组的地方,然后相应地调整下标。 如果我们真的对一个新添加的分组的内容没兴趣的话,我们可以使它“不被捕获”,就像这样:
1
2
3
4
5
|
import
re
string
=
'Hello foobar'
pattern
=
re.search(r
'(?:H.*)(f.*)(b.*)'
, string)
print
"f* => {0}"
.
format
(pattern.group(
1
))
# prints f* => foo
print
"b* => {0}"
.
format
(pattern.group(
2
))
# prints b* => bar
|
通过在分组的前面添加 ?:
,我们就再也不用在模式数组中捕获它了。所以数组中其他的值也不需要移动。
像前面那个例子一样,这又是一个防止我们掉进陷阱的方法。我们实际上可以给分组命名, 然后我们就可以通过名字来引用它们,而不再需要使用数组下标。格式是:(?Ppattern)
我们可以重写前面那个例子,就像这样:
1
|