正则表达式
形式化定义
该部分参考 [^1]
正则表达式的概念源于形式语言和自动机理论。先介绍一些一些相关的基本概念,符号规定如下:
| 符号 | 含义 | 举例 |
|---|---|---|
| 字母表,一个非空有穷的字符集 | ||
| 字符串,字母表中符号的有穷序列 | ||
| 语言,字符串的集合 |
不想打公式了,以后有时间在补充 🕊 😢
python 正则表达式
python 中可以使用 re 模块来实现正则表达式的相关功能。本文会介绍 re 中的函数和标志。
在这之前首先介绍 python 中各种字符串的形式和正则表达式语法。
python 字符串
-
最普通的字符串
'abc', "abc" -
格式化字符串:
name = 'ikun' # 1: % s = 'name is %s' %(name) # 2: format s = 'name is {name}'.format(name=name) # or s = 'name is {}'.format(name) # 3: f-string s = f"name is {name.replace('i', '')}" # s: name is kun
这两种字符串中,对于特殊字符需要转义,比如最熟悉的换行符 \n,如果需要字符串输出 \n,那么需要 s='\\n',这种写法可读性太差,即反斜杠灾难。其次是在格式化字符串中如果需要包含一对 {}, 则需要 s='name is {{}}',这样的话字符串的内容就是 name is {}
- 原始字符串
原始字符串以r开头,这种字符串引号内的内容就是字符串的内容,“所见即所得”,不进行任何转义。比如s=r'a\nb,那么输出字符串的内容就是a\nb
正则表达式语法
首先需要描述字符串规则,我们称之为“模式”。可以在 regex101 上在线测试正则语法
下面讨论一些常用的(以后用到新的再补充),完整版参考 [^2] 以及 [^3]
表示一类字符
| 模式 | 规则 |
|---|---|
. |
除换行符外的所有字符,指定 DOTALL 后匹配所有字符 |
\d |
数字字符 |
\D |
非数字字符 |
\w |
数字字符或字母或下划线 |
\W |
除了 \w 的字符 |
\s |
匹配任何空白字符,等价于字符类 [ \t\n\r\f\v] |
\S |
匹配任何非空白字符,等价于字符类 [^ \t\n\r\f\v] |
DOTALL 相关信息可以参考 标志
集合运算
| 模式 | 规则 |
|---|---|
[] |
字符集合,匹配括号内的所有可能字符,如 [abc],[0-9A-Z], 或除了括号里的都匹配,比如 [^123] |
A|B |
匹配正则 A 或 B,从左到右匹配,一旦成功就不再继续匹配 |
[] 的使用中有一个需要注意的点,re 的文档如是说:
特殊字符在集合中会失去其特殊意义。比如 [(+)] 只会匹配这几个字面字符之一 ‘(’, ‘+’, '', or ‘)’
元字符 (除了 ) 在字符类中是不起作用的。
分组
| 模式 | 规则 |
|---|---|
() |
分组,可用来捕获字符串中感兴趣的部分 |
分组是有编号的,比如对于模式 (a(b)c)d:
- 首先,组 0 一定存在,对应整个匹配结果
- 其次,组按照从左到右的顺序编号
- 可以同时传递多个组号
- groups 返回一个元组,从 1 到最后一个组
- 当知道分组的编号 n 后,可以在模式中引用,引用方式为
\n
p = re.compile('(a(b)c)d')
m = p.match('abcd')
m.group(0)
# 'abcd'
m.group(1)
# abc
m.group(2)
# b
m.group(2,1,2)
# ('b', 'abc', 'b')
括号在我们的传统的理解中,往往表示的是一种改变运算优先级的方式,这与正则中括号用来捕捉值的用途不同。为了让括号更“纯粹”,即只表示正则的一部分,可以使用非捕获组的语法,即 (?:...)
目前为止,分组的一个问题是,很难去跟踪组号,因此给组命名是一种容易想到的助记的方式,语法为 (?P<name>...)。有了命名后,在group 方法中就可以直接使用分组名了;同样的,引用也可以用分组名的方式,应写作 (?P=name)
例如:
import re
pattern = r"(\w+) \1"
# pattern = r"(?P<word>\w+) (?P=word)"
s = "cat cat dog cat dog dog dog"
print(re.findall(pattern, s))
两种 pattern 下的输出均为 ['cat', 'dog']
此外,介绍分组中的一个坑,如下:
m = re.match("([abc])+", "abc")
g = m.groups()
print(g)
输出结果只有 c,问题的解释可以参考 [^4],以及 [^5] 中的解释:
Repetition and Backreferences
As I mentioned in the above inside look, the regex engine does not permanently substitute backreferences in the regular expression. It will use the last match saved into the backreference each time it needs to be used. If a new match is found by capturing parentheses, the previously saved match is overwritten. There is a clear difference between ([abc]+) and ([abc])+. Though both successfully match cab, the first regex will put cab into the first backreference, while the second regex will only store b. That is because in the second regex, the plus caused the pair of parentheses to repeat three times. The first time, c was stored. The second time, a, and the third time b. Each time, the previous value was overwritten, so b remains.
数量限定
首先是三个特殊符号
| 模式 | 规则 |
|---|---|
<reg>* |
对前面的正则进行 0 到任意多次匹配 |
<reg>+ |
对前面的正则进行 1 到任意多次匹配 |
<reg>? |
对前面的正则进行 0 到 1 次匹配 |
这三个都是对字符进行贪婪的匹配,即尽可能多的匹配。为了进行非贪婪的匹配(尽可能少的匹配),可以使用 ? 的第二个语义,即限定为非贪婪的匹配模式。*?, +?, ?? 都是非贪婪的匹配
然后是数量重复
| 模式 | 规则 |
|---|---|
<reg>{m,n} |
对前面的正则匹配 m 到 n 个重复 |
如果忽略 m,则可以写为 <reg>{n};
如果忽略 n,则写为 <reg>{m,},此时上界为无限次
这种写法也是贪婪的匹配,即尽可能多的匹配,所以同样可以使用 ? 来进行非贪婪的限定,即<reg>{m, n}?
需要注意的是 {m,n} 这中间是不能有空格的
标志
| 标志 | 含义 |
|---|---|
re.S 或 re.DOTALL |
让 . 特殊字符匹配任何字符,包括换行符 |
模块函数
re 提供了不少的函数,下面从介绍一些常用的:
re.search(pattern, string, flag=0)
找到字符串中第一个与模式匹配的子串,返回对应的 re.Match;没找到则返回 None
re.match(pattern, string, flag=0)
和 search 类似,但是只在字符串的开头匹配。
对于返回值 re.Match 可以参见后文 re.Match
search 和 match 的区别可以从下面的测试中看出
import re
pattern = "<(.*?)>(.*?)<(.*?)>"
s = "fasd<a>first</a>fdag"
matches = re.search(pattern, s)
print(matches)
matches = re.match(pattern, s)
print(matches)
输出结果为:
<re.Match object; span=(4, 16), match='<a>first</a>'>
None
re.findall(pattern, string, flags=0)
该函数返回 pattern 在 string 中所有的 非重叠 匹配,以列表的形式返回。
import re
patterns = r"\d*?"
s = "1234000"
matches = re.findall(patterns, s)
print(matches)
输出结果为:
['', '1', '', '2', '', '3', '', '4', '', '0', '', '0', '', '0', '']
- 由于使用的是非贪婪的匹配,因此最开始应该匹配 0 次得到空字符;
- 然后由于 findall 函数是进行非重叠的匹配,因此下一次匹配不能再匹配 0 次(或者说,不能再以空字符打头) ,否则和
matches[0]重叠了,因此匹配一次,得到 1; - 然后还是为了不再重叠,匹配的开始位置要向后移,否则会重叠,后面以此类推…
列表的形式和 分组 是有关系的。返回结果取决于模式中捕获组的数量。如果没有组,列表元素是与模式匹配的字符串。如果有且仅有一个组,列表元素是与匹配到的字符串的组的部分。如果有多个组,列表元素是一个元组,元组的每一个元素对应匹配到的字符串的一个组。非捕获组不影响结果。
import re
patterns = [
"<.*?>.*?<.*?>",
"<.*?>(.*?)<.*?>",
"<(.*?)>(.*?)<(.*?)>"
]
s = "<a>first</a><b>second<c>"
for pattern in patterns:
mathches = re.findall(pattern, s)
print(mathches)
输出结果为:
['<a>first</a>', '<b>second<c>']
['first', 'second']
[('a', 'first', '/a'), ('b', 'second', 'c')]
这三行分别对应三种情况:
- 没有分组,匹配到整个字符串;
- 一个分组,列表元素是模式()内对应的部分
- 多个分组,列表元素是元组,元组元素对应模式中的一个括号
re.sub(pattern, repl, string [, count=0])
该函数用 repl 非重叠的替换字符串中被匹配的子串,count 指定了最大的替换次数,默认为 0,代表全部替换。
re.compile(pattern, flags=0)
该函数返回一个编译的正则对象,然后我们可以调用这个这个对象的 Pattern.match(string), Pattern.search(string) 等方法,这样做的好处是
re.split(pattern, string, maxsplit, flag)
用法和普通的 str.split 类似,但是这里分隔符是按照模式进行匹配,如果模式中有用于捕获的括号,则组内的内容也会被包含在括号中
import re
pattern1 = '\s'
pattern2 = '(\s)'
s = '1 d\ttest'
print(re.split(pattern1, s))
print(re.split(pattern2, s))
输出为
text['1', 'd', 'test'] ['1', ' ', 'd', '\t', 'test']
re.Match
re.Match 是 re.match 和 re.search 的返回值类型,介绍几个常用的方法。
在 分组 中已经提到了 group 和 groups 方法,现在介绍三个和位置有关的方法,start, end, span
从名字就能看出他们的含义,start 指的是匹配的开始的标号,end 指的是匹配结束的标号,这两个若未产生匹配则为 -1;而 span 则是一个二元组 (m.start(group), end(group))
进阶
可以参考网上的博客,比如 [^6]
References
[^1]:计算理论导引 Sipser
[^2]:正则表达式语法-python3.12.0文档
[^3]:正则表达式指南-python3.12.0文档
[^4]:Why Does a Repeated Capture Group Return these Strings?-Stack Overflow
[^5]:Regex Tutorial - Backreferences To Match The Same Text Again
[^6]:正则表达式入门
