0.摘要
初步了解 P y t h o n 3 Python3 Python3中的正则表达式。
1.案例研究: 街道地址
我的目的是要标准化街道的格式,比如把街道最后的英文单词
R
O
A
D
ROAD
ROAD替换为
R
D
.
RD.
RD.。在第一个例子中使用
r
e
p
l
a
c
e
(
)
replace()
replace()就可以了。
但是有些街道名中
R
O
A
D
ROAD
ROAD出现了多次,比如第二个例子,这时候替换得到的结果是错误的。
为了解决上面这个问题,你可能会这么考虑:只在地址的最后四个字符中替换
R
O
A
D
ROAD
ROAD(
s
[
−
4
:
]
s[-4:]
s[−4:])。然后把剩下的字符串独立开来处理(
s
[
:
−
4
]
s[:-4]
s[:−4])。这个方法很笨拙。比如,这个方法会依赖于你要替换的字符串长度(如果你用
.
S
T
.ST
.ST来替换
S
T
R
E
E
T
STREET
STREET,就需要在
s
[
−
6
:
]
s[-6:]
s[−6:]中查找
S
T
R
E
E
T
STREET
STREET,然后再取
s
[
:
−
6
]
s[:-6]
s[:−6]。你难道还想半年后回来继续修改BUG?反正我是不想。
是时候转换到正则表达式了。在
p
y
t
h
o
n
python
python中,所有的正则表达式相关功能都包含在
r
e
re
re模块中。注意第一个参数最后的美元符号,这是一个匹配
R
O
A
D
ROAD
ROAD仅仅出现在字符串结尾的正则表达式。表示字符串结尾(还有一个相应的表示字符串开头的字符 ^ )。正则表达式模块的
r
e
.
s
u
b
(
)
re.sub()
re.sub()函数可以做字符串替换,它在字符串
s
s
s中用正则表达式
R
O
A
D
$
ROAD$
ROAD$来搜索并替换成
R
D
.
RD.
RD.。它只会匹配字符串结尾的
R
O
A
D
ROAD
ROAD,而不会匹配到
B
R
O
A
D
BROAD
BROAD中的
R
O
A
D
ROAD
ROAD,因为这种情况它在字符串的中间。
但是如果有些地址的结尾没有
R
O
A
D
ROAD
ROAD的话,还是会出现问题:
我真正想要的
R
O
A
D
ROAD
ROAD,必须是匹配到字符串结尾,并且是独立的词(他不能是某个比较长的词的一部分)。为了在正则表达式中表达这个独立的词,你可以使用“\b”(匹配单词的边界)。它的意思是在此处必须有一个分隔符。在
p
y
t
h
o
n
python
python中,比较复杂的是“\”字符必须被转义,这有的时候会导致“\”字符传染(想想可能还要对“\”字符做转义的情况)。
为了解决这个问题,可以使用原始字符串。这只需要在字符串的前面添加一个字符"r"。它告诉
p
y
t
h
o
n
python
python,字符串中没有任何字符需要转义。‘\t’是一个制表符,但r‘\t’只是一个字符‘\’紧跟着一个字符t。我建议在处理正则表达式的时候总是使用原始字符串。否则,会因为理解正则表达式而消耗大量时间(本身正则表达式就已经够让人困惑的了)。
哎,不幸的是,我发现了更多的地方与我的逻辑背道而驰。街道地址包含了独立的单词ROAD,但并不是在字符串尾,因为街道后面还有个单元号。因为ROAD不在末尾,所以不能匹配,因此
r
e
.
s
u
b
(
)
re.sub()
re.sub()最后没有做任何的替换,只是返回了一个原始的字符串,这并不是我想要的。
为了解决这个问题,我删除了正则表达式尾部的$,然后添加了一个\b。现在这个正则表达式的意思是:在字符串的任意位置匹配独立的ROAD单词,不管是在字符串的结束还是开始,或者中间的任意一个位置。
2.案例研究: 罗马数字
下面是几个通常的规则来构成罗马数字:
1.
1.
1.大部分时候用字符相叠加来表示数字。
I
I
I是1,
I
I
II
II是2,
I
I
I
III
III是3。
V
I
VI
VI是6(挨个看来,是“5 和 1”的组合),
V
I
I
VII
VII是7,
V
I
I
I
VIII
VIII是8。
2.
2.
2.含有10的字符(
I
I
I,
X
X
X,
C
C
C和
M
M
M)最多可以重复出现三个。为了表示4,必须用同一位数的下一个更大的数字5来减去一。不能用
I
I
I
I
IIII
IIII来表示4,而应该是
I
V
IV
IV(意思是比5小1)。40写做
X
L
XL
XL(比50小10),41写做
X
L
I
XLI
XLI,42写做
X
L
I
I
XLII
XLII,43写做
X
L
I
I
I
XLIII
XLIII,44写做
X
L
I
V
XLIV
XLIV(比50小10并且比5小1)。
3.
3.
3.为了表示一个中间的数字,需要从一个最终的值来减。比如:9需要从10来减:8是
V
I
I
I
VIII
VIII,但9是
I
X
IX
IX(比10小1),并不是
V
I
I
I
I
VIIII
VIIII(
I
I
I字符不能重复4次)。90是
X
C
XC
XC,900是
C
M
CM
CM。
4.
4.
4.表示5的字符不能在一个数字中重复出现。10只能用
X
X
X表示,不能用
V
V
VV
VV表示。100只能用
C
C
C表示,而不是
L
L
LL
LL。
5.
5.
5.罗马数字是从左到右来计算,因此字符的顺序非常重要。
D
C
DC
DC表示600,而
C
D
CD
CD完全是另一个数字400(比500小100)。
C
I
CI
CI是101,
I
C
IC
IC不是一个罗马数字(因为你不能从100减1,你只能写成
X
C
I
X
XCIX
XCIX,表示比100小10,且比10小1)。
2.1检查千位数
怎么验证一个字符串是否是一个合法的罗马数字呢?我们可以每次取一个字符来处理。因为罗马数字总是从高位到低位来书写。我们从最高位的千位开始。表示1000或者更高的位数值,方法是用一系列的M来重复表示。
这个模式有三部分。^表示必须从字符串开头匹配;
M
?
M?
M?表示匹配
M
M
M 1次或0次;$匹配字符串结束。所以这个模式可以匹配字符串
M
、
M
M
、
M
M
M
M、MM、MMM
M、MM、MMM或空串。
r
e
re
re模块最基本的方法是
s
e
a
r
c
h
(
)
search()
search()函数。它使用正则表达式来匹配字符串。如果成功匹配,
s
e
a
r
c
h
(
)
search()
search()返回一个匹配对象。匹配对象中有很多的方法来描述这个匹配结果信息。如果没有匹配到,
s
e
a
r
c
h
(
)
search()
search()返回
N
o
n
e
None
None。你只需要关注
s
e
a
r
c
h
(
)
search()
search()函数的返回值就可以知道是否匹配成功。
2.2检查百位数
这个正则表达式的写法从上面千位的匹配方法接着往后写。检查字符串开始(^),然后是千位,后面才是新的部分。这里用圆括号定义了三个不同的匹配模式,他们是用竖线分隔的:CM,CD和D?C?C?C?(这表示是一个可选的D,以及紧跟的0到3个可选的字符C)。正则表达式按从左到右的顺序依次匹配,如果第一个CM匹配成功,用竖线分隔这几个中的后面其他的都会被忽略(相当于逻辑运算符||)。
3.使用语法{n,m}
在上一节中,你处理过同样的字符可以重复0到3次的情况。实际上,还有另一种正则表达式的书写方式可以表达同样的意思,而且这种表达方式更具有可读性。首先看看我们在前面例子中使用的方法。
现在使用
{
n
,
m
}
\{n,m\}
{n,m}语法:
这个正则表达式的意思是:匹配字符串开始,然后是任意的0到3个M字符,再是字符串结尾。0和3的位置可以写任意的数字。如果你想表示可以匹配的最小次数为1次,最多为3次M字符,可以写成M{1,3}。
3.1检查十位和个位
首先我们要清楚十位和个位都有哪些表示。
- 10 = X 10=X 10=X
- 20 = X X 20=XX 20=XX
- 30 = X X X 30=XXX 30=XXX
- 40 = X L 40=XL 40=XL
- 50 = L 50=L 50=L
- 60 = L X 60=LX 60=LX
- 70 = L X X 70=LXX 70=LXX
- 80 = L X X X 80=LXXX 80=LXXX
- 90 = X C 90=XC 90=XC
那么十位的可以表示为: ( X C ∣ X L ∣ L ? X { 0 , 3 } ) (XC|XL|L?X\{0,3\}) (XC∣XL∣L?X{0,3})。
- 1 = I 1=I 1=I
- 2 = I I 2=II 2=II
- 3 = I I I 3=III 3=III
- 4 = I V 4=IV 4=IV
- 5 = V 5=V 5=V
- 6 = V I 6=VI 6=VI
- 7 = V I I 7=VII 7=VII
- 8 = V I I 8=VII 8=VII
- 9 = I X 9=IX 9=IX
个位的可以表示为
(
I
X
∣
I
V
∣
V
?
I
{
0
,
3
}
)
(IX|IV|V?I\{0,3\})
(IX∣IV∣V?I{0,3})。结合之前的模式,我们可以得出最终结果:
4.松散正则表达式
现在我们来研究一下怎么让你的正则表达式更具有维护性,但表达的意思却是相同的。到目前为止,你只是处理了一些小型的正则表达式。就像你所看到的,他们难以阅读,甚至你不能保证半年后,你还能理解这些东西,并指出他们是干什么的。所以你需要在正则表达式内部添加一些说明信息。
p
y
t
h
o
n
python
python允许你使用松散正字表达式来达到目的。松散正字表达式和普通紧凑的正则表达式有两点不同:
- 空白符被忽略。空格、制表符和回车在正则表达式中并不会匹配空格、制表符、回车。如果你想在正则表达式中匹配他们,可以在前面加一个\来转义。
- 注释信息被忽略。松散正则表达式中的注释和
p
y
t
h
o
n
python
python代码中的一样,都是以#开头直到行尾。它可以在多行正则表达式中增加注释信息,这就避免了在python代码中的多行注释。他们的工作方式是一样的。
注意,如果要使用松散正则表达式,需要传递一个叫 r e . V E R B O S E re.VERBOSE re.VERBOSE的参数。就像你看到的那样,正则表达式中有很多空白符,他们都被忽略掉了。还有一些注释信息,当然也被正则表达式忽略掉。当空白符和注释信息被忽略掉后,这个正则表达式和上面的是完全一样的,但是它有更高的可读性。如果不传递这个参数,空白符等将不会被忽略,可以看第四个例子。
5.案例研究: 解析电话号码
到目前为止,我们主要关注于整个表达式是否能匹配到,要么整个匹配,要么整个都不匹配。但正则表达式还有更加强大的功能。如果正则表达式成功匹配,你可以找到正则表达式中某一部分匹配到什么。
我们通常从左到右的阅读正则表达式。首先是匹配字符串开始位置,然后是(\d{3})。\d{3}表示什么意思?\d表示任意的数字(0到9),{3}表示一定要匹配3个数字。这个是你前面看到的{n,m}表示方法。把他们放在圆括号中,表示必须匹配3个数字,并且把他们记做一个组。分组的概念我们后面会说到。然后匹配一个连字符,接着匹配另外的3个数字,他们也同样作为一个组。然后又是一个连字符,后面还要准确匹配4个数字,他们也作为一位分组。最后匹配字符串结尾。
为了使用正则表达式匹配到的这些分组,需要对
s
e
a
r
c
h
(
)
search()
search()函数的返回值调用
g
r
o
u
p
s
(
)
groups()
groups()方法。它会返回一个这个正则表达式中定义的所有分组结果组成的元组。在这里,我们定义了三个分组,一个三个数字,另一个是三个数字,以及一个四个数字。
请注意,不要链式调用
s
e
a
r
c
h
(
)
search()
search()和
g
r
o
u
p
s
(
)
groups()
groups()。因为前者的结果可能是
N
o
n
e
None
None。
这个正则表达式和前面的一样。匹配了字符串开始位置,然后是一个三个数字的分组,接着一个连字符,又是一个三个数字的分组,又是一个连字符,然后一个四个数字的分组。这三个分组匹配的内容都会被记忆下来。和上面不同的是,这里多匹配了一个连字符以及一个分组,这个分组里的内容是匹配一个或更多个数字。最后是字符串结尾。
现在
g
r
o
u
p
s
(
)
groups()
groups()方法返回有四个元素的元组。因为正则表达式现在定义了四个组。
不幸的是,这个正则表达式仍然不是最终答案。因为它假设这些数字是有连字符分隔的。实际上还有用空格,逗号和点分隔的情况。这就需要用更加通用的解决方案来匹配这些不同的分隔符。
噢,这个正则表达式不但不能做到你想要的,而且还不如上一个了!因为我们现在不能匹配没有分机号的电话号码,这绝对不是你想要的。
注意了!你匹配了字符串开始,然后是3个数字的分组,接着是\D+,这是什么?好吧,\D匹配除了数字以外的任意字符,+的意思是一个或多个。因此\D+匹配一个或一个以上的非数字字符。这就是你用来替换连字符的东西,它用来匹配不同的分隔符。
所以当分隔符为空格、连字符或其它的字符时,依然可以匹配成功。但是还有一些写问题需要解决,比如没有分隔符时,没有分机号时。
这里和上面唯一不同的地方是,把所有的+换成了*。号码之间的分隔符不再用\D+来匹配,而是使用\D*。+表示1个或多个,*表示0个或多个。
所以当没有分隔符或者没有分机号时,它依然可以工作。注意当没有分机号时,第四个字符串是空的。
这还没有结束。还有什么问题呢?在区域码前面还可能有其他字符。但正则表达式假设区域码在字符串的开头,没关系,你还可以使用0个或更多的非数字字符串来跳过区位码前面的字符。
只需要在第一个分组之前加上\D*来匹配0个或多个非数字字符即可。注意你不会对这些非数字字符分组,因为他们不在圆括号内,也就是说不是一个组。如果发现有这些字符,这里只是跳过他们,然后开始对后面的区域码匹配、分组。
但是如果在第一个分组之前有数字字符,比如第4个例子,我们还是无法成功匹配。因为\D*并不能匹配数字字符。
到目前为止,所有的正则表达式都匹配了字符串开始位置。但现在在字符串的开头可能有一些你想忽略掉的不确定的字符。为了匹配到想要的数据,你需要跳过他们。我们来看看不明确匹配字符串开始的方法。
注意正则表达式没有^。不会再匹配字符串开始位置了。正则表达式不会匹配整个字符串,而是试图找到一个字符串开始匹配的位置,然后从这个位置开始匹配。
6.小结
这只是正则表达式能完成的工作中的冰山一角。换句话说,尽管你可能很受打击,相信我,你已经不是什么都不知道了。
现在,你应该已经熟悉了下面的技巧:
正则表达式非常强大,但它也并不是解决每一个问题的正确答案。你需要更多的了解来判断哪些情况适合使用正则表达式。某些时候它可以解决你的问题,某些时候它可能带来更多的问题。