正则表达式

简介

正则虽小, 但是不好学, 有一定的难度的. 正则表达式不只限于 Python, 而是独立于编程语言, 用于处理复杂文本的强大的高级文本操作工具. 正则表达式来源于 Perl 语言, 其他的编程语言也是支持了 Perl 的这个正则语言. 基本上, 语法相似度高达 90%.

正则对于字符串的操作, 无非就是分割, 匹配, 查找和替换. 也就是四个字: 模糊查询

比如说, 我现在有这样的一个字符串

1
s = "yyt always loves lc but lc loves yyt forever"

我们可以使用字符串的一个方法 .find() 来查找是否出现, 并且获取出现的位置.

1
2
3
s = "yyt always loves lc but lc loves yyt forever"
print(s.find("yyt")) # 0
print(s.find("lc")) # 17

这叫做精准查询, 不是正则. 这个例子中看不太出来, 不过我们可以换一个字符串:

1
s = "11 always loves 123 but 123 loves 11 forever and today is 2025.4.13"

我想要找到所有的数字, 如果数据量很大, 我们就不能用人眼找了. 这里就需要使用正则表达式!


在 Python 中, 如果需要使用正则表达式, 需要引入一个标准库的模块 re. 我们需要匹配, 那么先输入匹配的规则, 再指定匹配的字符串, 即调用函数: re.findall(规则, 字符串).

这里什么意思暂时不用知道, 看看就 OK

1
2
3
4
5
import re
s = "11 always loves 123 but 123 loves 11 forever and today is 2025.4.13"
# 利用正则, 寻找所有的数字
ret = re.findall(r"\d+", s)
print(ret)

该段代码会输出一个列表: ['11', '123', '123', '11', '2025', '4', '13'], 这就是正则表达式的用法了: 模糊的, 快速的匹配一个字符串中的相应字符.

元字符

正则表达式其实就是两个知识点, 一个是元字符, 另一个是方法. 方法其实只有几个而已, 元字符反而是最重要的. 下面会记录最常用的 11 种元字符, 全部都需要掌握, 记住名字.

通配符

写出来其实就是一个句点号: ., 它可以匹配除了换行符 \n 以外的任何符号. 这里提供这样一个有很多单词组成的字符串, 大部分单词都是以 a 开头以 e 结尾:

1
s = "age ape apple angle agree alone alive aside awake appl\ne"

正则表达式的匹配, 我们可以进行精准匹配, 比如我就要寻找 apple:

1
2
3
4
import re
s = "age ape apple angle agree alone alive aside awake appl\ne"
ret = re.findall(r"apple", s)
print(ret) # ['apple']

这样可以, 但是没有必要; 更多的, 我们还是要进行模糊的匹配. 比如说, 我改一下, 查询 a.e, 则代表我要查询:

  • 第一个字符是 a
  • 第二个字符无所谓
  • 第三个字符是 e

的单词. 调用如下代码, 可以得到结果.

1
2
3
4
import re
s = "age ape apple angle agree alone alive aside awake appl\ne"
ret = re.findall(r"a.e", s)
print(ret) # ['age', 'ape', 'ake']

[!note] 注意
这里的 ake 其实来自于 awake, 并不是错了, 而是匹配到了这一部分内容
另外, 假如有这样一个单词: a\ne, 也是不会被匹配的, 因为通配符无法匹配换行符

那么同理, 我想要匹配 a...e, 则会返回 ['apple', 'angle', 'agree', 'alone', 'alive', 'aside', 'awake'], 可以使用代码进行验证.

总而言之, 一个点 ., 表示的其实就是任何字符 (除了换行符).

字符集

顾名思义, 也可以匹配字符, 但是范围更小一些了. 字符集写作 [], 它会匹配一个中括号中的, 出现的任意字符符号, 字符之间不需要分隔符且数量不限. 不过这代表的是, 只要符合其中的一个就算.

例如这样一个字符串:

1
s = "yys yyt ywt yot yyo yyk y1t"

我只想要匹配 y 开头, t 结尾, 中间只能是 y 或者 1, 则可以使用字符集的形式进行匹配:

1
2
3
4
import re
s = "yys yyt ywt yot yyo yyk y1t"
ret = re.findall(r"y[y1]t", s)
print(ret) # ['yyt', 'y1t']

[!info] 注意
匹配的规则是: 中间是 y 或者 1, 所以如果你匹配字符串 yy1t, 并不符合规则.
如果你写的匹配表达式为 y,1, 也是不对的, 这样如果有字符串 y,t, 也会匹配成功


但如果我想要匹配很多东西, 比如 a~z 的所有小写字符呢? 可以使用字符集的一个 - 来表示一段范围. 比如我有如下的匹配, 寻找中间是小写字母的字符串:

1
2
3
4
import re
s = "y12 yy1 y3t 4y5 666 yyt"
ret = re.findall(r"y[a-z].", s)
print(ret) # ['yy1', 'yyt']

同理, 大写字符也是一样的, 直接使用 [A-Z] 即可. 这样相较于上面的通配符, 就进行了一个范围的缩小.

1
2
3
4
import re
s = "y12 yA1 y3t 4Z5 666 yyt"
ret = re.findall(r".[A-Z].", s)
print(ret) # ['yA1', '4Z5']

数字也是一样的, 从小到大, 就是 [0-9], 这样就匹配了所有的数字.

1
2
3
4
import re
s = "y12 yA1 y3t 4Z5 666 yyt"
ret = re.findall(r".[0-9].", s)
print(ret) # ['y12', 'A1 ', 'y3t', ' 4Z', ' 66']

[!warning] 注意
这里由于我们使用了通配符 ., 所以空格也是会被识别的, 只要连续三个字符中间是数字, 就会被匹配返回, 所以出现了类似于 A1 这样的东西.

另外, 字符集中还有一种方式, 叫做取反. 比如我希望, 只要一个字符串中间的字符不是数字, 就成立, 则可以使用取反的写法. 取反写作 ^, 会让当前字符集的内容取反. 如下, 会取出所有的, 中间不是数组的连续三个字符, 包括换行符.

1
2
3
4
import re
s = "y12 yA1 y3t 4Z5 666 yyt a\n1"
ret = re.findall(r".[^0-9].", s)
print(ret) # ['2 y', '1 y', '3t ', '4Z5', '6 y', 'yt ', 'a\n1']

综上, 如果我现在想要取出一段字符串中的所有非英文数字字符, 就可以直接使用取反+区间的方式了. 下面是一个案例:

1
2
3
4
import re
s = "H3ll0 w@r|d y&t and L("
ret = re.findall(r"[^a-zA-Z0-9]", s)
print(ret) # [' ', '@', '|', ' ', '&', ' ', ' ', '(']

重复元字符

在之前的内容中, 匹配的都是一个字符, 就算是字符集, 也仅仅匹配的是一个字符. 但是我们可能会有如下需求: 匹配所有 a 开头的, e 结尾的单词. 这个情况下, 我们之前学的就无法胜任了; 这里就可以使用重复元字符来实现.

重复元字符一共有四个: {}, *, +, ?, 下面边举例边学习.

{} 数量范围贪婪符

现在有如下字符串, 我想要筛选里面的指定的 a 开头, e 结尾的单词:

1
s = "lc loves apple and ahe and age and aaa and aee"

首先, 我可以要求, 只寻找中间有 1 个任意字符的这样的单词. 那么花括号直接写一个 1 即可:

1
2
3
4
import re
s = "lc loves apple and ahe and age and aaa and aee"
ret = re.findall(r"a.{1}e", s)
print(ret) # ['ahe', 'age', 'aee']

[!note] 等效
这其实就等效于一个 . 不过一般我们不会这么用的.


这里可以限制一下, 我想要中间的字符数量在 13 个, 则直接写 .{1,3} 即可, 这代表可以有 13 个任意字符. 如下代码, 可以筛选出来我们需要的内容:

1
2
3
4
import re
s = "lc loves apple and ahe and age and aaa and aee"
ret = re.findall(r"a.{1,3}e", s)
print(ret) # ['apple', 'ahe', 'age', 'aee']

同理, 上面的是一个闭区间, 那么如果我空了一半呢? 自然就是一个开区间了. 也就代表可以有无数个任意字符, 只要两边是 a 和 e 即可. 如下代码, 实现了这样的匹配效果.

1
2
3
4
import re
s = "yyt yyyt yyyyyt yyy123t yy11yy123t ytytytyt"
ret = re.findall(r"y.{1,}t", s)
print(ret) # ['yyt yyyt yyyyyt yyy123t yy11yy123t ytytytyt']

[!error] 注意
默认来说, 贪婪是往大了走的, 所以对于上面这个字符串, 整体看的话, 刚好第一个字符是 y, 最后一个字符是 t, 所以直接返回了整个字符串. 这并不是 Bug, 而是默认的贪婪匹配规则.

显然, 我们不想要这种结果. 这里我们需要改变默认的贪婪匹配.

  • 默认贪婪匹配: 按照最大匹配数进行优先分配
  • 取消贪婪匹配: 按照最小匹配数进行优先分配

这里, 我们需要在贪婪后面, 紧跟一个 ?, 进行反向即可.

1
2
3
4
import re
s = "yyt yyyt yyyyyt yyy123t yy11yy123t ytytytyt"
ret = re.findall(r"y.{1,}?t", s)
print(ret) # ['yyt', 'yyyt', 'yyyyyt', 'yyy123t', 'yy11yy123t', 'ytyt', 'ytyt']

至此, 已经可以正常匹配了.

[!warning] 注意
如果里面有换行符 \n, 则会进行自动截断. 因为 . 无法匹配换行符!
当然, 也是可以有办法匹配的, 只需要在函数后面再跟一个参数即可: re.findall(r"y.{1,}?t", s, re.S)


另外, 花括号前面也并不是只能是点, 也可以是其他的字符. 比如我想要找到一个字符串中的, 所有用 a 开头, b 在中间, c 来结尾的字符串, 则可以这么写:

1
2
3
4
import re
s = "abbbbbc aaaacb bbbbac bbbbca abc abbbc aabbcc"
ret = re.findall(r"ab{1,}?c", s)
print(ret) # ['abbbbbc', 'abc', 'abbbc', 'abbc']

所以说, 这个花括号前面的东西不仅为普通的字符, 其他的任何字符都是可以进行匹配的.


最后, 可以综合前面的字符集一起运用. 比如, 我只想要中间的字符为小写字母, 不想有其他的特殊字符或者数字, 就可以这么写:

1
2
3
4
import re
s = "yyt yyyt yyyyyt yyy123t yy11yy123t ytytytyt"
ret = re.findall(r"y[a-z]{1,}?t", s, re.S)
print(ret) # ['yyt', 'yyyt', 'yyyyyt', 'ytyt', 'ytyt']

给一个实际一些的案例, 比如我要从一句话中, 找到所有的 Java 版本号, 就可以这么写:

1
2
3
4
import re
s = "Yyt studies Java8 and Lc studies Java20"
ret = re.findall(r"Java[0-9]{1,}", s, re.S)
print(ret) # ['Java8', 'Java20']

* 左边原子出现 0 或多次

[!note] 注意
花括号才是最重要的, 这些特殊符号不过是简洁了一些而已.

这里的 * 表示左边的原子出现 0 次或者多次. 等同于 {0,}. 例如, 我现在要匹配 a 开头 e 结尾的所有单词, 就可以使用这个字符来简化代码了:

1
2
3
4
import re
s = "apple age aho ahe ae aoe ayytlce"
ret = re.findall(r"a[a-z]*?e", s)
print(ret) # ['apple', 'age', 'ahe', 'ae', 'aoe', 'ayytlce']

这里使用 [a-z] 是为了屏蔽空格, 否则会输出不应该输出的内容.

[!note] 注意
相反的, 如果你没有加反贪婪符号 ?, 则会输出从第一个 e 开始, 到最后一个 e 字符. 不过其实这种用的并不多, 因为我们一般需要的是某一段内容.

应用案例

比如说, 有这样一个爬取到的字符串:

1
2
3
4
5
6
7
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
<li>草莓</li>
<li>西瓜</li>
</ul>

我现在想要使用正则表达式, 获取所有的水果是什么. 这个时候, 就可以使用正则表达式快速的获取内容了! 示例代码如下:

1
2
3
4
import re
s = "<ul><li>苹果</li><li>香蕉</li><li>橙子</li><li>草莓</li><li>西瓜</li></ul>"
ret = re.findall(r"<li>.*?</li>", s)
print(ret) # ['<li>苹果</li>', '<li>香蕉</li>', '<li>橙子</li>', '<li>草莓</li>', '<li>西瓜</li>']

这样就可以筛选出来所有的 li 内容了, 这里其实就是用到了一个非常重要的组合: .*?

[!info] 扩展
这里的 . 其实也可以是其他的符号, 甚至是字符集, 比如 [a-zA-Z], 这样匹配的就是只有英文字符的内容了; 或者是 [0-9], 这样子匹配的就是全数字内容了.

相比起来, 相当于对范围做了进一步的限定.

+ 左边元字符出现 1 或多次

这里的 + 表示左边的原子出现 1 次或者多次. 等同于 {1,}

[!note] 小想法
其实这个和上面的 * 几乎没有区别, 不过中间的东西至少出现一次而已; 不过呢, 我们如果要获取, 自然中间都是有点东西的, 自然也就不会出现空内容的情况了√

使用和上面的 * 几乎一摸一样, 这里不再进行演示.

? 左边元字符出现 1 或多次

这里的 ? 表示前面的原子出现 0 次或者 1 次. 等同于 {0,1}

[!warning] 注意
这里的问号并不仅仅代表刚才花括号里面的取反操作, 反而是代表了出现 0~1 次.
因为重复符号并不是元字符√

使用方式大差不差, 不过多进行介绍.

补充知识

虽然前面的重复元字符部分也使用到了这个符号, 但是作用是完全不一样的. 重复元字符中, 表示的是非 xxx 的关系, 这里则表示的是一行的开头位置.

比如说, 我有如下字符串, 我需要的是获取这个字符串中的所有数字:

1
s = "Yyt is 18 years old, and born in 2006 10 16"

我们可以思考, 是否需要取消贪婪. 如果取消贪婪, 相当于有一个数字就行了, 这显然不是我们想要的; 我们想要的是连续的数字, 所以这里反而不需要取消贪婪!

1
2
3
4
import re
s = "Yyt is 18 years old, and born in 2006 10 16"
ret = re.findall(r"[0-9]+", s)
print(ret) # ['18', '2006', '10', '16']

边界符

^ 开始边界符

比如说, 我有这样的一个字符串:

1
s = "/2025/03/29/CTFShow-web17-WP/1.jpg"

假如我现在要进行路径的匹配, 只允许传入的路径为 /2025/03/29/ 开头, 则需要使用开始边界符. 如果不使用的话, 前面随便传递一个什么, 都是符合的, 就会造成问题.

1
2
3
4
5
import re

s = "xxxxxxx/2025/03/29/CTFShow-web17-WP/1.jpg\n/2025/03/29/CTFShow-web17-WP/2.jpg"
ret = re.findall(r"/[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}/", s)
print(ret) # ['/2025/03/29/', '/2025/03/29/']

理论上, 我们只想要查询符合要求的路径字符串, 但是之前的写法仍然会保留非合法的内容. 这里就需要做一个限制, 告诉程序, 我们要找的是开头为 xxx 的字符串. 直接前面加上一个 ^ 即可. 如下代码, 改后可以匹配符合要求的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

pattern = r"^/[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}/"

s = "xxxxxxx/2025/03/29/CTFShow-web17-WP/1.jpg"
ret = re.findall(pattern, s)
print(ret)

s = "/2025/03/29/CTFShow-web17-WP/2.jpg"
ret = re.findall(pattern, s)
print(ret)

# []
# ['/2025/03/29/']

第一个匹配失败了, 所以为空列表, 成功过滤 url.

$ 结束边界符

还是上面的路径例子, 我现在就想找到所有的, 以 / 为开头, 以中间内容后的 / 为结尾的字符串. 就可以在后面加一个 $ 结束边界符了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import re

pattern = r"^/[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}/$"

str_list = [
    "xxxxxxx/2025/03/29/CTFShow-web17-WP/1.jpg",
    "/2025/03/29/CTFShow-web17-WP/2.jpg",
    "/2025/03/21/"
]
for i in str_list:
    print(re.findall(pattern, i))

"""
[]
[]
['/2025/03/21/']
"""

转义符

正则的转义符其实就是反斜杠 \, 可以和后面的字符组成一种特殊的作用符号. 不过还请注意, 正则的转义符和普通语言中的转义符并不一样, 使用起来完全不一样. 绝对不要混淆!

元字符 描述
\d 匹配一个数字原子, 等价于 [0-9]
\D 匹配一个非数字原子, 等价于 [^0-9] 或者 [^\d]
\w 匹配一个包括下划线的单词原子, 等价于 [A-Za-z0-9_]
\W 匹配一个非单词原子, 等价于 [^A-Za-z0-9_][^\w]
\n 匹配一个换行符
\t 匹配一个制表符
\s 匹配一个空白元字符, 包括但不限于空格, 制表符, 换页符
\S 匹配一个非空白元字符.
\b 匹配一个单词边界原子, 也就是单词之间的空隙
\B 匹配一个非单词边界原子, 等价于 [^\b]

假如我要找到一个字符串中的所有数字, 之前我们可以这样写:

1
2
3
4
5
6
7
import re

s = "kaede's age is 19 and yyt's age is 18 and kaede loves yyt until 999999..."

# 根据之前所学, 可以使用如下正则
regex1 = r"[0-9]+"
print(re.findall(regex1, s)) # ['19', '18', '999999']

可以是可以, 但是不够简单. 这里可以直接用一个转义符 \d 来等同于阿拉伯数字.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

# 假如需要找到一个字符串中的所有数字
s = "kaede's age is 19 and yyt's age is 18 and kaede loves yyt until 999999..."

# 根据之前所学, 可以使用如下正则
regex1 = r"[0-9]+"
regex2 = r"\d+"
print(re.findall(regex1, s))
print(re.findall(regex2, s))

# 输出是一摸一样的.
# ['19', '18', '999999']
# ['19', '18', '999999']

如果想要找到一个字符串中的所有单词, 则可以直接使用正则: \w+, 只匹配单词元字符:

1
2
3
4
5
6
7
import re

s = "kaede's age is 19 and yyt's age is 18 and kaede loves yyt until 999999..."

regex = r"\w+"
print(re.findall(regex, s))
# ['kaede', 's', 'age', 'is', '19', 'and', 'yyt', 's', 'age', 'is', '18', 'and', 'kaede', 'loves', 'yyt', 'until', '999999']

不过呢, 这个单词字符并不包含空格, 以防止贪婪模式的困扰.


单词边界 \b 比较特殊, 单词的边界, 指的就是单词之间的空白. 比如我希望能够找到一个字符串中的所有的 cat 单词, 不希望匹配到含有这个单词的更长一些的单词. 这个时候, 我们就需要使用单词边界符了.

可能想的是, 在 cat 检查的时候, 后面加一个空格, 可是如果 cat 后面是 ! 等特殊符号, 也是会有问题的. 不如直接使用单词分界符:

1
2
3
4
5
6
import re

s = "lc cat and yyt cat catch a good chance! good cat!"
regex = r"cat\b"
print(re.findall(regex, s))
# ['cat', 'cat', 'cat']

[!warning] 注意

剩下的转义符都大差不差了, 正常使用即可.

分组与优先提取

在正则表达式中, () 就是分组符号, 其实就是把某一些内容作为一个独立的整体进行处理. 另外, 小括号也有优先提取的功能, 会优先匹配括号中的内容

1
2
3
4
5
6
7
import re

# 我想找到 bat重复2~3次的字符串
s = "bat batbatbat batbat bbatbat bbat bbaatt"

print(re.findall(r"\b(?:bat){2,3}\b", s))
# ['batbatbat', 'batbat']

注意, 这里涉及到了优先提取相关的东西. 不过, 小括号的作为整体已经体现出来了.


优先提取, 就是加上 ?:. 例如, 我要找到一个字符串中的所有邮箱:

1
2
3
4
5
6
7
8
9
10
11
import re

s = """
Hello Friend, You can visit fumoe@fumoe.top or
have a look at kaedeshimizu@qq.com to look for a
help, and I'am sure it (aaa_test@gmail.com) Will
make a sence.
"""

print(re.findall(r"\b[\w-]+@\w+\.\w+\b", s))
# ['fumoe@fumoe.top', 'kaedeshimizu@qq.com', 'aaa_test@gmail.com']

该段代码没有问题, 不过我可能想要强调某些部分, 比如我只想要获取邮箱的前面, 名字部分的内容, 我就可以给名字部分加上括号, 这样获取到的内容就是只有名字了:

1
2
3
4
5
6
7
8
9
10
11
import re

s = """
Hello Friend, You can visit fumoe@fumoe.top or
have a look at kaedeshimizu@qq.com to look for a
help, and I'am sure it (aaa_test@gmail.com) Will
make a sence.
"""

print(re.findall(r"\b([\w-]+)@\w+\.\w+\b", s))
# ['fumoe', 'kaedeshimizu', 'aaa_test']

如果有多个括号, 则会同时进行提取, 比如我既获取前面的名字, 又要 xxx.com 的 xxx 部分:

1
2
3
4
5
6
7
8
9
10
11
12
import re

s = """
Hello Friend, You can visit fumoe@fumoe.top or
have a look at kaedeshimizu@qq.com to look for a
help, and I'am sure it (aaa_test@gmail.com) Will
make a sence.
"""

# 返回的内容是元组形式
print(re.findall(r"\b([\w-]+)@(\w+)\.\w+\b", s))
# [('fumoe', 'fumoe'), ('kaedeshimizu', 'qq'), ('aaa_test', 'gmail')]

或者元字符

这里的或是逻辑的或, 没有和普通有太大的出入. 或者, 就是很多个选一个出来. 在之前的普通字符集部分, 假如我想要匹配多个字符, 可以直接写: abcde, 这里的几个字符本身就是或者的关系, 所以不需要写 |.

但是, 如果我现在有一个单词需要或操作, 则需要使用 | 表示或者.

1
2
3
4
5
6
7
8
9
10
import re

# 我想找到 bat重复2~3次的字符串
s = """
I love huoguo, I guess I like the orange best.
However, strawberry is always a good choice.
"""

print(re.findall(r"orange|strawberry|apple", s))
# ['orange', 'strawberry']

常用方法

我们可以使用自带的一些方法, 更方便的从字符串中找到我们需要的内容. 下面是一些常用的正则表达式方法, 根据需要选用即可:

函数 描述
findall 按照指定的正则, 查找符合正则的所有匹配项, 以列表返回
search 在字符串的 任何位置 寻找, 找到了则返回 re.Match 对象, 否则 None
match 在字符串的 开始位置 寻找, 找到了则返回 re.Match 对象, 否则 None
split 按照正则切分字符串, 返回一个分割后的列表
sub 正常寻找, 但是可以替换找到的匹配项
complie 编译正则表达式模式, 并且生成一个正则表达式对象, 可以重用这个对象

假如, 我想要找到一个日志中的第一个 ERROR:

1
2
3
4
5
ERROR: Hello World
WARN: TEST WARN
ERROR: ARE YOU OK!
INFO: YESSS
ERROR: KO!

则可以使用 search 方法. 用法完全一样, 直接记录即可.

[!note] 注意

返回的是一个 Match 对象, 这个对象是有很多属性的, 不妨看看.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re

# 我想找到 bat重复2~3次的字符串
with open("log.txt", "r", encoding="utf-8") as f:
s = f.read()

# 寻找到第一个error
# 注意 如果匹配成功, 则返回的是一个Match对象 否则返回None
matcher = re.search("ERROR:.*", s)
if matcher:
# 拿到开始和结束的地方
print(matcher.span()) # (0, 18)
# 只拿到开始
print(matcher.start()) # 0
# 只拿到结束
print(matcher.end()) # 18
# 拿到具体的值
print(matcher.group()) # ERROR: Hello World