Python 正则表达式

正则简介

一、什么是正则表达式?

​ 正则表达式本身与某种编程语言并没有什么实质性的联系,它只是匹配字符串内容的一种规则。官方的定义是,正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

二、应用领域

​ 正则匹配可以应用于用户登录或注册时输入的用户名、密码、邮箱、手机号等信息是否合法;也可以应用于爬虫领域,从爬取的数据中过滤出想要的内容。既然正则表达式是一种匹配字符串的规则,所以正则表达式操作的对象一定是字符串对象。

三、正则在线测试工具

http://tool.chinaz.com/regex

正则语法

一、字符组

在同一个位置可能出现的各种字符组成了一个字符组,在正则表达式中用[ ]表示。字符分为很多类,比如数字、字母、标点等等

[ ] 的使用

例如:

1
2
3
4
正则:      [1aB]
待匹配字符:1ABs
匹配结果:匹配到两个,分别是 1 B
解释:字符组只匹配一个字符的位置,只要目标字符中有出现在字符组范围内的,就能匹配上,如目标字符中的1B
1
2
3
4
正则:      [0-9]
待匹配字符:12AB
匹配结果:匹配到两个,分别是 1 2
解释:也可以用-表示范围,[0-9]就和[0123456789]是一个意思,目标字符串只要出现在数字范围内就能匹配到
1
2
3
4
正则:      [a-z]
待匹配字符:12abs
匹配结果:匹配到三个,分别是 a b s
解释:根据英文字符的ascii码范围去匹配,目标字符串只要出现在a-z范围的字符就能匹配到,[a-z][abcdefghijklmnopqrstuvwxyz]表达的意思一样
1
2
3
4
正则:      [A-Z]
待匹配字符:12ACL
匹配结果:匹配到三个,分别是 A C L
解释:根据英文字符的ascii码范围去匹配,目标字符串只要出现在A-Z范围的字符就能匹配到,[A-Z][ABCDEFGHIJKLMNOPQRSTUVWXYZ]表达的意思一样
1
2
3
4
正则:      [0-9][a-z][A-Z]
待匹配字符:12ACL5dC
匹配结果:匹配到一个,分别是5dC
解释:此时的正则规则是由三个字符组组成,且第一个字符必须是一个数字,第二个必须是小写字母,第三个必须是大写字母,在待匹配的字符串中只能找到 5dC 符合要求
1
2
3
4
正则:      [0-9a-zA-Z]
待匹配字符:12AcL
匹配结果:匹配到五个,分别是 1 2 A c L
解释:正则的范围包括所有的数字和字母,因此都可以匹配到
1
2
3
4
正则:      [abc0-9]
待匹配字符:12AcL
匹配结果:匹配到三个,分别是 1 2 c
解释:匹配a b c或者0-9之间的数字

注:使用 “-” 时,必须由小到大写,如 0-9,a-z,A-Z,不能从大到小写,如 9-0,g-a,Z-B

二、元字符

. 匹配除换行符以外的任意字符

1
2
3
正则:     .
待匹配字符:12AcL
匹配结果:匹配到五个,分别是 1 2 A c L

\w 匹配字母或数字或下划线

1
2
3
正则:		\w
待匹配字符:s_a1@
匹配结果:匹配到四个,分别是 s _ a 1

\s 匹配任意的空白符

\d 匹配数字

\n 匹配一个换行符

\t 匹配一个制表符

\b 匹配一个单词的结尾(边界)

1
2
3
4
正则:		o\b
待匹配字符:hellohello
匹配结果:匹配到1个,结果为第二个hello中的o,即字符串最后一个o
解释:正则中字符o后面紧跟着结尾边界,待匹配字符串中,第一个hello的o后面是字母h,不是结尾,所以只能匹配到最后一个o

^ 匹配字符串的开始

$ 匹配字符串的结尾

1
2
3
4
正则:		^hello$
待匹配字符:hellohello
匹配结果:匹配不到结果
解释:正则中hello单词的前面必须是开始符号,末尾必须是结束符号,待匹配的字符串中找不到对应的字段

\W 匹配非字母或数字或下划线

1
\w 互斥,[\w\W] 表示匹配所有

\S 匹配非空白符

1
\s 互斥,[\s\S] 表示匹配所有

\D 匹配非数字

1
\d 互斥,[\d\D] 表示匹配所有

a|b 匹配字符a或者b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
正则:		a|b
待匹配字符:abc
匹配结果:匹配到2个,结果分别为 a b

注:当出现的选择的范围是包含关系时,要把大的范围写在前面
# 错误写法
正则: abc|abcd
待匹配字符:abcd
匹配结果:匹配到1个,结果分别为abc
解释:正则是从左往右进行匹配的,此时无论是输入 abc 还是 abcd 都会被第一个条件匹配到,第二个条件无法执行

# 修改
正则: abcd|abc
待匹配字符:abcd
匹配结果:匹配到1个,结果分别为abcd
解释:此时输入 abc,使用第二个条件匹配,输入 abcd,使用第一个条件匹配

( ) 匹配括号内的表达式,也表示一个组

1
一般和量词结合使用

[ ] 匹配字符组中的内容

[^ ] 匹配非字符组中的所有内容

1
2
3
4
正则:		[^0-9a-z]
待匹配字符:0aA
匹配结果:匹配到1个,结果分别为A
解释:匹配除了数字和小写字母其他以外的所有字符

三、量词

? 重复一次或者零次(即当前字符可有可无)

+ 重复一次或多次(即当前字符至少出现一次)

* 重复零次或多次

{n} 重复n次

{n, } 至少重复n次

{n,m} 重复n到m次

四、正则匹配实例

. ^ $ 联合使用

正则 待匹配字符 匹配 结果 说明
海. 海燕海娇海东 海燕 海娇 海东 匹配所有"海."的字符
^海. 海燕海娇海东 海燕 只从开头匹配"海."
海.$ 海燕海娇海东 海东 只匹配结尾的"海.$"

? + * {} 联合使用

正则 待匹配字符 匹配 结果 说明
李.? 李杰和李莲英和李二棍子 李杰 李莲 李二 ?表示重复零次或一次,即只匹配"李"后面一个任意字符
李.* 李杰和李莲英和李二棍子 李杰和李莲英和李二棍子 *表示重复零次或多次,即匹配"李"后面0或多个任意字符
李.+ 李杰和李莲英和李二棍子 李杰和李莲英和李二棍子 +表示重复一次或多次,即只匹配"李"后面1个或多个任意字符
李.{1,2} 李杰和李莲英和李二棍子 李杰和 李莲英 李二棍 {1,2}匹配1到2次任意字符

字符集 [] [^]

正则 待匹配字符 匹配 结果 说明
李[杰莲英二棍子]* 李杰和李莲英和李二棍子 李杰 李莲英 李二棍子 表示匹配"李"字后面[杰莲英二棍子]的字符任意次
李[^和]* 李杰和李莲英和李二棍子 李杰 李莲英 李二棍子 表示匹配一个不是"和"的字符任意次
[\d] 456bdha3 4 5 6 3 表示匹配任意一个数字,匹配到4个结果
[\d]+ 456bdha3 456 3 表示匹配任意个数字,匹配到2个结果

分组( ) 或| [^] 联合使用

身份证号码是一个长度为15或18个字符的字符串,如果是15位则全部由数字组成,首位不能为0;如果是18位,则前17位全部是数字,末位可能是数字或x,

正则 待匹配字符 匹配 结果 说明
^[1-9]\d{13,16}[0-9x]$ 110101198001017032 110101198001017032 表示可以匹配一个正确的身份证号
^[1-9]\d{13,16}[0-9x]$ 1101011980010170 1101011980010170 表示也可以匹配这串数字,但这并不是一个正确的身份证号码,它是一个16位的数字
^[1-9]\d{14}(\d{2}[0-9x])?$ 1101011980010170 False 现在不会匹配错误的身份证号了()表示分组,将\d{2}[0-9x]分成一组,就可以整体约束他们出现的次数为0-1次
^([1-9]\d{16}[0-9x]|[1-9]\d{14})$ 110105199812067023 110105199812067023 表示先匹配[1-9]\d{16}[0-9x]如果没有匹配上就匹配[1-9]\d{14}

五、正则表达式小练习

1
2
# 匹配任意长度的正整数
[1-9]\d*
1
2
# 匹配小数
-?\d+\.\d+
1
2
3
4
# 匹配整数或者小数
-?\d+\.?\d* 有缺陷 1. 2.这样的内容都能被匹配上
-?\d+\.?\d+ 有缺陷 1 2 这样的一位数都匹配不上了
-?\d+(\.\d+)? 将小数点和小数分组成一个整体的表达式,对整体进行取零次或者一次的操作,即小数和小数点可有可无,有就是小数,没有就是整数
1
2
# 匹配负数
-0\.\d+|-[1-9]\d*(\.\d+)?
1
2
# 匹配手机号
1[3-9]\d{9}
1
2
# 匹配qq号
[1-9]\d{4,11}
1
2
# 匹配8-10位的用户密码,包含数字字母下划线和?@密码
[\w@?]{8,10}
1
2
# 匹配验证码,4位数字字母组成的
[\da-zA-Z]{4}

六、特殊的用法和现象

量词 + ? 的用法

​ 量词中的 ? + * {n,} 和 {n,m} 都是属于贪婪匹配,即尽可能多的匹配符合规则的字符,例如使用规则 ab{1,4} 去匹配 abbbbbbb,得到的结果是 abbbb,但是实际上 ab,abb,abbb 也是符合正则匹配的规则的,但是最后的结果还是以{1,4}中较大的范围优先,这就是贪婪匹配。

​ 在正则内部,贪婪匹配是根据回溯算法来实现的。如,使用<.*> 规则去匹配<dshdksadk><dsahdksdjks>dhaskdksdjklsdjklsdj ,得到的结果为<dshdksadk><dsahdksdjks>,内部的匹配流程是从左往右分析规则,即从待匹配字符串离开时找,找到第一个 < 号,往后匹配所有字符,即匹配到结尾的 j ,此时规则发现是以 > 结尾的,就回头在找到这个符号为止。

而 “量词+?” 表示尽可能少的去匹配,即惰性匹配。如,使用<.*?> 规则去匹配<dshdksadk><dsahdksdjks>dhaskdksdjklsdjklsdj ,得到的结果为<dshdksadk>,内部实现的流程是规则在开始匹配时就会把 > 带着,一但找到这个符号,就不继续往后找了。

正则 待匹配字符 匹配 结果 说明
李.*? 李杰和李莲英和李二棍子

* 重复零次或多次,尽可能重复多次
*? 尽可能重复零次
李.+? 李杰和李莲英和李二棍子 李杰
李莲
李二
+ 重复一次或多次,尽可能重复多次
+? 尽可能重复一次
李.?? 李杰和李莲英和李二棍子

? 重复一次或零次次,尽可能重复一次
?? 尽可能重复零次次
李.{1,4}? 李杰和李莲英和李二棍子 李杰
李莲
李二
{n, m} 重复n次到m次,尽可能重复m次
{n, m}? 尽可能重复n次
李.{1,}? 李杰和李莲英和李二棍子 李杰
李莲
李二
{n, }重复n次或多次,尽可能重复多次
{n, }? 尽可能重复n次

正则表达式中符号的转义

​ 在字符组中一些特殊的字符会现出原形,如 [()+*?/$.] 就表示匹配对应的字符符号,原来的作用都不会生效,[-] 只有写在字符组的首位的时候表示普通的减号或者负号,写在其他位置的时候表示范围,如 [1 - 9],而所有的 \w \d \s(\n,\t, ) \W \D \S都表示它原本的意义,因此为了避免有的特殊符号在字符组中会产生特殊的作用,我们可以统一在这些符号前添加一个转义符 “\“

1
2
3
[\\d]	表示匹配\或者d
[\-] 表示匹配减号
[\(] 表示匹配括号

Python中使用正则

一、匹配相关

findall 方法

​ 将所有匹配上的内容全部返回,返回值是一个列表;如果没有匹配结果,返回一个空列表

1
2
3
4
5
ret1 = re.findall("\d+", "12sdasd5897dshkda1364")
print(ret1) # ['12', '5897', '1364']

ret2 = re.findall("\d+", "asdsadsa")
print(ret2) # []

search 方法

​ 如果匹配上了,返回一个正则匹配结果的对象,匹配不上返回None;通过group方法从对象中取值,但是只能拿到第一个值。

1
2
3
4
5
6
ret1 = re.search("\d+", "12sdasd5897dshkda1364")
print(ret1) # <_sre.SRE_Match object; span=(0, 2), match='12'> span为匹配到的结果的索引值范围,match为匹配到的结果
print(ret1.group()) # 12

ret2 = re.search("\d+", "asdsadsa")
print(ret2) # None

match 方法

​ 和search方法一样,如果匹配上了,返回一个正则匹配结果的对象,匹配不上返回None;通过group方法从对象中拿到第一个值,但是match必须从头开始匹配

1
2
3
4
5
6
ret1 = re.match("\d+", "12sdasd5897dshkda1364")
print(ret1) # <_sre.SRE_Match object; span=(0, 2), match='12'>
print(ret1.group()) # 12

ret2 = re.match("\d+", "@!12sdasd5897dshkda1364")
print(ret2) # match是从头开始匹配的,代码中要从头开始匹配数字,但是开头是符号,所以匹配失败返回None

二、替换相关

sub 方法

​ 将原字符串中对应字段替换成新规定的内容,返回替换后的结果,默认全部替换,可以指定替换的次数

1
2
3
4
5
6
ret1 = re.sub("\d+", "H", "123sdasd456das789dsa")
print(ret1) # HsdasdHdasHdsa

# 指定替换次数
ret2 = re.sub("\d+", "H", "123sdasd456das789dsa", 2) # 只替换两处
print(ret2) # HsdasdHdas789dsa

subn 方法

​ 将原字符串中对应字段替换成新规定的内容,返回值是一个元组,包含替换后的结果以及替换的次数,默认全部替换,可以指定替换的次数

1
2
3
4
5
6
ret1 = re.subn("\d+", "H", "123sdasd456das789dsa")
print(ret1) # ('HsdasdHdasHdsa', 3)

# 指定替换次数
ret2 = re.subn("\d+", "H", "123sdasd456das789dsa", 2) # 只替换两处
print(ret2) # ('HsdasdHdas789dsa', 2)

三、切割相关

​ 在原字符串中按照匹配的内容进行切割,返回值是一个列表,存放切割后的结果

1
2
ret = re.split("\d+", "dsad123dsada456dsada789dsad")
print(ret) # ['dsad', 'dsada', 'dsada', 'dsad']

四、进阶方法

compile 方法(时间效率)

​ 对于python中的正则机制而言,其流程是先将正则表达式转换成python解释器能够理解的代码,再对字符串进行匹配,因此对于较长或者比较复杂的正则表达式,每次翻译解释就需要耗费一定的时间,如果频繁使用该正则,对整个任务的时间花费将会造成较大的影响。因此可以先使用compile方法,对正则表达式进行预编译,每次使用时直接使用预编译好的对象,从而节省时间。

注:只有在多次使用某一个相同的正则表达式的时候,这个compile才会帮助我们提高程序的效率

1
2
3
4
5
match_num = re.compile("0\.\d+|[1-9]\d*(\.\d+)?")
print(match_num) # re.compile('0\\.\\d+|[1-9]\\d*(\\.\\d+)?') 预编译
# 匹配/切割/替换等方法对于预编译对象皆可用
ret1 = match_num.search("12sdasd545")
print(ret1.group()) # 12

finditer 方法(空间效率)

​ 如果匹配到的内容特别多,一次性读入内存,会影响代码的处理效率,因此可以使用finditer方法,返回一个迭代器,迭代器中存放的是正则匹配结果的对象,逐个取值,节省内存,提高效率。

1
2
3
4
5
6
ret = re.finditer("\d+", "sdas1s23ds45s78s6d5d485")
print(ret) # <callable_iterator object at 0x000001AD72D0C470>
print(ret.__next__()) # <_sre.SRE_Match object; span=(4, 5), match='1'>

for i in ret:
print(i.group())

五、分组相关

分组命名

  • (?P正则表达式) 表示给分组起名字
  • (?P=name)表示使用这个分组,这里匹配到的内容应该和之前定义的分组中的内容完全相同
1
2
3
4
5
6
# 基本使用
ret = re.search("(?P<year>\d+)年(?P<month>\d+)月(?P<day>\d+)日", "今天是2020年02月20日")
print(ret.group()) # 2020年02月20日
print(ret.group("year")) # 2020
print(ret.group("month")) # 02
print(ret.group("day")) # 20
1
2
3
4
5
6
7
8
9
# 在正则规则中使用之前定义的分组
ret = re.search("<(?P<tag>\w+)>\w+</\w+>", "<a>wahahawahaha</b>")
print(ret.group()) # <a>wahahawahaha</b>
print(ret.group("tag")) # a

ret = re.search("<(?P<tag>\w+)>\w+</(?P=tag)>", "<a>wahahawahaha</b>")
print(ret.group()) # AttributeError: 'NoneType' object has no attribute 'group',报错
print(ret.group("tag"))
# 报错是由于正则规则中,定义的分组命名 ?P<tag> 的内容应该是 a,所以后面使用分组时 ?P=tag 对应的值也要是 a,但是待匹配中对应的位置的值是 b,所以没有匹配到结果,返回None,None中没有group方法,所以报错

通过索引使用分组

​ 不用给分组命名,直接使用分组的索引

1
2
3
4
5
6
# 索引是从1开始的,所以分组(\w+) (\d+) (\w+)的索引分别是 1 2 3,在正则规则中想要使用分组的索引,只需要 "\索引" 即可
ret = re.search(r"<(\w+)>(\d+)(\w+)</\1>", "<a>12345sdsad</a>") # python字符串中"\"可能无法转义,可以在字符串最前面加一个r进行强制转义
print(ret.group()) # <a>dshak12345</a>
print(ret.group(1)) # a
print(ret.group(2)) # 12345
print(ret.group(3)) # sdsad

python 中的正则表达式有着一些自己的特性:

  • findall 方法中,正则规则出现分组,匹配的结果会优先显示分组的结果,要想取消分组优先,使用 (?:正则表达式)
1
2
3
4
5
6
ret = re.findall("(\d+)\w", "12HHH456AAAdsjs")
print(ret) # ['12', '456'] 理论上结果应该是 ['12H', '456A'],这就是因为分组结果优先的原因

# 取消分组优先
ret = re.findall("(?:\d+)\w", "12HHH456AAAdsjs")
print(ret)
  • split 方法遇到分组,会保留切割的内容
1
2
3
4
5
6
ret = re.split("(\d+)", "dsa123dsds45ds")
print(ret) # ['dsa', '123', 'dsds', '45', 'ds'] 理论上列表中应该不保留数字

# 取消分组的影响
ret = re.split("(?:\d+)", "dsa123dsds45ds")
print(ret)
  • search中有分组的话,通过group(n)就能够拿到group中的匹配的内容
1
2
3
4
5
6
# 索引是从1开始的,所以分组(\w+) (\d+) (\w+)的索引分别是 1 2 3
ret = re.search(r"<(\w+)>(\d+)(\w+)</\1>", "<a>12345sdsad</a>")
print(ret.group()) # <a>dshak12345</a>
print(ret.group(1)) # a
print(ret.group(2)) # 12345
print(ret.group(3)) # sdsad

补:re 模块中,各个方法中的参数 flags

1
2
3
4
5
6
7
8
9
flags有很多可选值:
re.I(IGNORECASE)忽略大小写,括号内是完整的写法
re.M(MULTILINE)多行模式,改变^和$的行为
re.S(DOTALL)点可以匹配任意字符,包括换行符
re.L(LOCALE)做本地化识别的匹配,表示特殊字符集 \w, \W, \b, \B, \s, \S 依赖于当前环境,不推荐使用
re.U(UNICODE) 使用\w \W \s \S \d \D使用取决于unicode定义的字符属性。在python3中默认使用该flag
re.X(VERBOSE)冗长模式,该模式下pattern字符串可以是多行的,忽略空白字符,并可以添加注释

一般常用前三个
1
2
ret = re.findall("[a-z]", "dsaQWE", flags=re.I)
print(ret) # ['d', 's', 'a', 'Q', 'W', 'E']

Python 正则表达式
https://clark-cdc.github.io/2019/05/10/0014-正则表达式/
作者
clark
发布于
2019年5月10日
许可协议