【python语法】正则表达式
为什么要用正则表达式
对字符串进行操作几乎是每种编程语言中最重要的功能之一。很简单就可以理解,因为人类进行信息传播主要靠的是文字,也就是字符串,但是这么多信息并不完全是我们所要的,所以我们会通过编程来提取或者验证字符串的部分。
正则表达式就是用来匹配字符串的工具,其实它定义了一套语法,用若干描述字符就可以匹配出某段字符串的特征来。凡是符合种描述规则的,我们就认为它匹配。
所以比如我们要判断一串字符是否为合法的Email地址的方法就是:
- 创建一个符合Email特征的
正则表达式
- 然后使用该正则表达式去匹配输入的字符串,以判断是否合法。
正则表达式
元字符
用\d可以匹配一个数字,\w可以匹配一个字母或数字
元字符 | 匹配 |
---|---|
. | 任意字符(但是不包括换行符\n\r等) |
\w | 字母 or 数字 or 下划线 |
\s | 空白符(包括Tab等) |
\d | 数字 |
举个例子 'py.'
可以匹配'pyc'
、'pyo'
、'py!'
等等。因为.
表示的是任意字符,所以可以匹配正常的字母,也可以匹配!
注意一个元字符只代表一个字符,比如\w只代表一个字母或者数字。
可以用[]
表示范围,比如[0-9]
表示匹配0~9之间的任意一个数字
[0-9a-zA-Z\_]
可以匹配一个数字、字母或者下划线,可以等价于\w
有时需要查找不属于某个能简单定义的字符类的字符,这就是反义
代码/语法 | 匹配 |
---|---|
[^x] | 除了x以外的任意字符 |
[^aeiou] | 除了aeiou这几个字母以外的任意字符 |
匹配变长的
如果好匹配变长的字符,用*
表示0个或者以上的字符,用+
表示1个或者以上的字符,用?
表示0个或者1个字符。
还可以用大括号来表示,用{n}表示n个字符,用{n,m}表示n-m个字符。
代码/语法 | 说明 |
---|---|
* | 重复0次以上,等价于 |
+ | 重复1次以上,等价于 |
? | 重复0次或者1次,等价于 |
重复n次 | |
重复n次以上 | |
重复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\_]+
可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100'
,'0_Z'
,'Py3000'
等等; -
[a-zA-Z\_][0-9a-zA-Z\_]*
可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量; -
[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}
更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
注意与
通配符
区分,linux的bash命令行中可以使用通配符,用*
来代理任意个的字符。对于正则表达式而言,必须使用.*
来表示任意个字符
那么对之前电话号码的那个例子,我们可以用更复杂的表达式来匹配\(?0\d{2}[) -]?\d{8}。\(?0\d{2}[) -]?\d{8}。
,可以匹配(010)88886666,或022-22334455,或02912345678等。
- 首先是一个转义字符(,它能出现0次或1次(?),
- 然后是一个0,后面跟着2个数字(\d{2}),
- 然后是)或-或空格中的一个,它出现1次或不出现(?),
- 最后是8个数字(\d
但是这个表达式也能匹配010)12345678或(022-87654321这样的“不正确”的格式。后面会说怎么样修改就可以解决这个问题。
边界限定符
边界限定 | 匹配 |
---|---|
^ | 字符串的开始 |
$ | 字符串的结束 |
比如^\d{5,12}$
表示以数字开头,以数字结尾,整行匹配,同时长度在5~12位一串数字。
分支条件
所谓分支条件就类似逻辑中的“或”,满足任意一个条件即匹配。具体方法是用|
把不同的规则分隔开
比如之前讲过的匹配电话号码的例子。
0\d{2}-\d{8}|0\d{3}-\d{7}
这个表达式能匹配- 三位区号,8位本地号(如010-12345678),
- 4位区号,7位本地号(0376-2233445)。
\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}
:这个表达式被|
分为两个条件- 左边的表达式:
\(0\d{2}\)
可以匹配(010),[- ]?
表示之间的连接符可以为-
,也可以用空格间隔,也可以没有。 - 右边的表达式
0\d{2}[- ]?\d{8}
:表示区号不用小括号括起来。
- 左边的表达式:
注意:匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。
分组
之前提到的是怎么重复单个字符(直接在字符后面加上限定符就行了);
但如果想要重复多个字符又该怎么办?可以用小括号
来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了
比如(\d{1,3}\.){3}\d{1,3}
可以按顺序进行分析,
- \d{1,3}匹配1到3位的数字,
- (\d{1,3}.){3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,
- 最后再加上一个一到三位的数字(\d{1,3})。
总结
相信突然一下出现这么的符号大家一定是懵逼的。下面我们来总结一下{}
, []
, ()
这几种符号的用途。
{2,3}
:需要与它前面的字符结合,比如a{2,3}
表示a
出现2~3次[]
:有3层含义[a-z]
:表示一个范围,也就是a~z
之间的一个
字符[.*]
:只要放入了[]
里面的.*
都不表示之前的含义,只是单纯作为一个普通的符号而已。比如这里面就表示要么为点号
要么为星号
的符号。[^a]
:表示非a
的所有字符。主要不要和^a
混淆,^a
表示以a
开头的一行。
贪婪匹配与懒惰匹配
对a.*b
来说 ,它将匹配最长的以a开始,以b结束的字符串,比如用它来搜索aabab的时候,会匹配整个字符串aabab,这就是贪婪匹配
,也就是尽可能多的匹配
那么懒惰匹配
指的就是尽可能少的匹配字符。在.*
后面加上一个?
以后,可以转换为懒惰匹配模式,那么.*?
意味着使匹配成功的前提下使用最少的重复。比如把它应用于aabab,会匹配aab和ab
为什么第一个匹配是aab而不是ab?因为正则表达式有一条规则:最先开始的匹配拥有最高的优先权
| 代码/语法 | 说明 |
|-|
| *? | 重复任意次,但尽可能少重复 |
| +? | 重复1次或更多次,但尽可能少重复 |
| ?? | 重复0次或1次,但尽可能少重复 |
| {n,m}? | 重复n到m次,但尽可能少重复 |
| {n,}? | 重复n次以上,但尽可能少重复 |
匹配汉字
匹配汉字的表达式为[\u4E00-\u9FA5]
,这是汉字的UTF-8编码的范围。
python调用正则表达式
Python提供re模块,包含所有正则表达式的功能。由于Python的字符串本身也用\转义,所以要特别注意:
比如python字符串s = 'ABC\\-001'
对应的正则表达式变成'ABC\-001'
所以最好把python字符串上加上r
前缀,就不用考虑转义的问题,比如s = r'ABC\-001' # Python的字符串
如何判断正则表达式是否匹配:
- 引入
re
模块:import re
- 使用
match
方法,如果匹配成功,返回一个Match对象,否则返回None
test = '用户输入的字符串'
if re.match(r'正则表达式', test):
print('ok')
else:
print('failed')
切分字符串
使用正则表达式后,切分字符变得更灵活。
如下使用split 的正常切分代码,可以看出无法识别连续的空格
>>> 'a b c'.split(' ')
['a', 'b', '', '', 'c']
使用正则表达式可以实现更复杂的切分:
>>> re.split(r'[\s\,\;]+', 'a,b;; c d')
['a', 'b', 'c', 'd']
分组
除了判断是否匹配
之外,正则表达式可以提取子串
的强大功能。用()表示的就是要提取的分组(Group)。
比如
m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
这个正则表达式定义了两个分组,可以匹配-
前后的两个表达式。
m.group(0)
:获得的是'010-12345'm.group(1)
:获得是“010”m.group(2)
:获得是'12345'
group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。
贪婪匹配
正则表达式默认就是贪婪匹配的。比如
>>> re.match(r'^(\d+)(0*)$', '102300').groups()
#结果是('102300', ''),\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了
必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')
再比如
import re
line = "boooooobby123";
reg_str = ".*(b.*b).*";
match_obj = re.match (reg_str , line);
if match_obj:
print (match_obj.group(1));
因为.*
是贪婪匹配的,所以它会一直匹配到booooooboooooo
,那么小括号里面实际只匹配了bb
如果使用非贪婪模式,也就是在.*
后面加一个?
import re
line = "boooooobby123";
reg_str = ".*?(b.*?b).*";
match_obj = re.match (reg_str , line);
if match_obj:
print (match_obj.group(1));
例子:提取日期
下面我们希望能自动化的把一段文字中的生日
给提取出来,但是如果之前没有规定格式的话,大家会随心所欲的写日期,比如
- 出生于2018年1月23日
- 出生于2018/1/23
- 出生于2018-1-23
- 出生于2018-01-23
- 出生于2018-01
- 出生于2018年01月
下面我们需要给一个正则表达式,要求他能匹配上面所有的日期格式。
- 首先匹配日期中的年的部分,从上面的文本可以看出,只有
2018年
、2018-
,
2018/
这几种形式。也就是可以先用\d{4}
表示数字,再用[年-\]
来表示符号。凑起来就是
regex = r"出生于(\d{4}[年/-])"
- 再来看
月份
的数字部分只可能有01
和1
两种形式:\d{1,2}
月份
后面的部分就相对比较复杂了。同样的,我们可以进行分类列举,然后使用分支条件即可统一表达。- 匹配
2018年1月23日
和2018-01-23
以及2018/1/23
的月
后面的部分:[月/-]\d{1,2}日?
- 匹配
2018年01月
这种的月
后面的部分:[月/-]$
- 匹配
2018-01
的月
后面的部分,当然是直接用结尾符
:$
- 最后用
()
括起来,使用|
进行分类讨论。
- 匹配
([月/-]\d{1,2}日?|[月/-]$|$)
最后把所有的部分合并起来。
import re
lines = [
"出生于2018年1月23日",
"出生于2018/1/23",
"出生于2018-1-23",
"出生于2018-01-23",
"出生于2018-01",
"出生于2018年01月"]
regex = r"出生于(\d{4}[年/-]\d{1,2}([月/-]\d{1,2}日?|[月/-]$|$))"
for line in lines :
m = re.match(regex , line )
if m :
print(m.group(1));
编译
使用正则表达式时,re模块内部会干两件事情:
- 编译正则表达式,此时会进行语法分析,如果表达式本身不合法,会报错;
- 用编译后的正则表达式去匹配字符串。
那么如果一个正则表达式要使用非常多次,可以预编译该正则表达式
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')