文章目录
正则一词,在汉语词典中被解释为“正其礼仪法则”,和正则表达式似乎毫不相干。幸好汉字可以望文生义,将正则理解为合乎标准的规则,似乎也很贴切。所谓正则表达式,就是用事先定义好的一些特定字符以及这些特定字符的组合,组成一个“规则字符串”,用来表达对字符串的一种过滤逻辑。正则表达式的英文写作Regular Expression,简写为RE。
正则表达式的规则之艰深晦涩,足令初学者望而却步。其实,只要理解了基本概念,稍微归纳一下知识点,掌握并熟练应用正则表达式,也不是什么难事儿,大约三十分钟就可以做到。我们可以把正则表达式的学习分成两部分:
- 如何写正则表达式?
- 怎么用正则表达式?
第一个问题和语言无关,需要了解正则表达式的字符集和特殊符号集,以及几条规则;第二个问题,就是Python内置的正则表达式标准模块re的用法。
1. 正则表达式的写法
写正则表达式,就是用规则描述你想找到的字符串的特征。比如,下面的写法描述了由小写字母组成的、长度为3到8位的字符串。
>>> pstr = r'[a-z]{3,8}'
这是一个原生字符串(字符串前标r,不会对反斜杠做特殊处理),方括号内约定了允许使用的字符集,花括号约定了字符的最少位数和最多位数。类似的,正则表达式定义了若干符号用来描述正则表达式的字符集和组合规则。下表列出了这些常用的符号和含义。
符号 | 说明 |
---|---|
. | 匹配除换行符\n外的任意字符 |
\ | 转义字符,使后一个字符改变原来的意思。例如\n,把字母n变成了换行符 |
* | 匹配0次或多次,等效于 {0,} |
+ | 匹配1次或多次,等效于 {1,} |
? | 匹配0次或1次,等效于 {0,1} |
^ | 匹配字符串开头。在多行模式中匹配每一行的开头 |
$ | 匹配字符串末尾,在多行模式中匹配每一行的末尾 |
| | 匹配被分割的表达式中的任意一个,从左到右匹配 |
{ } | {m}表示匹配m次,{m,n}表示匹配至少m次,至n多次,{m,}表示匹配至少m次 |
[ ] | 匹配字符集,字符集内^表示取反 |
( ) | 子表达式,可以后接数量词 |
\d | 匹配数字字符集,等效于[0-9] |
\D | 匹配非数字字符集,等效于[^0-9] |
\s | 匹配包括空格、制表位、回车、换行等在内的空白字符集 |
\S | 匹配非空字符集 |
\w | 匹配包括下划线在内的数字和字母,等效于[A-Za-z0-9_] |
\W | 匹配包括下划线在内的数字和字母以外的字符集,等效于[^A-Za-z0-9_] |
了解了这些符号的含义和用法,就可以用它们来描述任意复杂的字符串了。下面是几个稍微复杂一点的正则表达式,初学者也应该很容易理解。
>>> pstr = r'[1-9]\d{2}' # 100到999的数字
>>> pstr = r'公元([1-9]\d*)年' # 公元和年之间是不以0开头的数字,数字被指定为子表达式
>>> pstr = r'color=(red|blue)' # color=red,或者color=blue,颜色被指定为子表达式
2. 正则表达式的用法
尽管用原生字符串来表示一个正则表达式,在使用时没有任何问题,但我们更习惯把原生字符串表示的正则表达式用re.compile()编译成一个模式对象,这样就可以直接使用模式对象的各种方法了。
>>> import re
>>> pstr = r'公元([1-9]\d*)年'
>>> p = re.compile(pstr) #
有了正则表达式的模式对象,接下来,还要弄明白我们要做什么。听起来有些滑稽,但这的确是一个问题:我们很多时候真的并不清楚自己想要作什么。通常,使用正则表示式不外乎这样几个目的。
- 验证一个字符串是否符合正则表达式约定的规则
- 从一个字符串中找到符合正则表达式约定规则的子串
- 从一个字符串中找出所有符合正则表达式约定规则的子串
- 一个字符串若存在符合正则表达式约定规则的子串,则用这个子串分割字符串
- 替换一个字符串中所有符合正则表达式约定规则的子串
针对这五个功能需求,模式对象提供了match()、search()、findall()、split()、sub()等五个方法,与之一一对应。
2.1 模式匹配:match()
模式匹配是从字符串的起始位置开始的,只要起始位置字符不同,匹配即失败。而字符串如果长于模式串,则不会影响匹配结果。
>>> s1 = '公元2020年'
>>> s2 = '公元2020年以后'
>>> s3 = '自公元2020年以来'
>>> p = re.compile(r'公元([1-9]\d*)年')
>>> result = p.match(s1) # 匹配s1
>>> print(result) # 匹配成功
<re.Match object; span=(0, 7), match='公元2020年'>
>>> result.group() # group()方法返回匹配到的字符串
'公元2020年'
>>> result.groups()# groups()方法返回子表达式匹配到的字符串
('2020',)
>>> result = p.match(s2) # 匹配成功,虽然s2末尾多了两个字
>>> print(result)
<re.Match object; span=(0, 7), match='公元2020年'>
>>> result = p.match(s3) # 匹配失败,因为s3开头多了一个字
>>> print(result)
None
如果要求字符串完全匹配,则需要在正则表达式最后加上$,表示期待被匹配的字符串结尾和模式串一致。
>>> p = re.compile(r'公元([1-9]\d*)年$')
>>> result = p.match(s2) # 模式串加上$后,末尾多了两个字的s2匹配失败
>>> print(result)
None
2.2 模式搜索:search()
模式对象的search()方法会在字符串内查找匹配的字符串,找到第一个匹配即返回;如果字符串没有匹配,则返回None。
>>> p = re.compile(r'公元([1-9]\d*)年')
>>> result = p.search(s1)
>>> print(result) # s1存在匹配的子串
<re.Match object; span=(0, 7), match='公元2020年'>
>>> result.group()
'公元2020年'
>>> result.groups()
('2020',)
>>> print(p.search(s2)) # s2存在匹配的子串
<re.Match object; span=(0, 7), match='公元2020年'>
>>> print(p.search(s3)) # s3存在匹配的子串
<re.Match object; span=(1, 8), match='公元2020年'>
>>> p = re.compile(r'^公元([1-9]\d*)年$') # 若指定以模式串开头和结尾
>>> print(p.search(s2)) # s2不存在匹配的子串
None
>>> print(p.search(s3)) # s3不存在匹配的子串
None
2.3 匹配所有:findall()
模式对象的findall()方法以列表形式返回字符串中所有匹配的子串。另外,模式对象的finditer()和findall()类似,只不过返回的是一个迭代器。以下面这一段多行文本为例,我们需要从中解析出5行11列数值数据。
>>> txt = """
WDC for Geomagnetism, Kyoto
Hourly Equatorial Dst Values (REAL-TIME)
MARCH 2020
DAY 1 2 3 4 5 6 7 8 9 10
1 -19 -11 -10 -7 -8 -9 -11 -14 -15 -9
2 -12 -14 -14 -16 -15 -14 -12 -11 -12 -10
3 1 -3 -10 -9 -9 -9 -10 -10 -13 -9
4 -6 -3 -1 -2 -2 -3 -2 -3 -6 -3
5 1 3 3 3 0 -3 -2 -2 -1 2
"""
分析可知,每一个数据之前都有至少一个空格,除了第1列为正数,其他列数据有正有负。因为解析以行为单位逐行处理,所以需要指定模式编译装饰参数为re.M,且模式字符串首尾需要加上^和$符号。据此,可以很容易写出正则表达式,并解析出全部数据。
>>> p = re.compile('^\s([1-5])\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$', flags=re.M)
>>> result = p.findall(txt)
>>> result
[('1', '-19', '-11', '-10', '-7', '-8', '-9', '-11', '-14', '-15', '-9'), ('2', '-12', '-14', '-14', '-16', '-15', '-14', '-12', '-11', '-12', '-10'), ('3', '1', '-3', '-10', '-9', '-9', '-9', '-10', '-10', '-13', '-9'), ('4', '-6', '-3', '-1', '-2', '-2', '-3', '-2', '-3', '-6', '-3'), ('5', '1', '3', '3', '3', '0', '-3', '-2', '-2', '-1', '2')]
2.4 子串分割:split()
模式对象的split()方法返回用匹配的子串分割字符串后得到的列表,若无匹配子串,则返回空列表。下面的例子,演示了用标点符号分割字符串。不用正则表达式的话,很难实现这个功能。
>>> s = '无论,还是。或者?都是分隔符'
>>> p = re.compile(r'[,。?]')
>>> p.split(s)
['无论', '还是', '或者', '都是分隔符']
2.5 子串替换:sub()
模式对象的sub()方法返回用指定内容替换匹配子串后的字符串。下面的代码,演示了用加号(+)替换所有的标点符号。当然,这里的标点符号集只列出了3个元素。
>>> s = '无论,还是。或者?都是分隔符'
>>> p = re.compile('[,。?]')
>>> p.sub('+', s)
'无论+还是+或者+都是分隔符'