盘一盘 Python 系列特别篇 – 正则表达式
- 2020 年 3 月 12 日
- 筆記
以下文章来源于王的机器 ,作者王圣元
0
引言
正则表达式(Regular Expression, RE)就是一组定义某种搜索模式(pattern)的字符。
最简单的 RE 例子如下。
'steven'
很明显,这样一个 RE 只能搜索出含 steven 的字符串。
你可以用 Python 代码来验证,但现在假设我们还不会写,我们可以去 https://regex101.com/ 来验证。如下图右上角所示,匹配成功。

这样来搜索未免太傻了,有没有稍微智能一点的方法。再看下面的 RE。
^s....n$
上面的 RE 定义的模式如下:任何 6 个字符的单词,以 s 开头 (^s 的效果),以 n 收尾 (n$ 的效果)。之所以是 6 个字符,是因为有 4 个点 (.) 加上 s 和 n 字符。用上面那个网站做验证,这个 RE ^s….n$ 的若干匹配结果如下:
- seven:不匹配(五个字母)
- strong man:不匹配(十个字母加空格)
- soften:匹配
- steven:匹配
- Steven:不匹配
看最后两个 steven 和 Steven,区别是第一个字母的大小写,如果我想匹配两者怎么办呢?用下面的 RE
^[s|S]....n$
中括号 [] 表示一个集合,而 | 分隔集合里面的元素,在本例是 s 和 S。意思就是匹配开头的 s 或 S,结尾是 n 的 6 字符的单词。


这样每次固定单词长度也不太智能吧(比如长度为 n 就需要手动输入 n 个点 .),开头 s 结尾 n 的单词好多呢,我如果都想搜索出来该怎么办呢?看下面的 RE
^s[a-z]+n$
现在 sun 和 strengthen 都可以匹配出来了。起作用的是 [a-z]+,[a-z] 表示小写的字母 a 到 z 的集合,而 + 代表大于一次,联合在一起的意思就是该单词“以 s 开头,以 n 结尾,中间有大于一个的任何小写字母”。


但上述模式对单词 self-restrain 不起作用,因为有个短连接线(hyphen)。

没关系,我们把 – 加入字母集合里,写成 [a-z-]+,注意第一个 – 表示从 a 到 z,第二个 – 表示短连接线。现在可以匹配 self-restrain 了。

目前对 RE 有点感觉了吧,即便不会确切的表示也没关系,因为这就是本帖要介绍的。还是那句话,兴趣最重要,有兴趣才能有效的往下看。
本帖结构如下:
- 原始字符串
- 五类元字符
- 七个函数
- 三个实例
注:本帖里的 RE 可视化可参考链接 https://www.debuggex.com/。
1
原始字符串
原始字符串(raw string)是所有的字符串都是直接按照字面的意思来使用,没有转义特殊或不能打印的字符,通常简称为 r-string。
如果没有转义字符,原始字符串和普通字符串是一样的,比如
print('hello') print(r'hello')
hello hello
如果有转义字符(用反斜线 ),原始字符串和普通字符串是不一样的,比如
print('blake') print(r'blake')
lake blake
因此,不管什么时候用原始字符串准没错。
2
元字符
元字符(meta character)就是一种自带特殊含义的字符,也叫特殊字符。比如 [] * + ? {} | () . ^ $ ,原字符按用途可分五类:
- 表示集合:[]
- 表示次数:* + ? {}
- 表示并列:|
- 用于提取:()
- 用于转义:. ^ $
首先定义一个函数,当在句子(是个字符串 str)没有发现模式 pat 时,返回“没有找到”,反之打印出所有符合模式的子字符串。
import re def look_for(pat, str): return '没有找到' if re.search(pat, str) is None else re.findall(pat, str)
上面代码中的 re 是 Python 中正则表达式的库,而 search 和 findall 是包里的两个函数,顾名思义它们做的就是搜索和找出全部的意思,第三节会详解讲。
2.1
集合字符
中括号(square bracket)- []
中括号表示一个字符集,即创建的模式匹配中括号里指定字符集中的任意一个字符,字符集有三种方式来表现:
- 明确字符:[abc] 会匹配字符 a,b 或者 c
- 范围字符:[a-z] 会匹配字符 a 到 z
- 补集字符:[^6] 会匹配除了 6 以外的字符
下面我们来一一细看。
明确字符
匹配中括号里面的任意一个字符。
pat = r'[abc]'
print( look_for(pat, 'a') ) print( look_for(pat, 'ac') ) print( look_for(pat, 'cba') ) print( look_for(pat, 'steven') )
['a'] ['a', 'c'] ['c', 'b', 'a'] 没有找到
分析如下:
该模式只匹配字符 a,b 或者 c,因此前三个例子的字符串里都有相应字符匹配,而最后例子里的 steven 不包含 a, b 或 c。
模式 [abc] 的可视图如下,注意 “One of” 是说集合里面的字符是“或”的关系。

范围字符
在 [ ] 中加入 – 即可设定范围,比如
- [a-e] = [abcde]
- [1-4] = [1234]
- [a-ep] = [abcdep]
- [0-38] = [01238]
看两个例子。
print( look_for(r'[a-ep]', 'person') ) print( look_for(r'[0-38]', '666') )
['p', 'e'] 没有找到
分析如下:
- 例一的模式等价于 [abcdep],匹配单词 person 里面的 p 和 e 字符。
- 例二的模式等价于 [01238],不匹配单词 666 里面的任何字符。
模式 [a-ep] 和 [0-38] 的可视图如下。


补集字符
在 [ ] 中加入 ^ 即可除去后面的字符集,比如
- [^abc] 就是非 a, b, c 的字符
- [^123] 就是非 1, 2, 3 的字符
看四个例子。
print( look_for(r'[^abc]', 'baba') ) print( look_for(r'[^abc]', 'steven') ) print( look_for(r'[^123]', '456') ) print( look_for(r'[^123]', '1+2=3') )
没有找到 ['s', 't', 'e', 'v', 'e', 'n'] ['4', '5', '6'] ['+', '=']
分析如下:
- 例一 baba 里面所有字母不是 a 就是 b,因此没有匹配
- 例二 steven 所有字母都不是 a, b 和 c,因此全部匹配
- 例三 456 所有字母不是 1,2 和 3,因此全部匹配
- 例四 1+2=3 有 +号和 =号不是 1, 2 和 3,因此它俩匹配
模式 [^abc] 和 [^123] 的可视图如下。注意 “None of” 是说集合里面的字符是的补集。


2.2
次数字符
上面的模式有个致命短板,就是只能匹配一个字符!这在实际应用几乎没用,因此我们需要某些带有“次数功能”的元字符,如下:
- 贪婪模式:
- * 表示后面可跟 0 个或多个字符
- + 表示后面可跟 1 个或多个字符
- ? 表示后面可跟 0 个或 1 个字符
- 非贪婪模式:
- *? 表示后面可跟 0 个或多个字符,但只取第一个
- +? 表示后面可跟 1 个或多个字符,但只取第一个
- ?? 表示后面可跟 0 个或 1 个字符,但只取第一个
贪婪模式和非贪婪模式的区别在下面讲 ? 的时候会介绍。
星号(asterisk)- *
首先定义“字符 u 可以出现 0 次或多次”的模式,结果不需要解释。
pat = r'colou*r'
print( look_for(pat, 'color') ) print( look_for(pat, 'colour') ) print( look_for(pat, 'colouuuuuur') )
['color'] ['colour'] ['colouuuuuur']
模式 colou*r 的可视图如下。

注意 u 附近有三条通路
- 上路跳过 u,代表零个 u
- 中路穿越 u,代表一个 u
- 下路循环 u,代表多个 u
加号(plus sign)- +
首先定义“字符 u 可以出现 1 次或多次”的模式,结果不需要解释。
pat = r'colou+r'
print( look_for(pat, 'color') ) print( look_for(pat, 'colour') ) print( look_for(pat, 'colouuuuuur') )
没有找到 ['colour'] ['colouuuuuur']
模式 colou+r 的可视图如下。

注意 u 附近有两条通路
- 中路穿越 u,代表一个 u
- 下路循环 u,代表多个 u
问号(question mark)- ?
首先定义“字符 u 可以出现 0 次或 1 次”的模式,结果不需要解释。
pat = r'colou?r'
print( look_for(pat, 'color') ) print( look_for(pat, 'colour') ) print( look_for(pat, 'colouuuuuur') )
['color'] ['colour'] 没有找到
模式 colou+r 的可视图如下。

注意 u 附近有两条通路
- 上路跳过 u,代表零个 u
- 中路穿越 u,代表一个 u
有的时候一个句子里会有重复的字符,假如是 > 字符,如果我们要匹配这个>,到底在哪一个 > 就停止了呢?
这个就是贪心(greedy)模式和非贪心(non-greedy)模式的区别,让我们来看个例子。
heading = r'<h1>TITLE</h1>'
如果模式是 <.+>,那么我们要获取的就是以 < 开头,以 > 结尾,中间有 1 个或多个字符的字符串。这里我们先提前介绍 . 字符,它是一个通配符,可以代表任何除新行 (n) 的字符。
pat = r'<.+>' print( look_for(pat, heading) )
['<h1>TITLE</h1>']
结果如上,获取的字符串确实以 < 开头,以 > 结尾,但是仔细看下,其实在 heading[3] 出也是 >,为什么没有匹配到它而是匹配到最后一个 > 呢?
原因就是上面用了贪婪模式,即在整个表达式匹配成功的前提下,尽可能多的匹配。那么其对立的非贪婪模式,就是在整个表达式匹配成功的前提下,尽可能少的匹配。
实现非贪婪模式只需在最后加一个 ? 字符,代码如下:
pat = r'<.+?>' print( look_for(pat, heading) )
['<h1>', '</h1>']
结果无需解释。
有意思的是,模式 <.+> 和 <.+?> 的可视化图长得一样,如下。这样我们就无法从图上分辨是否使用贪婪或非贪婪的模式了,只能从代码中识别了。

大括号(curly bracket)- {}
有的时候我们非常明确要匹配的字符出现几次,比如
- 中国的手机号位数是 13 位,n = 13
- 密码需要 8 位以上,n ≥ 8
- 公众号文章标题长度不能超过 64,n ≤ 64
- 用户名需要在 8 到 16 位之间,8 ≤ n ≤ 16
这时我们可以设定具体的上界或(和)下界,使得代码更加有效也更好读懂,规则如下:
- {n} 左边的字符串是否出现 n 次
- {n, } 左边的字符串是否出现大于等于 n 次
- {, n} 左边的字符串是否出现小于等于 n 次
- {n, m} 左边的字符串是否出现在 n 次和 m 次之间
用规则来看例子,很容易看懂。
s = 'a11bbb2222ccccc'
print( look_for(r'[a-z]{1}', s) ) print( look_for(r'[0-9]{2,}', s) ) print( look_for(r'[a-z]{,5}', s) ) print( look_for(r'[0-9]{2,4}', s) )
['a', 'b', 'b', 'b', 'c', 'c', 'c', 'c', 'c'] ['11', '2222'] ['a', '', '', 'bbb', '', '', '', '', 'ccccc', ''] ['11', '2222']
需要解释的是例三,匹配五个以下的 a 到 z 小写字母,当然也包括零个,因此结果包含那些空字符。
模式 {n}, { ,n}, {n, } 和 {n, m} 的可视图如下:

上面都是贪婪模式,当然也有其对应的非贪婪模式,但只有 {n, m}? 有意义,原因自己想。上面的模式对于前一个字符重复 m 到 n 次,并且取尽可能少的情况。比如在字符串'sssss'中,s{2,4} 会匹配 4 个 s,但 s{2,4}? 只匹配 2 个 s。
2.3
并列字符
字符集合问题解决了,字符次数问题解决了,如果现在面临的问题着是匹配 A 或 B 其中一个呢?用垂线 | 字符,A|B,如果 A 匹配了,则不再查找 B,反之亦然。
垂线(vertical line)- |
首先定义“句子出现 like 或 love 一词”的模式。
pat = r'like|love'
print( look_for(pat, 'like you') ) print( look_for(pat, 'love you') )
['like'] ['love']
模式 like|love 的可视图如下,其并列模式体现在黑点到白点的并行通路上。

2.4
提取字符
如果你想把匹配的内容提取出来,用小括号,而在小括号里面你可以设计任意正则表达式。
小括号(square bracket)- ()
首先定义“beat 的第三人称,过去式,过去分词和现在进行式”的模式,为了获取 beat 加正确后缀的所有单词。
pat = r'beat(s|ed|en|ing)'
print( look_for(pat, 'beats') ) print( look_for(pat, 'beated') ) print( look_for(pat, 'beaten') ) print( look_for(pat, 'beating') )
['s'] ['ed'] ['en'] ['ing']
我们将出现在 () 里面的后缀都获取出来了,其可视图如下,我们发现“Group 1”代表 () 起的作用。

但其实这不是我们想要的,我们想把带着后缀的 beat 给获取出来。那么只有在最外面再加一层 (),模式如下。
pat = r'(beat(s|ed|en|ing))'
print( look_for(pat, 'beats') ) print( look_for(pat, 'beated') ) print( look_for(pat, 'beaten') ) print( look_for(pat, 'beating') )
[('beats', 's')] [('beated', 'ed')] [('beaten', 'en')] [('beating', 'ing')]
其可视图如下,我们发现 Group 2 嵌套在 Group 1 里面。

现在带着后缀的 beat 已经获取出来了,上面列表中每个元组的第一个元素,但完美主义者不想要后缀(即元组的第二个元素),可以用下面的骚模式。
在 () 中最前面加入 ?:。(?:) 代表只匹配不获取(non-capturing),结果看上去非常自然。
pat = r'(beat(?:s|ed|en|ing))'
print( look_for(pat, 'beats') ) print( look_for(pat, 'beated') ) print( look_for(pat, 'beaten') ) print( look_for(pat, 'beating') )
['beats'] ['beated'] ['beaten'] ['beating']
其可视图如下,我们发现只有一个 Group 1,那个内括号,代表不被获取的内容,没有体现在下图中。

2.5
转义字符
字符集合问题解决了,字符次数问题解决了,字符并列问题解决了,字符获取问题解决了,看上去我们能做很多事了。别急,RE 给你最后一击,转义字符,让模式更加强大。
转义字符,顾名思义,就是能转换自身含义的字符。
点 . 不再是点,美元 $ 不再是美元,等等等等。。。
点(dot)- .
点 . 表示除新行(newline)的任意字符,它是个通配符。用它最无脑简便,但是代码也最难读懂,效率也最低下。
定义“含有 1 个或多个非新行字符”的模式。
pat = r'.+'
print( look_for(pat, 'a') ) print( look_for(pat, 'b1') ) print( look_for(pat, 'C@9') ) print( look_for(pat, '$ 9_fZ') ) print( look_for(pat, '9z_trn') )
['a'] ['b1'] ['C@9'] ['$ 9_fZ'] ['9z_tr']
除了最后例子中的 n 没有匹配到,其他的字符全部匹配出来。
托字符(carat)- ^
托字符 ^ 表示字符串开头。
定义“以 s 开头字符串”的模式。
pat = r'^s[w]*'
print( look_for(pat, 'son') ) print( look_for(pat, 'shot') ) print( look_for(pat, 'come') )
['son'] ['shot'] 没有找到
结果太明显,不解释。
美元符(dollar sign)- $
美元符 $ 表示字符串结尾。
定义“以 s 结尾字符串”的模式。
pat = r'[w]*s$'
print( look_for(pat, 'yes') ) print( look_for(pat, 'mess') ) print( look_for(pat, 'come') )
['yes'] ['mess'] 没有找到
结果太明显,不解释。
反斜杠(backslash)-
更厉害的是,反斜杠 可对特殊字符进行转义,也可对普通字符转义。
- 将特殊字符转成自身含义:用 作用在 ^ . 等身上,代表乘方 ^、小数点 . 和除号 \
- 将自身字符转成特殊含义:用 作用在 w d n 等身上,代表字母 w、数字 d 和新行 n
特殊 –> 自身
在反斜杠的限制下,$ 终于代表美元了!
pat = r'$[0-9.]+'
print( look_for(pat, 'it costs $99.99') )
['$99.99']
在反斜杠的限制下, ^ . 终于代表乘方、小数点和除号了!
pat = r'(\|/|^|.)'
print( look_for(pat, '(5/2)^2=6.25') )
['/', '^', '.']
没有了反斜杠的限制,一切乱了套,点 . 就是通配符,可以匹配字符串里所有字符。
pat = r'(|/|^|.)'
print( look_for(pat, '(5/2)^2=6.25') )
['', '(', '5', '/', '2', ')', '^', '2', '=', '6', '.', '2', '5']
但如果在中括号 [] 集合里,每个字符就是它本身的意义,点就是点,而不是通配符。
pat = r'[/^.]'
print( look_for(pat, '(5/2)^2=6.25') )
['/', '^', '.']
自身 –> 特殊
规则总结如下(大写和小写互补,两者加一起是全集):
- b:匹配空字符串,但仅适用于单词的“首尾”
- B:匹配空字符串,但仅适用于单词的“非首尾”
- d:匹配任何“数字”字符,等价于 [0-9]
- D:匹配任何“非数字”字符,等价于 [^0-9]
- s:匹配任何“空白”字符,等价于 [tnr]
- S:匹配任何“非空白”字符,等价于 [^tnr]
- w:匹配任何“字母数字下划线”字符,等价于 [a-zA-Z0-9_]
- W:匹配任何“非字母数字下划线”字符,等价于 [^a-zA-Z0-9_]
- A:匹配句子的“开头”字符,等价于 ^
- Z:匹配句子的“结尾”字符,等价于 $
- t:匹配句子的“制表键 (tab)”字符
- r:匹配句子的“回车键 (return)”字符
- n:匹配句子的“换行键 (newline)”字符
b B
pat = r'blearnb' print( look_for(pat, 'learn Python') ) print( look_for(pat, 'relearn Python') ) print( look_for(pat, 'learning Python') ) print( look_for(pat, 'relearning Python') )
['learn'] 没有找到 没有找到 没有找到
b 只能抓住 learn 前后的首尾空字符,那么只能匹配不带前缀和后缀的 learn, 即 learn 本身。
pat = r'BlearnB' print( look_for(pat, 'learn Python') ) print( look_for(pat, 'relearn Python') ) print( look_for(pat, 'learning Python') ) print( look_for(pat, 'relearning Python') )
没有找到 没有找到 没有找到 ['learn']
B 只能抓住 learn 前后的非首尾空字符,那么只能匹配带前缀和后缀的 learn,即 relearning。
pat = r'blearnB' print( look_for(pat, 'learn Python') ) print( look_for(pat, 'relearn Python') ) print( look_for(pat, 'learning Python') ) print( look_for(pat, 'relearning Python') )
没有找到 没有找到 ['learn'] 没有找到
learn 前 b 后 B,只能匹配带后缀的 learn,即 learning。
pat = r'Blearnb' print( look_for(pat, 'learn Python') ) print( look_for(pat, 'relearn Python') ) print( look_for(pat, 'learning Python') ) print( look_for(pat, 'relearning Python') )
没有找到 ['learn'] 没有找到 没有找到
learn 前 B 后 b,只能匹配带前缀的 learn,即 relearn。
d D
pat = r'd+' print( look_for(pat, '12+ab34-cd56*ef78/gh90%ij') )
['12', '34', '56', '78', '90']
匹配数字,不解释。
pat = r'D+' print( look_for(pat, '12+ab34-cd56*ef78/gh90%ij') )
['+ab', '-cd', '*ef', '/gh', '%ij']
匹配非数字,不解释。
s S
pat = r's+' s = '''please don't leave me alone''' print( look_for(pat, s) )
[' ', 'n', ' ', 'n ']
匹配各种空格比如制表、回车或新行,不解释。
pat = r'S+' print( look_for(pat, s) )
['please', "don't", 'leave', 'me', 'alone']
匹配各种非空格,不解释。
w W
pat = r'w+' print( look_for(pat, '12+ab_34-cd56_ef78') )
['12', 'ab_34', 'cd56_ef78']
匹配字母数字下划线,不解释。
pat = r'W+' print( look_for(pat, '12+ab_34-cd56_ef78') )
['+', '-']
匹配非字母数字下划线,不解释。
A Z
pat1 = r'^y[w]*' pat2 = r'Ay[w]*' str1 = 'you rock' str2 = 'rock you' print( look_for(pat1, str1) ) print( look_for(pat2, str1) ) print( look_for(pat1, str2) ) print( look_for(pat2, str2) )
['you'] ['you'] 没有找到 没有找到
匹配开头字符,A 和 ^ 等价,不解释。
pat1 = r'[w]*k$' pat2 = r'[w]*kZ' str1 = 'you rock' str2 = 'rock you' print( look_for(pat1, str1) ) print( look_for(pat2, str1) ) print( look_for(pat1, str2) ) print( look_for(pat2, str2) )
['rock'] ['rock'] 没有找到 没有找到
匹配结尾字符,Z 和 $ 等价,不解释。
2
常用函数
了解完上节介绍的元字符的基本知识,本节的函数运用就很简单了。RE 包里常见的函数总结如下:
- match(pat, str):检查字符串的开头是否符合某个模式
- search(pat, str):检查字符串中是否符合某个模式
- findall(pat, str):返回所有符合某个模式的字符串,以列表形式输出
- finditer(pat, str):返回所有符合某个模式的字符串,以迭代器形式输出
- split(pat, str):以某个模式为分割点,拆分整个句子为一系列字符串,以列表形式输出
- sub(pat, repl, str):句子 str 中找到匹配正则表达式模式的所有子字符串,用另一个字符串 repl 进行替换
- compile(pat):将某个模式编译成对象,供之后使用
match(pat, str)
判断模式是否在字符串开头位置匹配。如果匹配,返回对象,如果不匹配,返回 None。
s = 'Kobe Bryant' print( re.match(r'Kobe', s) ) print( re.match(r'Kobe', s).group() ) print( re.match(r'Bryant', s) )
<re.Match object; span=(0, 4), match='Kobe'> Kobe None
该函数返回的是个对象(包括匹配的子字符串和在句中的位置索引),如果只需要子字符串,需要用 group() 函数。
由于值匹配句头,那么句中的 Bryant 无法被匹配到。
search(pat, str)
在字符串中查找匹配正则表达式模式的位置。如果匹配,返回对象,如果不匹配,返回 None。
s = 'Kobe Bryant' print( re.search(r'Kobe', s) ) print( re.search(r'Kobe', s).group() ) print( re.search(r'Bryant', s) ) print( re.search(r'Bryant', s).group() )
<re.Match object; span=(0, 4), match='Kobe'> Kobe <re.Match object; span=(5, 11), match='Bryant'> Bryant
该函数返回的是个对象(包括匹配的子字符串和在句中的位置索引),如果只需要子字符串,需要用 group() 函数。
如果句子出现两个 Bryant 呢?
s = 'Kobe Bryant loves Gianna Bryant' print( re.search(r'Bryant', s) ) print( re.search(r'Bryant', s).group() ) print( re.search(r'Bryant', s) )
<re.Match object; span=(5, 11), match='Bryant'> Bryant <re.Match object; span=(5, 11), match='Bryant'>
根据结果只匹配出第一个,我们需要下面的函数来匹配全部。
findall(pat, str)
在字符串中找到正则表达式所匹配的所有子串,并组成一个列表返回。
s = 'Kobe Bryant loves Gianna Bryant' print( re.findall(r'Kobe', s) ) print( re.findall(r'Bryant', s) ) print( re.findall(r'Gigi', s) )
['Kobe'] ['Bryant', 'Bryant'] []
结果不解释。
finditer(pat, str)
和 findall 类似,在字符串中找到正则表达式所匹配的所有子串,并组成一个迭代器返回。
s = 'Kobe Bryant loves Gianna Bryant' print( [i for i in re.finditer(r'Kobe', s)] ) print( [i for i in re.finditer(r'Bryant', s)] ) print( [i for i in re.finditer(r'Gigi', s)] )
[<re.Match object; span=(0, 4), match='Kobe'>] [<re.Match object; span=(5, 11), match='Bryant'>, <re.Match object; span=(25, 31), match='Bryant'>] []
如果需要匹配子串在原句中的位置索引,用 finditer,此外用 findall。
split(pat, str)
将字符串匹配正则表达式的部拆分开并返回一个列表。
s = 'Kobe Bryant loves Gianna Bryant' print( re.split(r's', s) )
['Kobe', 'Bryant', 'loves', 'Gianna', 'Bryant']
按空格拆分,不解释。
sub(pat, repl, str)
句子 str 中找到匹配正则表达式模式的所有子字符串,用另一个字符串 repl 进行替换。如果没有找到匹配模式的串,则返回未被修改的句子 str,其中 repl 既可以是字符串也可以是一个函数。
s = 'Kobe Bryant loves Gianna Bryant' print( re.sub(r's', '-', s) )
Kobe-Bryant-loves-Gianna-Bryant
用 – 代替空格。
print( re.sub(r'Gianna', 'Gigi', s) )
Kobe Bryant loves Gigi Bryant
用 Gigi 代替 Gianna。
print( re.sub(r'd+', '_', s) )
Kobe Bryant loves Gianna Bryant
用 _ 代替数字(一个或多个),但句中没有数字,因此没用替代动作。
print( re.sub(r'd*', '_', s)
_K_o_b_e_ _B_r_y_a_n_t_ _l_o_v_e_s_ _G_i_a_n_n_a_ _B_r_y_a_n_t_
用 _ 代替数字(零个或多个),虽然句中没有数字,但是零个数字就是空字符,因此 _ 替代所有空字符。好玩吧 🙂
compile(pat)
把正则表达式的模式转化成正则表达式对象,供其他函数如match 和 search 使用。对象创建出来可以循环使用,如果某种模式要重复使用话,用“先 compile 再 findall”的方式更加高效。
用处理电邮地址来举例。
email = '''Shengyuan Personal: [email protected] Shengyuan Work: [email protected] Shengyuan School: [email protected] Obama: [email protected]''' print(email)
Shengyuan Personal: [email protected] Shengyuan Work: [email protected] Shengyuan School: [email protected] Obama: [email protected]
创建电邮的模式 r'[w.-]+@[w.-]+',用 compile 先创建 RE 对象,供之后使用。
pat = r'[w.-]+@[w.-]+' obj = re.compile(pat) obj
re.compile(r'[w.-]+@[w.-]+', re.UNICODE)
在对象 obj 上分别使用 match, search, findall, findieter 等方法,结果如下:
print( obj.match(email), 'n') print( obj.search(email), 'n' ) print( obj.findall(email), 'n' ) print( [i for i in obj.finditer(email)])
None <re.Match object; span=(20, 41), match='[email protected]'> ['[email protected]', '[email protected]', '[email protected]', '[email protected]'] [<re.Match object; span=(20, 41), match='[email protected]'>, <re.Match object; span=(58, 88), match='[email protected]'>, <re.Match object; span=(107, 126), match='[email protected]'>, <re.Match object; span=(134, 161), match='[email protected]'>]
在对象 obj 上还可使用 sub 方法,结果如下:
print( obj.sub('[email protected]', email), 'n' )
Shengyuan Personal: [email protected] Shengyuan Work: [email protected] Shengyuan School: [email protected] Obama: [email protected]
在对象 obj 上还可使用 split 方法,即把 @ 前后的子串拆分出来,结果如下:
for addr in obj.findall(email): print( re.split(r'@', addr))
['quantsteven', 'gmail.com'] ['shengyuan', 'octagon-advisors.com'] ['g0700508', 'nus.edu.sg'] ['barack.obama', 'whitehouse.gov']
我们还可以再创建个 RE 对象 obj1,专门用来做拆分。
obj1 = re.compile(r'@') for addr in obj.findall(email): print( obj1.split(addr))
['quantsteven', 'gmail.com'] ['shengyuan', 'octagon-advisors.com'] ['g0700508', 'nus.edu.sg'] ['barack.obama', 'whitehouse.gov']
3
示例展示
3.1
密码例子
密码通常有如下要求。
- 最少 8 个最多 16 个字符.
- 至少含有一个大写字母,一个小写字母,一个数字
- 至少含有一个特殊字符 @ $ ! % * ? & _,但不包括空格
根据上面要求创建模式,相信都可以读懂了吧。
pat = r'^[0-9a-zA-Z@!$#%_-]{8,16}$' print( look_for(pat, 'stevenwang') ) print( look_for(pat, '19831031') ) print( look_for(pat, 'steven1031') ) print( look_for(pat, 'steven@1031') ) print( look_for(pat, 'Steven@1031') ) print( look_for(pat, 's1031') ) print( look_for(pat, 's@1031') ) print( look_for(pat, 'stevenwang19831031') ) print( look_for(pat, 'stevenwang@19831031') )
['stevenwang'] ['19831031'] ['steven1031'] ['steven@1031'] ['Steven@1031'] 没有找到 没有找到 没有找到 没有找到
结果好像不太对,因为密码必须要含有数字,大小写和特殊字符。
这时候需要用 (?=…) 这个骚操作,意思就是匹配 ’…’ 之前的字符串。在本例中 '…' 包括小写 [a-z],大写 [A-Z],数字 d,特殊字符 [@$!%*?&_],言下之义就是上面这些必须包含中密码中。
pat = r'^(?=.*[a-z]) (?=.*[A-Z]) (?=.*d) (?=.*[$@$!%*?&_]) [A-Za-zd$@$!%*?&_]{8,16}$' print( look_for(pat, 'stevenwang') ) print( look_for(pat, '19831031') ) print( look_for(pat, 'steven1031') ) print( look_for(pat, 'steven@1031') ) print( look_for(pat, 'Steven@1031') ) print( look_for(pat, 's1031') ) print( look_for(pat, 's@1031') ) print( look_for(pat, 'stevenwang19831031') ) print( look_for(pat, 'stevenwang@19831031') )
没有找到 没有找到 没有找到 没有找到 ['Steven@1031'] 没有找到 没有找到 没有找到 没有找到
结果完全正确。
上面模式的可视图如下:

3.2
邮箱例子
首先定义邮箱地址的模式 'S+@S+',还记得 S 是非空格字符,基本代表了所需的字符要求。我们想从从 email.txt 文本中筛选出所有邮箱信息。
pat = r'S+@S+' obj = re.compile(pat) email_list = [] hand = open('email.txt') for line in hand: line = line.rstrip() email_addr = obj.findall(line) if len(email_addr) > 0: email_list.append(email_addr[0]) list(set(email_list))

咋一看结果是对的,但细看(高亮处)有些邮箱地址包含了 <> 的符号,或者根本不是正常的邮箱地址,比如 apache@localhost。
这时候我们需要在模式中添加更多规则,如下。
- '[a-zA-Zd]S+ 代表第一字符要是数字或字母
- w+.[a-z]{2,3} 代表 A.B 这样的结构,其中 A 由若干字母数字下划线组成,而 B 由 2 或 3 个小写字母组成(因为通常邮箱最后就是 com, net, gov, edu 等等)。
pat = r'[a-zA-Zd]S+@w+.[a-z]{2,3}'

结果正常。
3.3
摘要例子
在下面摘要中获取人物、买卖动作、股票数量、股票代号、日期和股价这些关键信息。
news = """ Jack Black sold 15,000 shares in AMZN on 2019-03-06 at a price of $1044.00. David V.Love bought 811 shares in TLSA on 2020-01-19 at a price of $868.75. Steven exercised 262 shares in AAPL on 2020-02-04 at a price of $301.00. """
给大家留个任务,读懂下面代码,看懂了本帖知识就掌握了。我相信能看到这里的都可以看懂。
pat = r'([a-zA-Z. ]*)' 's(sold|bought|exercised)' 's*([d,]+)' '.*ins([A-Z]{,5})' '.*(d{4}-d{2}-d{2})' '.*price ofs($d*.d*)' re.findall( pat, news )
[('Jack Black', 'sold', '15,000', 'AMZN', '2019-03-06', '$1044.00'), ('David V.Love', 'bought', '811', 'TLSA', '2020-01-19', '$868.75'), ('Steven', 'exercised', '262', 'AAPL', '2020-02-04', '$301.00')]
上面模式的可视图如下:

4
总结
累死了,这次真不想总结了。
记住元字符的用处:集合、次数、并列、获取和转义。
记住函数的用法:先 compile 成 RE 对象,在 findall 或 finditer,由你想查看的信息完整度决定。
下帖我会来个关于 RE 的实际案例分析,并记录我在操作时遇到的问题和解决方案。
Stay Tuned!