正则表达式入门

Regular Expression is all about the patterns within string, which forms a language of logic.

1. 读者须知

这篇文章只能介绍以下入门级知识:

  • 正则表达式的思维方式
  • 正则表达式的相关概念
  • 如何写出正确的正则表达式
  • re — Python Build-in Module

这篇文章不能为你介绍正则表达式的历史或演进、底层编译逻辑等内容。

2. 简介

正则表达式(Regular Expression, regex)可以认为一种小型的计算机语言,用于表达某种字符(串)模式。我个人第一次学习的时候,感觉到它要求使用者有严格的逻辑。希望读者在文章的最后能感觉到,这篇文章使用了正则表达式的逻辑来介绍正则表达式。

我觉得正则表达式是一种必需品。每一个代码家都应该学会它,至少有所了解,并能写出简单的正则表达式匹配某些常见模式。说它必不可少,是因为在和字符串打交道的时候,固定的函数几乎不可能满足我们的“处理刚需”。

举个例子,找出电话本中所有名字含有“叶”的人名。它可能是姓“叶”,或者第二个字、第三个字是“叶”。这种情况比较简单,由于中文名字的长度非常有限,代码多啰嗦几行,便能完成任务。

更符合人类思维方式的做法是:直接显式地指明这个模式(pattern)——“叶”字前后均有可能有字符(串)的人名。比如叶青,麻仓叶等。极端的例子是:叶叶叶叶。

Where there is a pattern, there is a regex.

3. regex相关概念

3.1 字符集

正则表达式中的主体是:字符集 + 数量词。字符集用于表明哪些字符会参与模式匹配,数量词表示匹配的次数。在上面的例子中,“叶”是我们唯一需要匹配的字符,而它在人名(被匹配的字符串文本)中几乎只会出现1次。所以数量词的限制也是处理过程中的刚需。也就是说,我们不希望“叶叶”或“叶叶叶”出现在最后的结果中。

[….]表示一个用于匹配的自定义字符集,意味着对应位置可以是字符集中的任一字符。a[bcd]e代表了abe,ace,ade三种可能,也可以写成a[b-d]e

字符集中,我们可以使用范围来表示一堆字符。

  • [a-z]表示所有小写字母,
  • [0-9]表示数字字符集[0123456789]

范围可以堆砌。比如使用[a-zA-Z0-9]表示所有的大小写字母以及数字。而常用的字符集被预先定义好了,优秀的程序猿都是很贴心的。

预定义字符集 含义 等价字符集
\d 数字 [0-9]
\D 非数字 [^\d]
\s 各种空白字符 [ \t\r\n\f\v]
\S 非空白字符 [^\s]
\w 单词字符 [A-Za-z0-9_]
\W 非单词字符 [^\w]

观察上表,^放在字符集的最前表示取反。大写的均为反义。建议背下来。

3.2 数量词

上文已经提到“叶叶叶叶”,暗示正则表达式强大之处在于匹配不定长的字符串。定制这个需求依赖于数量词

数量词 含义
* 匹配前一个字符0或无限次
+ 匹配前一个字符1或无限次
? 匹配前一个字符0或1次
{m} 匹配前一个字符m次
{m,n} 匹配前一个字符m到n次

举例:姓“叶”的或姓“王”的,写成[叶王]{1}。不适合写成[叶王]?,因为可能是0次出现。这是危险的。假设有叶子,叶问,叶儿,王儿,海儿,并想找出姓“叶”或“王”,名为“儿”的所有名字,写成[叶王]{1}儿是正确的,但是[叶王]?儿则会多出一个“海儿”的结果。

到此,读者应该体会到正则表达式是一种关于逻辑的表达,用于过滤意料之外的情况。如果考虑不周,则可能得到意想不到的结果。更多详细介绍参见文末附表。

3.3 贪婪模式

正则表达式在Python中默认是贪婪的。例如,aa<div>bb</div>cc<div>dd</div>ee中,我们关心<div></div>之间的信息bb和dd。

如果使用<div>.*</div>会返回bb</div>cc<div>dd.(’.’可以匹配任意出换行符之外的字符)。

如果需要关闭贪婪模式,需要在数量词后紧接一个'?',即<div>.*?</div>,此时返回bb。

3.4 转义

与大多数编程语言相同,正则表达式里使用\作为转义字符,这就可能造成反斜杠困扰。

假如你需要匹配文本中的字符\,那么使用编程语言表示的正则表达式里将需要4个反斜杠\\\\:前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。

Python里的原生字符串很好地解决了这个问题,我们可以在字符串前缀r,来显式声明这是一个正则表达式。上面的例子中,正则表达式\\\\等价于r"\\"。同样,匹配一个数字的\\d可以写成r"\d"。有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。

4. re模块简介

4.1 re.compile(pattern, flags=0)

预先将正则表达式编译为RegexObject,如需在循环中多次调用,预编译可以节省时间。

1
2
3
4
pattern = re.compile(some_regex)
result = pattern.match(string)
# 等价于
result = re.match(some_regex, string)

第二个参数flag是匹配模式,取值可以使用按位或运算符’|’表示同时生效,比如re.I | re.M。这里尚未研究。但re.X比较实用,指详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。

1
2
3
4
a = re.compile(r"""\d + # 整数部分
\. # 小数点
\d * # 小数部分""", re.X)
b = re.compile(r"\d+\.\d*")

可以提高表达式可读性,也可以方便coder自行拆分复杂表达式,确保不会犯逻辑错误。

4.2 re.match(pattern, string, flags=0)

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
import re
m = re.match(r'(\w+) (\w+)(?P<sign>.*)', 'hello world!')
print "m.string:", m.string
print "m.re:", m.re
print "m.pos:", m.pos
print "m.endpos:", m.endpos
print "m.lastindex:", m.lastindex
print "m.lastgroup:", m.lastgroup
print "m.group(1,2):", m.group(1, 2)
print "m.groups():", m.groups()
print "m.groupdict():", m.groupdict()
print "m.start(2):", m.start(2)
print "m.end(2):", m.end(2)
print "m.span(2):", m.span(2)
print r"m.expand(r'\2 \1\3'):", m.expand(r'\2 \1\3')
### output ###
# m.string: hello world!
# m.re: <_sre.SRE_Pattern object at 0x016E1A38>
# m.pos: 0
# m.endpos: 12
# m.lastindex: 3
# m.lastgroup: sign
# m.group(1,2): ('hello', 'world')
# m.groups(): ('hello', 'world', '!')
# m.groupdict(): {'sign': '!'}
# m.start(2): 6
# m.end(2): 11
# m.span(2): (6, 11)
# m.expand(r'\2 \1\3'): world hello!

4.3 re.sub(pattern, repl, string, count=0, flags=0)

重要参数的含义依次为:匹配的模式,用于替换的字符串,原字符串。

1
2
3
4
5
6
7
8
9
text = 'aaabbbccc'
text = re.sub(r'aab{3}c?', '123', text)
print text
# text此时为 a123cc . 因为匹配到了aabbbc.
# 可以看到?表示0或1次,这里匹配了1次,也是贪婪的
# 等价于
text = 'aaabbbccc'
pattern = re.compile(r'aab{3}c?')
text = pattern.sub('123', text)

5.正则表达式一图流

6. Acknowledment

  1. Python正则表达式指南

  2. Python正则表达式操作指南

打赏作者一个苹果