正则表达式描述了一种字符串的模式,主要由下列构件组成。
字符和字符集
比如有一个从a到z的字符串,如果要写一个正则表达式来描述,最笨的方法当然是abcdefghijklmnopqrstuvwxyz
。使用字符集可以写成[a-z]
,0到9的自然数可以写成[0-9]
。还有更简便的方式,那就是通过一些\
转义的特殊字符用来表达字符集,比如\d
用来表达数字[0-9]
,非数字是\D
或者[^0-9]
(字符集前的^
用来反转字符集),\w
表示单词字符[a-zA-Z0-9]
,\W
表示非单词字符[^a-zA-Z0-9]
,\s
表示空白[ \t\n\r\f\v]
,\S
表示非空白[^ \t\n\r\f\v]
。还有一个.
是用来表示除了换行符之外的任意字符(.
用在字符集中表示字面意义.
)。最后,这些字符或者特殊字符的含义会受到标记
的影响。例如,a
在添加了IGNORECASE
标记的正则表达式中代表[aA]
,也就是说会忽略大小写,而\d
只有在添加了ASCII
标记时才代表[0-9]
,默认情况下是代表一个Unicode数字,其他的\D\w\W\s\S
特殊字符类似,最后,.
在添加了DOTALL
标记的正则表达式中代表任意字符包括换行符。可以说标记
是正则表达式的另一层转义,也可以理解成工作模式。
量词
量词用来描述一个表达式出现的次数,正常语法是{m,n}
,m是出现的最少次数,n是出现的最多次数。例如\d{1,3}
表示1到3个数字。\d{3}
表示3个数字。还有一些量词助记符,?
表示{0,1}
,*
表示{0,}
(至少0),+
表示{1,}
(至少1)。既然量词是表示了一个范围,需要明确说明到底是表达了上限还是下限。默认情况下,量词是表达了上限,也被称为贪婪匹配或者最大匹配。如果在量词后面加一个?
表示非贪婪匹配或者最小匹配。如\d{1,3}?
,\d+?
。
分组和捕获
在介绍分组前先说选择,aircraft|airplane|jet
表示aircraft或者airplane或者jet。同样也可以这样写,air(craft|plane)|jet
,我们这里有两个外部表达式air(craft|plane)
与jet
,其中第一个外部表达式还包含一个内部表达式craft|plane
。小括号内部的表达式就是一个group,group会默认捕获
匹配到的文本。这些捕获的文本会被保存便于后续使用,所以正则表达式可以用于“提取”文本。如果不想捕获分组内容,则这样写分组air(?:craft|plane)|jet
。那么如何引用被捕获的文本呢,默认情况下整体匹配是0号捕获,然后从左到右遇到一个(
编号加1,不捕获内容的分组不参与编号。例如(\w+)\s\1
,该表达式可以匹配一个单词,之后至少一个空白字符,再之后是与捕获的单词相同的单词(捕获编号0是自动创建的,不需要圆括号,其中存放的是整体匹配的内容)。除了用数字引用分组,还可以给分组命名,同样的匹配重复单词的正则表达式还可以这样写(?P<word>\w+)\s+(?P=word)
断言和标记
断言不会匹配任何文本,而是对断言所在的文本施加某些规定或约束。而标记则可理解成正则表达式的工作模式。
下面是一些断言及其含义:
符号 | 含义 |
---|---|
^ | 在起始处匹配,也可以在带MULTILINE标记的每个换行符后匹配 |
$ | 在结尾处匹配,也可以在带MULTILINE标记的每个换行符前匹配 |
\A | 在起始处匹配 |
\b | 在单词边界匹配,受ASCII标记影响——在字符集内部,则是backspace字符的转义字符 |
\B | 在非单词边界匹配,受ASCII标记影响 |
\Z | 在结尾处匹配 |
(?=e) | 如果表达式e在此断言处匹配,但没有超越此处——称为前瞻或正前瞻,则匹配 |
(?!e) | 如果表达式e在此断言处不匹配,也没有超越此处——称为负前瞻,则匹配 |
(?<=e) | 如果表达式e恰在本断言之前匹配——称为正回顾,则匹配 |
(?<!e) | 如果表达式e恰在本断言之前不匹配——称为负回顾,则匹配 |
依赖于前面的匹配是否发生来条件性地进行后面的匹配可以这样表达:
(?(id)yes_exp)
(?(id)yes_exp|no_exp)
其中的id是前面的捕获编号或者名称。
最后,给正则表达式设置标记的语法是(? flags)
,其中flags是a(ASCII),i(IGNORECASE),m(MULTILINE),s(DOTALL)与x(VERBOSE)中的一个或多个。
Python示例
import re
# 下面两个字符串分别是HTML和Python文件起始处的文件编码说明,我们需要从中提取出所使用的编码
html = '<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>'
python = '# -*- coding: latin1 -*-'
pattern = re.compile(r"""(?<![-\w]) # 负回顾断言,确保下面的匹配是在单词起始处并且不是以-开头
(?:(?:en)?coding|charset) # 匹配encoding或者coding或者charset,不参与捕获
(?:=(['"])?([-\w]+)(?(1)\1) # 匹配=号,然后是可选的引号,然后是一个单词,最后是条件匹配前面捕获的引号
|:\s*([-\w]+))""", # 匹配:号,然后是空白,最后是单词
re.IGNORECASE|re.VERBOSE) # 指定作用于正则表达式的标记,IGNORECASE表示忽略大小写
# VERBOSE允许我们在正则表达式中自由地使用空白与通常的Python注释,但存在一个约束,即如果我们需要匹配空白字符,就必须使用\s或者字符集,比如[ ]
match = pattern.search(html)
print(match.group(match.lastindex)) # ISO-8859-1,这里的lastindex是2
match = pattern.search(python)
print(match.group(match.lastindex)) # latin1, 请注意这里的lastindex是3而不是1,虽然选择符号|后面只有一个捕获的括号,但是选择该分支的前提是前一个分支匹配失败了,所以前面的两个捕获依然存在,只是值为None
# 下面命名分组的示例
kvs = """
name=caowentao
age = 31
"""
# 请注意上面的31后面有空白
pattern = re.compile(r"""^[ \t]* # 从换行符后开始匹配key前面的空白
(?P<key>\w+) # 命名该捕获组为key
[ \t]*=[ \t]* # 匹配key之后的空白,=号,以及value前面的空白
(?P<value>[^\n]+) # 命名该捕获组为value
(?<![ \t])""", # 断言前面的捕获的value后不包含空白
re.MULTILINE|re.VERBOSE) # MULTILINE标记指定^是从换行符后开始匹配
for match in pattern.finditer(kvs):
print('%s = %s' % (match.group('key'), match.group('value')))
# name = caowentao
# age = 31