python核心 - 正则表达式
字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。
正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则, 凡是符合规则的字符串,我们就认为它”匹配”了,否则,该字符串就是不合法的。
我们判断一个字符串是否是一个合法的电话号码分两步,首先创建一个符合电话号码规则的正则表达式, 然后用这个正则表达式来匹配这个字符串是否合法。
基础
因为正则表达式也是用字符串表示,我们就要先学习如何用字符来描述字符。
正则表达式中直接给出字符就是精确匹配,而用\d可匹配数字,\w匹配一个字母或数字。
- ‘00\d’可以匹配’006’,但无法匹配’00A’;
- ‘\d\d\d’可以匹配’123’;
- ‘\w\w\d’可以匹配’py3’;
.可以匹配任意一个字符,所以
- ‘py.’可以匹配’pyc’、’pyo’、’py!’等等
在正则表达式中匹配变长的字符:
- *表示任意个字符(包括0个)
- +表示至少一个字符
- ?表示0个或1个字符
- {n}表示n个字符
- {n,m}表示n-m个字符
来看一个复杂的例子:\d{3}\s+\d{3,8}
我们来从左到右解读一下:
- \d{3}表示匹配3个数字,例如’010’;
- \s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格,例如匹配’ ‘,’ ‘等;
- \d{3,8}表示3-8个数字,例如’1234567’。
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配’.010-12345’这样的号码呢?由于’.’是特殊字符, 在正则表达式中,要用’'转义,所以,上面的正则是’.\d{3}-\d{3,8}’
更多正则式
做更精确地匹配,可以用[]表示范围,还有些事定位字符串位置:
- [0-9a-zA-Z_]可以匹配一个数字、字母或者下划线;
- [a-zA-Z_][0-9a-zA-Z_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
- A|B可以匹配A或B,所以(P|p)ython可以匹配’Python’或者’python’
- ^表示行的开头,^\d表示必须以数字开头
- $表示行的结束,\d$表示必须以数字结束。
所以,py也可以匹配’python’但不匹配’ python’,但是加上^py$就变成了整行匹配, 就只能匹配’py’了,注意从字符串开始匹配。
下面列举一些正则表达式里的元字符及其作用
元字符 | 说明 |
---|---|
. | 代表任意字符 |
\ | 转义 |
[] | 匹配内部的任一字符或子表达式 |
[^] | 对字符集和取非 |
| 定义一个区间
| 匹配前面的字符或者子表达式0次或多次
*? | 惰性匹配上一个
| 匹配前一个字符或子表达式一次或多次
+? | 惰性匹配上一个
? | 匹配前一个字符或子表达式0次或1次重复
{n} | 匹配前一个字符或子表达式
{m,n} | 匹配前一个字符或子表达式至少m次至多n次
{n,} | 匹配前一个字符或者子表达式至少n次
{n,}? | 前一个的惰性匹配
^ | 匹配字符串的开头,MULTILINE下匹配每一行开头
$ | 匹配字符串结束,MULTILINE下匹配下一行前面
\A | 匹配字符串开头
\Z | 匹配字符串结尾
\c | 匹配一个控制字符
\b | 匹配单词边界
\t | 匹配制表符
\s | 匹配任意空白符[\t\r\n\f\v]
\S | 匹配任意非空白符[^ \t\r\n\f\v]
\d | 匹配任意数字
\D | 匹配数字以外的字符
\w | 匹配任意数字字母下划线
\W | 不匹配数字字母下划线
\1 | 匹配内容和group 1一样
re模块
python通过re模块提供对正则表达式的完全支持。由于Python的字符串本身也用\转义, 所以在构造正则表达式字符串的时候,强烈建议使用原始字符串,也就是带r前缀:
1 | s = r'ABC\.001' # Python的字符串 |
匹配
1 | import re |
match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None。
切分字符串
用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:
1 | 'a b c'.split(' ') |
无法识别连续的空格,用正则表达式试试:
1 | re.split(r'\s+', 'a b c') |
无论多少个空格都可以正常分割。加入,试试:
1 | re.split(r'[\s\,]+', 'a,b, c d') |
分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如
^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
1 | >> > m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345') |
如果正则表达式中定义了组,就可以在Match对象上用group()方法提取出子串来。
注意到group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。
贪婪匹配
最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0
1 | re.match(r'^(\d+)(0*)$', '102300').groups() |
由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。
必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配。
1 | re.match(r'^(\d+?)(0*)$', '102300').groups() |
当我们需要在字符串中查找符合正则式的字符串时,可以使用search来代替match,因为match永远是从开头开始匹配, 而search可以从中间开始匹配。
1 | # 贪婪匹配 |
预编译
当我们在Python中使用正则表达式时,re模块内部会干两件事情:
- 编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
- 用编译后的正则表达式去匹配字符串。
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式, 接下来重复使用时就不需要编译这个步骤了,直接匹配
1 | # 日期的正则式 |
编译后生成RE对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串
正则替换
前面讲过匹配用match,搜索用search,现在讲替换用sub。
基本用法:
1 | # 第一种预编译 |
两种用法和之前的match一样,就是在字符串’abcasd’中利用正则式’a’搜索,将搜索到的字符串全部替换成’A’
关于替换还有更多高级主题,还可以自定义替换规则,比如只替换第3个出现的。下面我用示例代码展示:
1 | #!/usr/bin/env python |
高级用法
先来详细讲解扩展用法(?…),一般来讲这个扩展不会创建新的分组group, (?P
不可捕获组
(?:…),匹配括号中的正则式,但是不能再后面通过组来捕获这个东西了, 无论是在正则式里面还是在match对象或替换中都不能通过组来捕获。
命名组
用法为(?P[‘“]).*?(?P=quote)
三种场景使用
场景 | 用法 |
---|---|
正则式本身 | (?P=quote) 或 \1 |
match对象m | m.group(‘quote’) 或 m.end(‘quote’) |
re.sub()替换 | \g或 \g<1> 或 \1 |
lookahead匹配
等于用法为Hello (?=World),匹配”Hello “,当且仅当紧跟着World的时候,匹配完不消耗字符串World。
不等于用法为Hello (?!World),匹配”Hello “,当且仅当后面不是紧跟着World的时候,匹配完不消耗字符串World。
lookbehind匹配
等于用法为(?<=Hello )World,匹配”World”,当且仅当前面紧跟着”Hello “,匹配不消耗Hello 。
不等于用法为(?<!Hello )World,匹配”World”,当且仅当前面不紧跟着”Hello “,匹配不消化Hello 。
条件匹配
(?(id/name)yes-pattern|no-pattern)
如果group id或name存在就匹配yes-pattern否则匹配no-pattern,no-pattern可以省略。
比如,(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)
匹配email: aa@gmail.com
或<aa@gmail.com>
,但不匹配<aa@gmail.com
注:上面使用不可捕获组语法(?:.\w+)+,所以这个正则式只有两个group。永远记住上面写过的(?…)不创建分组group
flags标志
re模块提供很多flags标志来影响匹配行为
用法:
1 | patt = re.compile('[a-zA-Z]+', flags=re.M) |
re.I/IGNORECASE: 忽略大小写
re.M/MULTILINE: 多行模式,主要影响的是^和$
re.S/DOTALL: .可表示换行符
关于正则表达式要讲的内容太多了,这里我介绍了python中最常用的功能,其他更加深入的主题,请参考专业书籍。