正则表达式
正则表达式
正则表达式在线验证工具:https://regex101.com/
正则表达式在线验证工具:https://regexr-cn.com/
python官方文档:https://docs.python.org/3/howto/regex.html
参考博客:https://www.byhy.net/tut/py/extra/regex/
练习网站:https://codejiaonang.com/#/course/regex_chapter1/0/0
练习讲解:https://www.bilibili.com/video/BV19t4y1y7qP?share_source=copy_web
总览
- 特殊字符,术语叫做metacharacters(元字符)
1 | . * + ? \ [ ] ^ $ { } | ( ) |
- 特殊字符表
实例 | 描述 |
---|---|
. |
匹配除 \n 之外的任何单个字符。要匹配包括 \n 在内的任何字符,请使用象 [.\n] 的模式。 |
? |
匹配一个字符零次或一次,另一个作用是非贪婪模式 |
+ |
匹配1次或多次 |
* |
匹配0次或多次 |
\| |
或者 |
`^` | 开头 |
$ |
结尾 |
[^...] |
非 |
(...) |
分组匹配 |
\b |
匹配单词边界 |
\B |
匹配非单词边界,\b 的补集 |
\d |
匹配一个数字字符。等价于[0-9] 。 |
\D |
匹配一个非数字字符。等价于 [^0-9] |
\s |
匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v] 。(注意方括号开头跟着一个空格) |
\S |
匹配任何非空白字符。等价于 [^ \f\n\r\t\v] |
\w |
匹配数字字母和下划线。等价于[A-Za-z0-9_] 。 |
\W |
匹配任何非数字字母和下划线。等价于 [^A-Za-z0-9_] 。 |
.
——匹配所有字符
例如要从下面的文本中选出所有的颜色
1 | 苹果是绿色的 |
观察文本后可以发现,也就是要找到所有以“色”字结尾,并且包括前面一个字发词语,那么可以写成
1 | .色 |
其中.
代表了任意的一个字符,注意只是一个字符。.色
合起来就表示要找任意一个字符后面是“色“这个字。
写在python代码中如下
1 | content = '''苹果是绿色的 |
运行结果如下
1 | 绿色 |
*
——重复匹配任意次
*
表示匹配前面的子表达式任意此,包括0次。
例如,在下面的文本中,选择每行逗号后面的字符串内容,包括逗号本身。(注意,这里的逗号是中文的逗号)
1 | 苹果,是绿色的 |
此时可以使用正则表达式
1 | ,.* #中文逗号 |
从逗号”,“开始,.*
匹配任意字符任意次,即从逗号开始匹配到行末(注意匹配都是以行为单位的,.*
是到行末结束不是到段末结束)
python代码
1 | content = '''苹果,是绿色的 |
运行结果
1 | ,是绿色的 |
注意,.*
在正则表达式中是非常常见的,表示匹配任意字符任意次数。当然*
星号前面的不是非得是点.
,也可以是其它字符,比如绿色*
,”绿“字开头后接任意次数”色“字
+
——重复匹配多次
+
表示匹配前面的子表达式一次或多次,不包括0次。
例如,从下面文本中选择逗号后面的内容(包括逗号本身),如果逗号后面没有内容就不匹配。
1 | 苹果,是绿色的 |
正则表达式如下设计
1 | ,.+ |
.+
和.*
都是常见的组合
{}
——匹配指定次数
花括号表示匹配前面的字符指定次数
例如下面的文本
1 | 红彤彤,绿油油,黑乎乎,绿油油油,绿油油油油 |
表达式绿油{2,4}
表示“绿”字开头后接绿字2到4次
- 典型应用场景——提取数字
1 | zwxQQ:1379347218 |
正则表达式\d{10}
,其中\d
表示数字
- 开闭区间
{x,} |
表示 |
---|---|
{1,} |
表示,等价于+ |
{0,} |
表示,等价于* |
*
和*?
——贪婪模式和非贪婪模式
* |
贪婪模式——匹配尽可能长的字符串 |
---|---|
*? |
非贪婪模式——匹配尽可能短的字符串 |
我们要把下面的字符串中的所有html标签都提取出来,
1 | source = '<html><head><title>Title</title>' |
得到这样的一个列表
1 | ['<html>', '<head>', '<title>', '</title>'] |
很容易想到使用正则表达式 <.*>
写出如下代码
1 | source = '<html><head><title>Title</title>' |
但是运行结果,却是
1 | ['<html><head><title>Title</title>'] |
怎么回事? 原来 在正则表达式中, ‘*’, ‘+’ 都是贪婪地,使用他们时,会尽可能多的匹配内容,
所以, <.*>
中的 星号(表示任意次数的重复),一直匹配到了 字符串最后的 </title>
里面的e。
解决这个问题,就需要使用非贪婪模式,也就是在星号后面加上 ?
,变成这样 <.*?>
代码改为
1 | source = '<html><head><title>Title</title>' |
再运行一遍,就可以了
?
——指定字符为可选字符
有时,我们可能想要匹配一个单词的不同写法,比如color
和colour
,或者honor
与honour
。
这个时候我们可以使用 ?
符号指定一个字符、字符组或其他基本单元可选,这意味着正则表达式引擎将会期望该字符出现零次或一次。
例如:
在这里 u?
表示u
是可选的,即可以出现也可以不出现,可以匹配的是 honor
和 honour
。
通过这个案例可以知道?
的作用就是匹配它之前的字符0
次或1
次。
|
——或者条件
例如要提取所有图片文件的后缀名,可以在各个后缀名之间加上一个 |
符号:
1 | (.jpg|.gif|.jpeg|.png) |
\
——对元字符的转义
在下面的文本中匹配点“.”前面的字符串,也包含点“.”本身
1 | 苹果.是绿色的 |
正则表达式如下设计
1 | .*\. |
其中\.
表示对.
的转义
python代码如下
1 | content = '''苹果.是绿色的 |
运行结果如下
1 | 苹果. |
\
——匹配某种类型的字符
快捷方式 | 描述 |
---|---|
\w |
与任意单词字符匹配,任意单词字符表示 [A-Z] 、 [a-z] 、[0-9] 、_ |
\W |
匹配任意一个非文字字符,等价于表达式 [^a-zA-Z0-9_] |
\d |
与任意数字匹配[0-9] |
\D |
匹配任意一个不是0-9之间的数字字符,等价于表达式 [^0-9] |
\s |
匹配任意一个空白字符,包括 空格、tab、换行符等,等价于表达式 [\t\n\r\f\v] |
\S |
匹配任意一个非空白字符,等价于表达式 [^ \t\n\r\f\v] |
Flag | Meaning |
---|---|
ASCII , A |
Makes several escapes like \w , \b , \s and \d match only on ASCII characters with the respective property. |
DOTALL , S |
Make . match any character, including newlines. |
IGNORECASE , I |
Do case-insensitive matches. |
LOCALE , L |
Do a locale-aware match. |
MULTILINE , M |
Multi-line matching, affecting ^ and $ . |
VERBOSE , X (for ‘extended’) |
Enable verbose REs, which can be organized more cleanly and understandably. |
[]
——匹配几个字符串之一
- 方括号表示要匹配方括号中指定的几个字符中的一个
比如[abc]
表示匹配 a, b, c 三者的任意一个字符
- 方括号中的
-
表示范围
例如[a-z]
表示所有的小写字母
- 一些元字符在方括号内失去了魔法,变得和普通字符一样了
比如[akm.]
匹配 a k m .
里面任意一个字符。
这里 .
在括号里面不在表示匹配任意字符了,而就是表示 .
这个字符本身\.
- 如果在方括号中使用
^
, 表示非
方括号里面的字符集合。
比如[^a-z]
表示匹配所有非小写字母的字符,^
非的是整个方括号,而不只是^
后接的表达式。
^
——匹配文本的起始位置
^
表示匹配文本的 开头
位置。
正则表达式可以设定 单行模式
和 多行模式
如果是 单行模式
,表示匹配 整个文本
的开头位置。
如果是 多行模式
,表示匹配 文本每行
的开头位置。
比如,下面的文本中,每行最前面的数字表示水果的编号,最后的数字表示价格
1 | 001-苹果价格-60, |
如果我们要提取所有的水果编号,用这样的正则表达式 ^\d+
- python代码如下
1 | content = '''001-苹果价格-60 |
注意re.compile()
的第二个参数re.M
指定了多行模式
运行结果如下
1 | 001 |
如果,去掉re.compile()
的第二个参数re.M
, 运行结果如下
1 | 001 |
就只有第一行了。因为单行模式下,^ 只会匹配整个文本的开头位置。
$
——匹配文本的结尾位置
$
表示匹配文本的 结尾
位置。
如果是 单行模式
,表示匹配 整个文本
的结尾位置。
如果是 多行模式
,表示匹配 文本每行
的结尾位置。
比如,下面的文本中,每行最前面的数字表示水果的编号,最后的数字表示价格
1 | 001-苹果价格-60, |
如果我们要提取所有的水果编号,用这样的正则表达式 \d+$
- 对应Python代码
1 | content = '''001-苹果价格-60 |
注意,re.compile()
的第二个参数re.MULTILINE
,指明了使用多行模式,
- 运行结果如下
1 | 60 |
如果,去掉 re.compile()
的第二个参数re.MULTILINE
, 运行结果如下
1 | 80 |
就只有最后一行了。因为单行模式下,$ 只会匹配整个文本的结束位置。
\b\B
——匹配边界
\b
——匹配数字字母下划线组成的单词的边界,即\w
组成的单词的边界
\B
——匹配非单词的边界,作为\b
的补集
实例——匹配man
- 如果直接使用man进行匹配就会匹配所有的man
1 | man |
- 使用
\b
可以识别出单词man
1 | \bman\b |
()
——组选择
括号称之为 正则表达式的 组选择。
组
就是把正则表达式匹配的内容里面 其中的某些部分
标记为某个组。
我们可以在正则表达式中标记 多个
组
为什么要有组的概念呢?因为我们往往需要提取已经匹配的 内容里面的 某些部分的信息。
前面,我们有个例子,从下面的文本中,选择每行逗号前面的字符串,也包括逗号本身。
1 | 苹果,苹果是绿色的 |
就可以这样写正则表达式 ^.*,
。
但是,如果我们要求 不包括逗号 呢?当然不能直接 这样写 ^.*
,因为最后的逗号是特征所在,如果去掉它,就没法找逗号前面的内容了。但是把逗号放在正则表达式中,又会包含逗号。
解决问题的方法就是使用组选择符——()
。
我们这样写 ^(.*),
,结果如下
- 实用场景——提取姓名和手机号
1 | 刘玄德,手机号码15945678901 |
1 | ^(.+),.+(\d{11}) |
(?:)
——非捕获组
有时候,我们并不需要捕获某个分组的内容,但是又想使用分组的特性。
这个时候就可以使用非捕获组(?:表达式)
,从而不捕获数据,还能使用分组的功能。例如:
\n
——分组的回溯引用
正则表达式还提供了一种引用之前匹配分组的机制,有些时候,我们或许会寻找到一个子匹配,该匹配接下来会再次出现。
例如,要匹配一段 HTML 代码,比如:0123<font>提示</font>abcd
,可能会编写出这样一段正则表达式:
这确实可以匹配,不过可能还有另一种情况,如果数据改成这样:<font>提示</bar>
在这里font
和 bar
明显不是一对正确的标签,但是我们编写的正则表达式还是将它们给匹配了,所以这个结果是错误的。
我们想让后面分组的正则也匹配font
,但是现在所有形式的都会匹配。
那如果想让后面分组的正则和第一个分组的正则匹配同样的数据该如何做呢?
可以使用分组的回溯引用,使用\N
可以引用编号为N
的分组,因此上述例子的代码我们可以改为:
通过这个例子,可以发现 \1
表示的就是第一个分组,在这里第一个分组匹配的是 font
所以\1
就代表font
。
另外一个例子,匹配<abc>…<cba>这种标签中间的内容
(?=)
——正向先行断言
正向先行断言:(?=表达式)
,指在某个位置向右看,表示所在位置右侧必须能匹配表达式
例如:
1 | 我喜欢你 我喜欢 我喜欢我 喜欢 喜欢你 |
如果要取出喜欢两个字,要求这个喜欢后面有你,这个时候就要这么写:喜欢(?=你)
,这就是正向先行断言。
正向先行断言之密码强度验证
(?=.*?b)
给出一串字符串,它是abcba
的每个字母间隙插入i
构成的
1 | iaibicibiai |
使用下面的正则表达式对其进行匹配
1 | i(?=.*?b) |
i
是要匹配的内容(?=...)
这个表示先行断言.*?b
这个表示以b结尾的字符串,?
表示以非贪婪模式匹配,比如iaib
、ib
、icib
等
使用这个正则表达式对该字符串进行匹配的结果如下
它匹配了4个i
- 第一个
i
被匹配是因为它后面接着aib
,满足先行断言条件(?=.*?b)
- 第二个
i
被匹配是因为它后面接着b
,满足先行断言条件(?=.*?b)
- 第三个
i
被匹配是因为它后面接着cib
,满足先行断言条件(?=.*?b)
- 第四个
i
被匹配是因为它后面接着b
,满足先行断言条件(?=.*?b)
- 第五个和第六个
i
没有被匹配,因为它后面没有再接上.*?b
(?=.*b)
换个正则表达式重新对上面的字符串进行匹配,去掉?
断言中表达式改用贪婪模式,匹配的结果是一样的但是过程不一样
1 | i(?=.*b) |
- 第一个
i
被匹配是因为它后面接着aibicib
,满足先行断言条件(?=.*b)
- 第二个
i
被匹配是因为它后面接着bicib
,满足先行断言条件(?=.*b)
- 第三个
i
被匹配是因为它后面接着cib
,满足先行断言条件(?=.*b)
- 第四个
i
被匹配是因为它后面接着b
,满足先行断言条件(?=.*b)
- 第五个和第六个
i
没有被匹配,因为它后面没有再接上.*b
和上面的不同只是先行断言的判断条件改成了贪婪模式,所以会看最远的b
(?=.+b)
再改用正则表达式对字符串进行匹配,将*
改为+
,此时的第4个i
将不会被匹配上
1 | i(?=.+b) |
- 第一个
i
被匹配是因为它后面接着aibicib
,满足先行断言条件(?=.+b)
- 第二个
i
被匹配是因为它后面接着bicib
,满足先行断言条件(?=.+b)
- 第三个
i
被匹配是因为它后面接着cib
,满足先行断言条件(?=.+b)
- 第四个
i
没有被匹配,因为它后面紧接着b
,.+b
要求b
前面至少要有一个字符,单独的b
是不满足先行断言条件(?=.+b)
的
匹配null
将上面的i
全部去掉,先行断言则会匹配null
,因为断言表达式(?=...)
前面没有接任何字符
密码强度验证
请编写一个正则表达式进行密码强度的验证,要求密码必须:
- 至少包含一个大写字母
- 至少包含一个小写字母
- 至少包含一个数字
- 至少
8
个字符
1 | (?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.{8}) |
这个正则表达式是由4个先行断言复合而成,对null
进行匹配,依次判断null
后面是否满足4个先行断言
(?=.*[A-Z])
匹配后面接着大写字母的null
(?=.*[a-z])
匹配后面接着小写字母的null
(?=.*[0-9])
匹配后面接着数字的null
(?=.{8})
匹配后面接着8个字符的null
- 合起来就是匹配后面接着大写字母、小写字母、数字和8个字符的
null
,即4个先行断言匹配的null
的交集
(?!)
——反向先行断言
反向先行断言(?!表达式)
的作用是保证右边不能出现某字符。
例如: 我喜欢你 我喜欢 我喜欢我 喜欢 喜欢你
如果要取出喜欢两个字,要求这个喜欢后面没有你,这个时候就要这么写:喜欢(?!你)
,这就是反向先行断言。
- 实例——匹配不是qq邮箱的邮箱
1 |
用@
作为关键词,匹配后面不接qq
的@
(?<=)
——正向后行断言
正向后行断言:(?<=表达式)
,指在某个位置向左看,表示所在位置左侧必须能匹配表达式
例如:如果要取出喜欢两个字,要求喜欢的前面有我,后面有你,这个时候就要这么写:(?<=我)喜欢(?=你)
。
(?<!)——反向后行断言
反向后行断言:(?<!表达式)
,指在某个位置向左看,表示所在位置左侧不能匹配表达式
例如:如果要取出喜欢两个字,要求喜欢的前面没有我,后面没有你,这个时候就要这么写:(?<!我)喜欢(?!你)
。
实例——匹配单dollar符号
请使用正则表达式匹配单dollar符号$...$
中的数据。
1 | (?<!\$)\$[^$]+\$(?!\$) |
(?<!\$)\$
表示\$
符号左边不能有\$
符号
\$(?!\$)
表示\$
符号右边不能有\$
符号
[^$]+
表示中间是非\$
符号组成的字符串,并且非空
同理匹配双dollar符号$$...$$
中间的内容可以使用下面的正则表达式
1 | (?<!\$)\$\$[^\$]+\$\$(?!\$) |
实例——匹配所有的小数
请使用正则表达式匹配所有的小数
1 | (?<!\..*)(?<=\d)\.(?=\d)(?!.*\.) |
\.
以小数点作为标识符(?<!\..*)
表示左边不能再有小数点(?<=\d)
表示小数点左边要有数字(?=\d)
表示小数点右边要有数字(?!.*\.)
表示小数点右边不能再有小数点
注意:有些正则表达式网址会对(?<!.*)
这个表达式报错
re.split
方法——字符串切分
字符串对象的 split
方法只适用于简单的字符串分割。 有时,你需要更加灵活的字符串切割。
比如,我们需要从下面字符串中提取武将的名字。
1 | names = '关羽; 张飞, 赵云,马超, 黄忠 李逵' |
我们发现这些名字之间, 有的是分号隔开,有的是逗号隔开,有的是空格隔开, 而且分割符号周围还有不定数量的空格
这时,可以使用正则表达式里面的 split 方法:
1 | import re |
正则表达式 [;,\s]\s*
指定了,分割符为 分号、逗号、空格 里面的任意一种均可,并且该符号周围可以有不定数量的空格。
re.sub
方法——字符串替换
字符串 对象的 replace
方法只适应于 简单的 替换。 有时,你需要更加灵活的字符串替换。
比如,我们需要在下面这段文本中 所有的 链接中 找到所以 /avxxxxxx/
这种 以 /av
开头,后面接一串数字, 这种模式的字符串。
然后,这些字符串全部 替换为 /cn345677/
。
1 | names = ''' |
被替换的内容不是固定的,所以没法用 字符串的replace方法。这时,可以使用正则表达式里面的 sub 方法:
1 | import re |
sub 方法也是替换 字符串, 但是被替换的内容 用 正则表达式来表示 符合特征的所有字符串。
第一个参数 /av\d+?/
这个正则表达式,表示以 /av
开头,后面是一串数字,再以 /
结尾的 这种特征的字符串 ,是需要被替换的内容。
第二个参数,这里 是 '/cn345677/'
这个字符串,表示要替换上的内容。
第三个参数是 源字符串。
re.sub
方法——指定替换函数
刚才的例子中,我们用来替换的是一个固定的字符串 /cn345677/
。如果,我们要求,替换后的内容 的是原来的数字+6, 比如 /av66771949/
替换为 /av66771955/
。怎么办?这种更加复杂的替换,我们可以把 sub的第2个参数 指定为一个函数
,该函数的返回值,就是用来替换的字符串。如下
1 | import re |
其中的match
对象由Python自动创建,每一个匹配都会创建一个match对象
获取组内字符串,如下
1 | match.group(0) # 获取整个匹配字符串 |
Python 3.6 以后的版本 ,写法也可以更加简洁,直接像列表一样使用下标,如下
1 | match[0] |
上面这个例子中:
正则表达式 re.sub
函数执行时, 每发现一个匹配的子串就会:
- 实例化一个 match对象。这个match 对象包含了这次匹配的信息, 比如:整个字符串是什么,匹配部分字符串是什么,里面的各个group分组 字符串是什么
- 调用执行 sub函数的第2个参数对象,也就是调用回调函数
subFunc
,并且把刚才产生的 match 对象作为参数传递给subFunc
Python re模块
-
导入模块
1
import re
-
findall
findall返回一个列表
1
2lst = re.findall(r"\d+", "移动客服10086,电信客服10000")
print(lst, type(lst))1
2# 返回一个列表
['10086', '10000'] <class 'list'> -
finditer
finditer返回一个迭代器,从迭代器中拿数据需要用
.group()
1
2itr = re.finditer(r"\d+", "移动客服10086,电信客服10000")
print(itr, type(itr))1
2# 返回一个迭代器
<callable_iterator object at 0x000001C373DF2B20> <class 'callable_iterator'>1
2
3
4
5for i in iter:
print(i.group())
# 输出
10086
10000 -
search
search返回的结果是match对象,拿数据需要
.group()
search全文匹配,并且只返回找到的第一个结果
1
2sch = re.search(r"\d+", "移动客服10086,电信客服10000")
print(sch.group())1
10086
-
match
match是从头开始匹配的,默认在正则前加了一个
^
,不是常用方法1
2
3
4mch = re.match(r"\d+", "移动客服10086,电信客服10000")
print(mch.group())
# 输出会报错,因为mch并没有匹配到 -
预加载正则表达式
预加载正则表达式可以稍稍提高一点程序运行的效率,并且可以多次使用
1
2
3
4obj = re.compile(r"\d+")
lst = obj.findall(r"\d+", "移动客服10086,电信客服10000")
itr = obj.finditer(r"\d+", "移动客服10086,电信客服10000") -
re.S
让.
能匹配换行符1
obj = re.compile(r"xxx", re.S)
-
使用组
使用
?P<group_name>
可以给组起名1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import re
s = """
移动客服:10086;
电信客服:10000;
联通客服:10010。
"""
obj = re.compile(r"^(?P<Name>.*客服).(?P<Number>\d{5}).$",re.M)
itr = obj.finditer(s)
for i in itr:
print(i.group("Name"))
print(i.group("Number"))1
2
3
4
5
6
7# 输出
移动客服
10086
电信客服
10000
联通客服
10010
单行匹配和多行匹配
1 | re.compile(r"", <参数>) |
正则替换
标题加一级
将所有的标题符号 #
, ##
, ###
, …, 全部加一级
- 使用
()
组 - 使用
$1
识别组, 作为替换的内容
1 | # Find |
多个空行变一行
1 | # Find |
行尾空格
1 | # Find |
mmseg–>mymmseg
1 | # Find |
\b
用于匹配单词边界