吐血总结—Python 正则表达式(从基础到进阶)

在使用爬虫爬取数据,或者做数据挖掘和分析的时候我们经常会使用到正则表达式,

在Python中使用正则需要导入re

import re

首先我们来看两个例子来体验一下正则表达式的威力吧:

现在有这样一段字符串
<table><tr>hello world 18111234589<tr><tr><span>name:张三,tel:18711001111</span></tr></table>
需要你做两件事:

  1. 提取字符串中<span>标签里的内容;
  2. 提取其中所有的手机号;

如果不用正则我们来完成这两个要求:

s = '<table><tr>hello world 18111234589<tr><tr><span>name:张三,tel:18711001111</span></tr></table>'
#第一题
start = s.find('<span>')
end = s.rfind('</span')

if start != -1:
    print(s[start+len('<span>'):end]) 

上述代码会输出:name:张三,tel:18711001111

第一题很简单嘛,好了,我们来看第二题:提取手机号,首先要了解手机号的规则:

  • 必须是11位的数字;
  • 第一位数字以1开头,第二位数字可以是[3,4,5,7,8]中的任意一个,后面9个是[0-9]中的任意一个数字(现在规则更多了,出现了19**的号码,这里作为例子就简单一点);

现在用Python代码来实现手机号的提取,看到这个字符串和规则,心里想,纳尼??这要我写多少个判断?我选择放弃。

image.png

不过活还是得干,只用Python的判断和循环我肯定不想干这个苦差事,用正则会怎么样呢?

import re

s = '<table><tr>hello world 18111234589<tr><tr><span>name:张三,tel:18711001111</span></tr></table>'

 #第一题 获取<span>标签内的所有数据
info = re.search(r'(?<=<span>).*(?=</span>)',s)
print(info.group(0))

#第二题  提取所有手机号码
phone = re.findall(r'1[34578]\d{9}',s,re.S)
print(phone)

输出的结果为:

name:张三,tel:18711001111
['18111234589', '18711001111']

和查找字符串并判断去找到目标字符串相比,正则就像是开了挂,哦不对,开挂实锤!

在来看个例子:

    张伟 86-14870293148  \n
   王伟   +86-13285654569    \n
    王芳        15856529115    \n
 李伟         13022816340  \n
  王秀英   (86)14785720656     \n
   李秀英    17201444672    \n
    李娜         15682812452     \n
    张秀英         14326967740     \n
    刘伟  15146435743    \n
   张敏        (86)-17712576838   \n
    李静       86 14295083635  \n
    张丽     (+86) 13722348123   \n
   王静         17587918887   \n
  王丽    15493106739    \n
 李强      13786842977   \n
 张静         86-15542304386     \n
    李敏         15642387356 \n
   王敏          18627216756  \n
 王磊       17206185726   \n
    李军      17857426238     \n
   刘洋        17345352790     \n

对于这样一段字符串,现在让你做如下处理:

1.提取所有 11 位数字电话号码

2.提取所有 18 或 13 开头的电话号码

3.提取所有“王”姓同学的名字

4.提取所有“张”姓同学的电话号码

5.重新排版,排版成统一的格式,去掉国家区号。

怎么做呢? 用常规的方法显然特别复杂,而用正则表达式就要方便很多,为此咱们就需要系统的学习正则表达式了。

要熟练的使用正则,咱们必须从基础的开始,就像以前背诵99乘法表一样,当你使用正则像,一一得一,一二得二一样游刃有余了,就差不多了。

Python正式表达式基础

正则表达式是一个以简单直观的方式通过寻找模式匹配文本的工具;

使用正则表达式有两个常见的原因。

  1. 第一个原因是数据挖掘——也就是说,当我们希望在一大堆文本中找到一小堆文本,例如:身份证,手机号,e-mail等;
  2. 第二个原因是验证,可以使用正则表达式确认获得的数据是你所期望的,如用户验证,密码长度和格式的验证;

Python 的 re 模块,使得Python具备了使用全部正则表达式的功能

re模块具有很多方法,可以让我们灵活的使用正则表达式,现在咱们的任务就是学习这些方法的使用。

Python中的正则表达式

首先我们学习第一个函数,search()函数,它的目的是接收一个正则表达式和一个字符串,并返回发现的第一个匹配的字符串,

import re

a = re.search(r'fox','the quick brown fox jumpred')  #第一个参数为正则表达式,第二个参数为要处理的字符串
print(a.span())      # span方法获取的是正则表达式匹配到的位置

b =  re.search(r'www','the quick brown fox jumpred') 
print(b)   #如果匹配不到则会返回None
(16, 19)
None

找到多个匹配

re.search 的一个限制是它仅仅返回最近一个以 match 对象形式的匹配,如果在一个字符串内存在多个匹配,re.search()只会返回第一个匹配,一般情况下这也是我们期望的结果,但是,有时候需要当多个匹配存在时进行多个匹配。

如果有这种需求咱们可以使用 findall 或者 finditer。两个方法的区别在于 findall 返回的是一个列表,finditer返回的是一个生成器。

l = re.findall(r'张','张三 张三丰 张无忌 张小凡')

print(l)  
['张', '张', '张', '张']

在这个例子中,我们会发现findall返回了所有匹配的值,这是因为“张”字在后面的字符串中出现了4次。

基础正则表达式

最简单的正则表达式是哪些仅包含简单字母数字字符的表达式——不包含任何其他字符。

字符串Python是一个有效的正则表达式,它仅匹配单词,默认正则表达式区分大小写,例如:

a = re.search('Python','I Like python')

print(a)

b = re.search('Python','I LIKE PYTHON')

print(b)
None
None

不过如果只是使用正则表达式来匹配文本,没有什么实际的意义,毕竟检测一个文本中是否有另一个字符串本来就非常简单。

正则表达式强大的地方在于能够指定用于匹配的文本模式。

字符组

字符组允许匹配一组可能出现的字符。

我们看个示例:

a = re.findall(r'[Pp]ython','I like Python3 and I like python2.7 ')

print(a)

['Python', 'python']

可以发现[Pp]既可以匹配大写的P也可以匹配小写的p

接下来在看一段代码:

a = re.findall(r'[Pp]thon','I LIKE Ppython3 and i like ppython2.7')
print(a)
[]

执行会发现输出为空数组,这里值的我们注意的是[Pp]仅匹配一个字符。

1.区间

有一些常见的字符组非常大,比如,我们要匹配的是任意数字,如果依照上述代码,每次我们都需要使用[0123456789] 这种方式明显很不明智,而如果要匹配从a-z的字母,我们也这样编写代码的话,肯定会让我们崩溃。

为了适应这一点,正则表达式引擎在字符组中使用连字符(-)代表区间,所以我们匹配任意数字可以使用[0-9],所以如果我们想要匹配所有小写字母,可以写成[a-z],想要匹配所有大写字母可以写成[A-Z]

可能我们还有个需求:匹配连字符。因为-在会被正则表达式引擎理解为代表连接区间,所以这个时候我们需要对-进行转义。

示例:

a = re.findall(r'[0-9]','xxx007abc')
b = re.findall(r'[a-z]','abc001ABC')
c = re.findall(r'[A-Za-z0-9]','abc007ABC')
d = re.findall(r'[0-9\-]','0edu 007-edu')
print(a)
print(b)
print(c)
print(d)
['0', '0', '7']
['a', 'b', 'c']
['a', 'b', 'c', '0', '0', '7', 'A', 'B', 'C']
['0', '0', '0', '7', '-']

2.取反

到目前我们定义的字符组都是由可能出现的字符定义,不过有时候我们可能希望根据不会出现的字符定义字符组,例如:匹配不包含数字的字符组。

a = re.findall(r'[^0-9]','xxx007abc')
b = re.search(r'[^0-9]','xxx007abc')
print(a)
print(b)
['x', 'x', 'x', 'a', 'b', 'c']
<re.Match object; span=(0, 1), match='x'>

可以通过在字符数组开头使用 ^ 字符实现取反操作,从而可以反转一个字符组(意味着会匹配任何指定字符之外的所有字符)

接下来在看一个表达式:n[^e] 这意味着字符n接下来的字符是除了e之外所有的字符

a = re.findall(r'n[^e]','final')
b = re.search(r'n[^e]','final')
c = re.findall('r[n[^e]]','Python')
print(a)
print(b)
print(c)
['na']
<re.Match object; span=(2, 4), match='na'>
[]

这里我们可以发现ab匹配的是na,字符a因为不是e所以可以被匹配,而变量c的值为空,在这里正则表达式引擎只匹配到了字符串n的位置,而n之后没有任何可以匹配[^e]的字符了,所以这里也匹配失败。

3.快捷方式

几种普通字符组还在正则表达式引擎中有几个预定义的快捷方式,如果我们想要定义单词,以目前学到的可能会使用[A-Za-z],但是,很多单词都是使用该巨剑以外的字符。比如中文,以及其他语言,

正则表达式引擎提供了一些快捷方式:\w ,与 “任意单词字符”匹配,在Python3中,基本上可以匹配任何语言的任意单词。

而当我们想要匹配任意数字的时候也可以使用快捷方式 \d ddigit,在Python3中它除了可以和[0-9]匹配,还可以和其他语言的数字匹配。

\s快捷方式匹配空白字符,比如空格,tab、换行 等。

\b 快捷方式匹配一个长度为0的字符串,但是,他仅仅在一个单词开始或结尾处匹配,这被称为词边界快捷方式。

快捷方式 描述
\w 与任意单词匹配
\d 与任意数字匹配
\s 匹配空白字符,比如空格 tab 换行 等
\b 匹配一个长度为0的子串

示例:

a = re.findall(r'\w','学好Python 大展拳脚')
b = re.search(r'\w','python3')
c = re.search(r'\d','编号89757')
print(a)
print(b)
print(c)
['学', '好', 'P', 'y', 't', 'h', 'o', 'n', '大', '展', '拳', '脚']
<re.Match object; span=(0, 1), match='p'>
<re.Match object; span=(2, 3), match='8'>

这里findall会返回所有能匹配的值,search只会返回第一个匹配到的值。

a = re.findall(r'\bmaster\b','masterxiao-master-xxx master abc')  #单词字符后面或前面不与另一个单词字符直接相邻
b = re.search('r\bmaster\b','master')

print(a)
print(b)
['master', 'master']
None
a = re.search(r'\smaster\s','masterxiao master xxx')
print(a)
<re.Match object; span=(10, 18), match=' master '>

快捷方式取反

之前提到了取反,快捷方式也可以取反, 例如对于 \w的取反为\W,可以发现将小写改写成大写即可。

注意这里\B 有所不同, \b 匹配的是在单词开始或结束位置长度为0的子字符串,而\B匹配不在单词开始和结束位置的长度为0的子字符串。

a = re.findall(r'\Bmaster\B','masterxiao master xxx master abc')  #单词字符后面或前面不与另一个单词字符直接相邻
b = re.search(r'master\B','masterxiao')

print(a)
print(b)
[]
<re.Match object; span=(0, 6), match='master'>

4. 字符串的开始和结束

正则表达式中 ^ 可以表示开始, $表示结束。

a = re.search(r'^python','this code in python3')
b = re.search(r'python$','this code in python3')

c = re.search(r'^python','python is my favorite')
d = re.search(r'python$','this code in python')

print(a)
print(b)
print(c)
print(d)
None
None
<re.Match object; span=(0, 6), match='python'>
<re.Match object; span=(13, 19), match='python'>

通过上述例子,我们可以发现 ^指定的是一个字符串的开始,$指定的是一个字符串的结束。

5.任意字符

.字符最后一个快捷方式字符,它代表匹配任何单个字符,不过值得注意的是,它只能出现在方括号字符组以外

值得注意的是:<font color = "red">.字符只有一个不能匹配的字符,也就是换行(\n),</font>,不过让.字符与换行符匹配也是可能的,以后会讨论。

a = re.search(r'p.th.n','hello python re')
b = re.search(r'p.....','学好 python 人见人爱')

print(a)
print(b)
<re.Match object; span=(6, 12), match='python'>
<re.Match object; span=(3, 9), match='python'>

可选字符

到目前为止,我们看到的正则表达式都是在正则表达式中的字符与被搜索的字符串中的字符保持1:1的关系。

不过有时,我们可能想要匹配一个单词的不同写法,比如“color”和“colour”,或者“honor”与“honour”。

这个时候我们可以使用 ? 符号指定一个字符、字符组或其他基本单元可选,这意味着正则表达式引擎将会期望该字符出现零次或一次

a = re.search(r'honou?r','He Served with honor and distinction')
b = re.search(r'honou?r','He Served with honour and distinction')
c = re.search(r'honou?r','He Served with honou and distinction')
print(a)
print(b)
print(c)
<re.Match object; span=(15, 20), match='honor'>
<re.Match object; span=(15, 21), match='honour'>
None

可以发现,在上述三个例子中,正则表达式为honou?r,这里可以匹配的是 honorhonour 不能匹配 honou,可以知道的是 ? 确定了前一个u是可选的,在第一个示例中,没有u,是没有问题可以匹配的,在第二个示例中,u存在这也没有问题。在第三个例子中,u存在但是r不存在,这样就不能匹配了。

重复

到目前为止,我们只是学习了关于仅出现一次的字符串匹配,在实际开发过程中,这样肯定不能满足需求,比如要匹配电话号码,比如匹配身份证号,这些都是很多个数字组成的。

如果遇到这样的情况,我们可能期望一个字符组连续匹配好几次。

在正则表达式在一个字符组后加上{N} 就可以表示 {N} 之前的字符组出现N次。

re.findall(r'[\d]{4}-[\d]{7}','张三:0731-8825951,李四:0733-8794561')
['0731-8825951', '0733-8794561']

1. 重复区间

可能有时候,我们不知道具体要匹配字符组要重复的次数,比如身份证有15位也有18位的。

这里重复区间就可以出场了,语法:{M,N}M是下界而N是上界。

举个例子:

a = re.search(r'[\d]{3,4}','0731')
b = re.search(r'[\d]{3,4}','073')

print(a)
print(b)
<re.Match object; span=(0, 4), match='0731'>
<re.Match object; span=(0, 3), match='073'>

通过上述代码,我们发现[\d]{3,4} 既可以匹配3个数字也可以匹配4个数字,不过当有4个数字的时候,优先匹配的是4个数字,这是因为正则表达式默认是贪婪模式,即尽可能的匹配更多字符,而要使用非贪婪模式,我们要在表达式后面加上 ?号。

a = re.search(r'[\d]{3,4}?','0731')
b = re.search(r'[\d]{3,4}?','073')

print(a)
print(b)
<re.Match object; span=(0, 3), match='073'>
<re.Match object; span=(0, 3), match='073'>

值得注意的是,上述代码这样子使用就只能匹配3个数字而无法匹配4个了,

2. 开闭区间

有时候我们可能遇到字符组的重复次数没有边界:

a = re.search(r'[\d]{1,}','0731 xxx')

print(a)
<re.Match object; span=(0, 4), match='0731'>

闭区间不写即可表示匹配一个或多个。

3. 速写

还可以使用两个速写字符指定常见的重复情况,可以使用 + 匹配1个或多个,使用 *代表0个或多个。

a = re.findall(r'[\d]+','0731-8859456')
b = re.findall(r'[\d]*','编号89758')

print(a)
print(b)
['0731', '8859456']
['', '', '89758', '']

分组

在Python正则中还提供了一种机制将表达式——分组,当使用分组时,除了获得整个匹配。还能够在匹配中选择每一个分组。

要实现分组很简单,使用()即可。

a = re.search(r'([\d]{4})-([\d]{7})','张三:0731-8825951')  #未使用分组

print(a.group())
print(a.groups())
print(a.group(1))
print(a.group(2))
0731-8825951
('0731', '8825951')
0731
8825951

不过电话号码可能很多时候不是我们看到的类似"XXXX-XXXXXXX" 这种形式 , 而会是其他形式比如:

2138675509
(213)8675509
213.867.5509
(213)-867-5509
1(213)867-5509
+1-213-867-5509

现在我们要想通过一个正则表达式将这些可能的号码全都匹配出来,应该怎么做呢?

a =  re.search(r'(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})','2138675509')
b =  re.search(r'(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})','(213)8675509')
c =  re.search(r'(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})','213.867.5509')
d =  re.search(r'(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})','(213)-867-5509')
e =  re.search(r'(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})','1(213)867-5509')
f =  re.search(r'(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})','+1-213-867-5509')

print(a.groups())
print(b.groups())
print(c.groups())
print(d.groups())
print(e.groups())
print(f.groups())
('', '213', '867', '5509')
('', '213', '867', '5509')
('', '213', '867', '5509')
('', '213', '867', '5509')
('1', '213', '867', '5509')
('+1', '213', '867', '5509')

相比我们之前遇到过的正则,这个相对来说复杂很多,不过我们把它拆分成四个模块就很好理解了:(\+?1?)?[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})

  • 第一部分:(\+?1?)?[ .-]? 首先()代表的是一个分组,\+代表匹配的+\是转义符,后面加了?表示非贪婪模式,即仅匹配一个+号,1?表示仅匹配一个1[ .-]? 则表示仅匹配一个空格或者.或者-连字符,总的来说第一部分匹配的就是遇到的任何形式的 +11 ,或可能是连字符。

  • 第二部分:\(?([\d]{3})\)?[ .-]?, \(? 表示仅匹配一个(号,([\d]{3})匹配三个数字,\)?表示仅匹配一个)号。接着匹配一个连字符或者空格。

相信理解了前两部分后面,后面部分也能理解。

理解了这个例子咱们做个练习:识别中国的电话号码,有数据如下:
中国区号码规则如下: (+)86-0XX(0XXX)-XXX-XXXX(XXXX-XXXX)

+86-010-82866931
86010-82866931
010 8286 6931
86(010)-69675217
86(0731)-728-3333
(0731)-2728-3353

请你使用正则表达式将这些号码提取成分组形式

1. 命名分组

一个命名分组的语法是在开始的“(”之后,添加?P<group_name>,来实现分组,例如:

a = re.search(r'[\d]{4}-[\d]{7}','张三:0731-8825951')  #未使用分组
b = re.search(r'(?P<first_group>[\d]{4})-(?P<second_group>[\d]{7})','张三:0731-8825951') #使用分组
print(a)
print(a.group())
print(a.groups())


print(b)
print(b.group())
print(b.groups())
print(b.group('first_group'))
print(b.group('second_group'))
print(b.group(0))
print(b.group(1))
print(b.group(2))
print(b.groupdict())
<re.Match object; span=(3, 15), match='0731-8825951'>
0731-8825951
()
<re.Match object; span=(3, 15), match='0731-8825951'>
0731-8825951
('0731', '8825951')
0731
8825951
0731-8825951
0731
8825951
{'first_group': '0731', 'second_group': '8825951'}

re.search()方法返回的是一个re.Match对象,运行上述例子我们可以发现,使用 ?P<group_name> 可以实现分组, 使用 .group(group_name)将分组名传入group()函数可以获取对应名称分组的数据,同样使用索引也是可以获取对应分组的数据的。

Match对象还提供了一个groupdict()方法,该方法在和groups()方法类似,不过它返回的是一个字典,而groups()放回的是一个元祖。

不过值得注意的是:groupdict()只会返回命名分组而不会返回非命名分组

match = re.search(r'(?P<first_group>[\d]{4})-([\d]{7})','张三:0731-8825951') #使用分组

print(match.group())
print(match.groups())

print(match.groupdict())
0731-8825951
('0731', '8825951')
{'first_group': '0731'}

在上述例子中,只有第一个是命名分组,第二个是编码分组,所以当调用groups()时,这两个分组都在元祖中返回,但是,当调用groupdict时,这两个分组则只有命名分组被返回了,如果从可维护的角度来说,命名分组十分有价值。

1. 引用已经存在的分组

正则表达式还提供了一种引用一个之前匹配分组的机制,有些时候,我们或许会寻找到一个子匹配,该匹配会接下来再次出现。

例如,如果我们尝试解析一段XML代码,比如:<font>提示</font>,我们可能会编写出这样一段代码:

a = re.search(r'<[\w_-]+>提示</[\w_-]+>','<font>提示</font>')

print(a)
<re.Match object; span=(0, 15), match='<font>提示</font>'>

上述代码确实可以匹配,不过也存在另一种情况,如果解析的是如下数据:<font>提示</bar>

a = re.search(r'<([\w_-]+)>提示<(/[\w_-]+)>','<font>提示</bar>')

print(a.group(1))
print(a.group(2))
font
/bar

这个时候我们可能直觉的想让后面分组的正则也匹配font,不过实际情况确是,所有形式的都会匹配,那如果我们想让后面的正则和第一个分组的正则该如何做呢?

可以使用分组引用,使用\N即可回溯引用编号为N的分组,因此上述例子的代码我们可以改为:

a = re.search(r'<([\w_-]+)>提示</\1>','<font>提示</bar>')
b = re.search(r'<([\w_-]+)>提示</\1>','<font>提示</font>')

print(a)
print(b.group())
print(b.groups())
None
<font>提示</font>
('font',)

通过上面的例子,我们可以发现回溯引用取代了第二个分组。所以与font不同的就无法匹配了;

值的注意的是:这里只是一个作为介绍回溯引用的例子,在实际开发中我们不应该用这种方式去解析XML文档,可以使用beautifulSoap等包去解析XML文件。

先行断言

取反先行断言

a = re.findall(r'n(?!e)','final')
b = re.findall(r'n(?!e)','python')

print(a)
print(b)
['n']
['n']

正向先行断言

a = re.findall(r'n(?=e)','final')
b = re.findall(r'n(?=e)','python')
c = re.findall(r'n(?=e)','jasmine')

print(a)
print(b)
print(c)
[]
[]
['n']

标记

不区分大小写

最简单直接的标记是 re.IGNORECASE,他会导致正则表达式变为不区分大小写。

a = re.search(r'python',"I LIKE PYTHON",re.IGNORECASE)

b = re.search(r'python','I LIKE PYTHON',re.I)

print(a)
print(b)
<re.Match object; span=(7, 13), match='PYTHON'>
<re.Match object; span=(7, 13), match='PYTHON'>

点匹配换行符

re.DOTALL标记(别名为re.S)可以让 .字符除了匹配其他字符之外,还匹配换行符。

a = re.search(r'.+','hello\nworld')
b = re.search(r'.+','hello\nworld',re.S)
c = re.search(r'.+','hello\nworld',re.DOTALL)

print(a)
print(b)
print(c)
<re.Match object; span=(0, 5), match='hello'>
<re.Match object; span=(0, 11), match='hello\nworld'>
<re.Match object; span=(0, 11), match='hello\nworld'>

多行模式

re.MULTILINE标记(别名为re.M)导致仅能够匹配字符串开始与结束的^$字符可以匹配字符串内任意行的开始与结束

a = re.search(r'^bar','foo\nbar')
b = re.search(r'^bar','foo\nbar',re.M)

print(a)
print(b)

None
<re.Match object; span=(4, 7), match='bar'>

详细模式

re.VERBOSE标记(别名为re.X)允许复杂的正则表达式以更容易的方式表示。

该标记做两件事,首先,他导致所有的空白(除了字符组中)被忽略,包括换行符。其次,它将#字符(同样,除非在字符组内)当做注释字符。

a = re.search(r'(?P<first>[\d]{3})-(?P<second>[\d]{4})','867-5556')
b = re.search(r"""(?P<first>[\d]{3})
            -    #匹配一个  - 连接符
            (?P<second>[\d]{4})  # 匹配四个数字
             """,
             '879-5578',re.X)

print(a)
print(b)
<re.Match object; span=(0, 8), match='867-5556'>
<re.Match object; span=(0, 8), match='879-5578'>

调试模式

re.DEBUG 标记(没有别名)在编译正则表达式时将一些调试信息输出到sys.stderr

a = re.search(r'(?P<first>[\d]{3})-(?P<second>[\d]{4})','867-5556',re.DEBUG)

print(a)
SUBPATTERN 1 0 0
  MAX_REPEAT 3 3
    IN
      CATEGORY CATEGORY_DIGIT
LITERAL 45
SUBPATTERN 2 0 0
  MAX_REPEAT 4 4
    IN
      CATEGORY CATEGORY_DIGIT

 0. INFO 4 0b0 8 8 (to 5)
 5: MARK 0
 7. REPEAT_ONE 9 3 3 (to 17)
11.   IN 4 (to 16)
13.     CATEGORY UNI_DIGIT
15.     FAILURE
16:   SUCCESS
17: MARK 1
19. LITERAL 0x2d ('-')
21. MARK 2
23. REPEAT_ONE 9 4 4 (to 33)
27.   IN 4 (to 32)
29.     CATEGORY UNI_DIGIT
31.     FAILURE
32:   SUCCESS
33: MARK 3
35. SUCCESS
<re.Match object; span=(0, 8), match='867-5556'>

使用多个标记

可能我们还需要同时使用多个标记,为了完成这点,可以使用|操作符。正确的语法是例如:re.DOTALL|re.MULTILINEre.S | re.M

内联标记

标记也可以简写,在一个正则表达式里使用标记也是可能的:

a = re.search('(?i)FOO','foo')
print(a)
<re.Match object; span=(0, 3), match='foo'>

可以发现这里的(?i),就等同于使用re.IGNORECASE 标记。

替换

正则表达式引擎并不仅仅局限于识别一个模式是否在字符串中存在,它还能够执行字符串替换,基于在原始字符串中的分组返回一个新字符串。

Python中的替换方法是re.sub,他接收三个参数,正则表达式,用于替换的字符串,被搜索的字符串。

只要实际匹配被替换,如果没有匹配,则re.sub最终不执行任何操作。

re.sub允许从被替换的字符串中的正则表达式模式使用同样的回溯引用,接下来我们看一个从电话号码中剥离无关格式数据的任务。

a = re.sub(r'[\d]+',r'a','213-667-8890')

b = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'\2\3\4','213-667-8890')

c = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'\2\3\4','(213)8675509')

d = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'\2\3\4','213.867.5509')

e = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'\2\3\4','+1-213-854-5557')

f = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'\2\3\4','1213-854-5557')


print(a)
print(b)
print(c)
print(d)
print(e)
print(f)
a-a-a
2136678890
2138675509
2138675509
2138545557
2138545557
# 还可以利用sub的功能使所有的电话号码格式一致
b = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'(\2)\3-\4','213-667-8890')

c = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'(\2)\3-\4','(213)8675509')

d = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'(\2)\3-\4','213.867.5509')

e = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'(\2)\3-\4','+1-213-854-5557')

f = re.sub(r'(\+?1?)[ .-]?\(?([\d]{3})\)?[ .-]?([\d]{3})[ .-]?([\d]{4})',r'(\2)\3-\4','1213-854-5557')

print(b)
print(c)
print(d)
print(e)
print(f)


(213)667-8890
(213)867-5509
(213)867-5509
(213)854-5557
(213)854-5557

可以发现被替换的字符串不仅限于使用于对字符串的回溯引用,其他字符按字面被解释,这个例子也可以看出re.sub也可以用于格式化数据,是多种不同的数据显示

已编译的正则表达式

re模块包含一个函数:compile,它返回一个已编译的正则表达式对象,该对象之后可以被复用。

regex = re.compile(r'[\d]{3,4}')

a = regex.search('578')
b = regex.search('编号8975')

print(a)
print(b)
<re.Match object; span=(0, 3), match='578'>
<re.Match object; span=(2, 6), match='8975'>

使用已编译的正则表达式有两个好处:

  1. 可用于作为方法的参数被传递

  2. 允许使用在re.search允许使用在re.search中不可用的两个额外参数,这两个参数分别是被搜索字符串的开始和结束位置,他们可用来减少对部分字符串的考虑。

regex = re.compile('[\d]+')
a = regex.search('1 编号89757,从这一刻就是你给我的姓名')
b = regex.search('1 编号89757,从这一刻就是你给我的姓名',pos=2)

#pos:匹配的起始位置,可选,默认为0
#endpos:匹配的结束位置,可选,默认为 len(string)
print(a)
print(b)
<re.Match object; span=(0, 1), match='1'>
<re.Match object; span=(4, 9), match='89757'>

正则表达式练习

纸上得来终觉浅,欲知此事须躬行,接下来我们做一些练习,来运用学到的知识:

第一题:

原始文本:

    张伟 86-14870293148  \n
   王伟   +86-13285654569    \n
    王芳        15856529115    \n
 李伟         13022816340  \n
  王秀英   (86)14785720656     \n
   李秀英    17201444672    \n
    李娜         15682812452     \n
    张秀英         14326967740     \n
    刘伟  15146435743    \n
   张敏        (86)-17712576838   \n
    李静       86 14295083635  \n
    张丽     (+86) 13722348123   \n
   王静         17587918887   \n
  王丽    15493106739    \n
 李强      13786842977   \n
 张静         86-15542304386     \n
    李敏         15642387356 \n
   王敏          18627216756  \n
 王磊       17206185726   \n
    李军      17857426238     \n
   刘洋        17345352790     \n

要求如下:

1.提取所有 11 位数字电话号码

2.提取所有 18 或 13 开头的电话号码

3.提取所有“王”姓同学的名字

4.提取所有“张”姓同学的电话号码 格式为 张XX的电话号码是: XXXXXX

5.重新排版,排版成统一的格式,去掉国家区号。

第二题:

原始文本:

  张伟         1996.8.24 
   王伟      1993年10月21日   
 王芳         1997-7-24   
   李伟       1996.3.21  
   王秀英        1991.12.0  
   李秀英     1994-7-5    
   李娜       1999.1.28   
   张秀英        1998-2-24   刘伟     1996.5.28      张敏      1996.10.26   
  李静      1993年1月6日    
   张丽        1992.5.21     王静  1998-5-1  
    王丽   1994-4-14  
   李强  1993.8.13   
   张静          1998年1月5日   李敏    1993-8-21  
   王敏      1997-3-7    
   王磊     1999-3-18    
  李军         1990-12-18  
    刘洋   1995年5月7日  
  1. 提取所有日期
  2. 提取所有 1996 年以前出生的学生
  3. 重新排版 要求以:张伟: 1990 年 9 月 15 日 格式
  4. 把所有 1996 年以前出生的学生出生年份改为 1996 并排版
  5. 提取生日 格式:张伟的生日是11月15号
import re
#第一题
# 1.提取所有 11 位数字电话号码

# 2.提取所有 18 或 13 开头的电话号码

# 3.提取所有“王”姓同学的名字

# 4.提取所有“张”姓同学的电话号码

# 5.重新排版,排版成统一的格式,去掉国家区号。

str = """
    张伟 86-14870293148  \n
   王伟   +86-13285654569    \n
    王芳        15856529115    \n
 李伟         13022816340  \n
  王秀英   (86)14785720656     \n
   李秀英    17201444672    \n
    李娜         15682812452     \n
    张秀英         14326967740     \n
    刘伟  15146435743    \n
   张敏        (86)-17712576838   \n
    李静       86 14295083635  \n
    张丽     (+86) 13722348123   \n
   王静         17587918887   \n
  王丽    15493106739    \n
 李强      13786842977   \n
 张静         86-15542304386     \n
    李敏        15642387356 \n
   王敏          18627216756  \n
 王磊       17206185726   \n
    李军      17857426238     \n
   刘洋        17345352790     \n
"""

a = re.findall(r'[\d]{11}',str)
b = re.findall(r'1[8|3][\d]{9}',str)
c = re.findall(r'王\S*',str)

d = re.findall(r'(张\S*)\s+\(?(\+?86?)?\)?[ .-]?([\d]{11})',str)

e = re.sub(r'\s+(\w\S+)\s+\(?(\+?86?)?\)?[ .-]?([\d]{11})',r'\1  \3\n',str)

print(a)
print(b)
print(c)
for line in d:
    print(line[0] + "的电话号码是:" + line[2])

print(e)


['14870293148', '13285654569', '15856529115', '13022816340', '14785720656', '17201444672', '15682812452', '14326967740', '15146435743', '17712576838', '14295083635', '13722348123', '17587918887', '15493106739', '13786842977', '15542304386', '15642387356', '18627216756', '17206185726', '17857426238', '17345352790']
['13285654569', '13022816340', '13722348123', '13786842977', '18627216756']
['王伟', '王芳', '王秀英', '王静', '王丽', '王敏', '王磊']
张伟的电话号码是:14870293148
张秀英的电话号码是:14326967740
张敏的电话号码是:17712576838
张丽的电话号码是:13722348123
张静的电话号码是:15542304386
张伟  14870293148
王伟  13285654569
王芳  15856529115
李伟  13022816340
王秀英  14785720656
李秀英  17201444672
李娜  15682812452
张秀英  14326967740
刘伟  15146435743
张敏  17712576838
李静  14295083635
张丽  13722348123
王静  17587918887
王丽  15493106739
李强  13786842977
张静  15542304386
李敏  15642387356
王敏  18627216756
王磊  17206185726
李军  17857426238
刘洋  17345352790
#第二题
# 提取所有日期
# 提取所有 1996 年以前出生的学生
# 重新排版
# 把所有 1996 年以前出生的学生出生年份改为 1996
# 提取生日 格式:张伟的生日是11月15号

str = """
  张伟         1996.8.24 
   王伟      1993年10月21日   
 王芳         1997-7-24   
   李伟       1996.3.21  
   王秀英        1991.12.0  
   李秀英     1994-7-5    
   李娜       1999.1.28   
   张秀英        1998-2-24   刘伟     1996.5.28      张敏      1996.10.26   
  李静      1993年1月6日    
   张丽        1992.5.21     王静  1998-5-1  
    王丽   1994-4-14  
   李强  1993.8.13   
   张静          1998年1月5日   李敏    1993-8-21  
   王敏      1997-3-7    
   王磊     1999-3-18    
  李军         1990-12-18  
    刘洋   1995年5月7日  
"""
a = re.findall(r'\d{4}[年.-]?\d{1,2}[月.-]?\d{1,2}\S?',str)  # 提取所有日期
b = re.findall(r'\S+\s+1\d{2}[0-5][年.-]?\d{1,2}[月.-]?\d{1,2}\S?',str)  # 提取所有 1996 年以前出生的学生
c = re.sub(r'\s*(\S+)\s+(\d{4})[年.-]?(\d{1,2})[月.-]?(\d{1,2})\S?',r'\1  \2 年 \3 月 \4 日 \n',str)   #重新排版
d = re.findall(r'(\S+)\s+(1\d{2}[0-6])\S?(\d{1,2})\S?(\d{1,2})\S?',str)
e = re.findall(r'(\S+)\s+\d{4}[年.-]?(\d{1,2})\S?(\d{1,2})\S?',str)


print(a)  
print("\n")
print(b)
print("\n")
print(c)
for line in d:
    print(line[0] + "  " + "1996" + "年" + line[2] + "月" + line[3] + "日" )

print(e)


    ['1996.8.24', '1993年10月21日', '1997-7-24', '1996.3.21', '1991.12.0', '1994-7-5', '1999.1.28', '1998-2-24', '1996.5.28', '1996.10.26', '1993年1月6日', '1992.5.21', '1998-5-1', '1994-4-14', '1993.8.13', '1998年1月5日', '1993-8-21', '1997-3-7', '1999-3-18', '1990-12-18', '1995年5月7日']
    
    
    ['王伟      1993年10月21日', '王秀英        1991.12.0', '李秀英     1994-7-5', '李静      1993年1月6日', '张丽        1992.5.21', '王丽   1994-4-14', '李强  1993.8.13', '李敏    1993-8-21', '李军         1990-12-18', '刘洋   1995年5月7日']
    
    
    张伟  1996 年 8 月 24 日 
    王伟  1993 年 10 月 21 日 
    王芳  1997 年 7 月 24 日 
    李伟  1996 年 3 月 21 日 
    王秀英  1991 年 12 月 0 日 
    李秀英  1994 年 7 月 5 日 
    李娜  1999 年 1 月 28 日 
    张秀英  1998 年 2 月 24 日 
    刘伟  1996 年 5 月 28 日 
    张敏  1996 年 10 月 26 日 
    李静  1993 年 1 月 6 日 
    张丽  1992 年 5 月 21 日 
    王静  1998 年 5 月 1 日 
    王丽  1994 年 4 月 14 日 
    李强  1993 年 8 月 13 日 
    张静  1998 年 1 月 5 日 
    李敏  1993 年 8 月 21 日 
    王敏  1997 年 3 月 7 日 
    王磊  1999 年 3 月 18 日 
    李军  1990 年 12 月 18 日 
    刘洋  1995 年 5 月 7 日 
      
    
    张伟  1996年8月24日
    王伟  1996年10月21日
    李伟  1996年3月21日
    王秀英  1996年12月0日
    李秀英  1996年7月5日
    刘伟  1996年5月28日
    张敏  1996年10月26日
    李静  1996年1月6日
    张丽  1996年5月21日
    王丽  1996年4月14日
    李强  1996年8月13日
    李敏  1996年8月21日
    李军  1996年12月18日
    刘洋  1996年5月7日
    [('张伟', '8', '24'), ('王伟', '10', '21'), ('王芳', '7', '24'), ('李伟', '3', '21'), ('王秀英', '12', '0'), ('李秀英', '7', '5'), ('李娜', '1', '28'), ('张秀英', '2', '24'), ('刘伟', '5', '28'), ('张敏', '10', '26'), ('李静', '1', '6'), ('张丽', '5', '21'), ('王静', '5', '1'), ('王丽', '4', '14'), ('李强', '8', '13'), ('张静', '1', '5'), ('李敏', '8', '21'), ('王敏', '3', '7'), ('王磊', '3', '18'), ('李军', '12', '18'), ('刘洋', '5', '7')]

正则表达式进阶练习

  1. 邮箱验证

  2. 密码强度验证(最少八个字符,至少一个大写字母,一个小写字母和一个数字)

  3. 18 位身份证号验证

  4. IP 地址验证

  5. 手机号验证

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容