正则表达式
有什么用?
大家看一个例子。
一个文本文件里面存储了 一些市场职位信息,格式如下所示
Python3 高级开发工程师 上海互教教育科技有限公司上海-浦东新区2万/月02-18满员
测试开发工程师(C++/python) 上海墨鹍数码科技有限公司上海-浦东新区2.5万/每月02-18未满员
Python3 开发工程师 上海德拓信息技术股份有限公司上海-徐汇区1.3万/每月02-18剩余11人
测试开发工程师(Python) 赫里普(上海)信息科技有限公司上海-浦东新区1.1万/每月02-18剩余5人
Python高级开发工程师 上海行动教育科技股份有限公司上海-闵行区2.8万/月02-18剩余255人
python开发工程师 上海优似腾软件开发有限公司上海-浦东新区2.5万/每月02-18满员
现在,我们需要写一个程序,从这些文本里面抓取 所有职位的薪资。
就是要获取这样的结果
怎么做?
大家先自己思考一下。
这是典型的字符串处理。
分析这里面的规律,可以发现,薪资的数字 后面 都有关键字 万/月
或者 万/每月
根据我们学过的知识,我们不难写出下面的代码
运行一下,发现完全可以。
在你高兴完之后,我们再看看写的代码。
怎么样?
太麻烦了,是不是。
为了从每行获取薪资对应的数字,我们 可是 写了不少行代码。
这种 从字符串中搜索出某种特征的子串
有没有更简单的方法呢?
解决方案就是我们今天要介绍的 正则表达式
。
如果我们使用正则表达式,代码可以这样
运行一下看看,结果是一样的。
但是代码却简单多了。
正则表达式,是一种语法,用来描述你想搜索的字符串的特征。
这里指定了一个正则表达式
([\d.]+)万/每{0,1}月
,就是正则表达式字符串,指定了 搜索子串的特征。
为什么这么写? 我们后面再介绍。
findall 函数返回所有匹配的子串,放在一个列表中。
从 上面的 例子可以看出, 用正则表达式关键的地方在于, 如何写出正确的表达式语法
。
正则表达式非常强大,语法非常复杂,如果你英文阅读能力还可以,那太好了,点击这里,参考Python官方文档里面的描述 。具体的使用细节包括语法都在里面。
本教程会给大家介绍一些常见的正则表达式语法。
在线验证
怎么验证你写的表达式 是否能正确匹配到要搜索的字符串呢?
大家可以访问这个网址: https://regex101.com/
按照下面的示意图片输入 搜索文本 和 表达式,查看你的表达式是否能正确匹配到字符串。
上面的验证网站在国外,如果你访问不了,试试这个国内正则表达式验证网站
常见语法
写在正则表达式里面的普通字符都是表示: 直接匹配它们。
比如 你下面的文本中,如果你要找所有的 test, 正则表达式就非常简单,直接输入 test 即可。
如下所示:
汉字也是一样,要寻找汉字,直接写在正则表达式里面就可以了。
但是有些特殊的字符,术语叫 metacharacters(元字符)。
它们出现在正则表达式字符串中,不是表示直接匹配他们, 而是表达一些特别的含义。
这些特殊的元字符包括下面这些:
我们分别介绍一下它们的含义:
点-匹配所有字符
.
表示要匹配除了 换行符
之外的任何 单个
字符。
比如,你要从下面的文本中,选择出所有的颜色。
也就是要找到所有 以 色
结尾,并且包括前面的一个字符的 词语。
就可以这样写正则表达式 .色
。
其中 点 代表了任意的一个字符, 注意是一个字符。
.色
合起来就表示 要找 任意一个字符 后面是 色 这个字, 合起来两个字的 字符串
验证一下,如下图所示
只要表达式正确,就可以写在Python代码中,如下所示
运行结果如下
高亮行 re.compile
函数返回的是 正则表达式对象
,它对应的匹配规则在参数中进行定义
后面 可以 调用这个对象的 findall
方法来进行正则表达式搜索操作。
如果你只是 一次性 使用 这个匹配规则,可以不用先 re.compile
产生正则表达式对象,
可以直接使用 re.findall
函数,如下
代码更简洁.
星号-重复匹配任意次
*
表示匹配前面的子表达式任意次,包括0次。
比如,你要从下面的文本中,选择每行逗号后面的字符串内容,包括逗号本身。注意,这里的逗号是中文的逗号。
就可以这样写正则表达式 ,.*
。
- 紧跟在 . 后面, 表示 任意字符可以出现任意次, 所以整个表达式的意思就是在逗号后面的 所有字符,包括逗号
验证一下,如下图所示
特别是最后一行,猴子逗号后面没有其它字符了,但是*表示可以匹配0次, 所以表达式也是成立的。
只要表达式正确,就可以写在Python代码中,如下所示
运行结果如下
注意, .* 在正则表达式中非常常见,表示匹配任意字符任意次数。
当然这个 * 前面不是非得是 点 ,也可以是其它字符,比如
加号-重复匹配多次
+
表示匹配前面的子表达式一次或多次,不包括0次。
比如,还是上面的例子,你要从文本中,选择每行逗号后面的字符串内容,包括逗号本身。
但是 添加一个条件, 如果逗号后面 没有内容,就不要选择了。
比如,下面的文本中,最后一行逗号后面 没有内容,就不要选择了。
就可以这样写正则表达式 ,.+
。
验证一下,如下图所示
最后一行,猴子逗号后面没有其它字符了,+表示至少匹配1次, 所以最后一行没有子串选中。
问号-匹配0-1次
?
表示匹配前面的子表达式0次或1次。
比如,还是上面的例子,你要从文本中,选择每行逗号后面的1个字符,也包括逗号本身。
就可以这样写正则表达式 ,.?
。
验证一下,如下图所示
最后一行,猴子逗号后面没有其它字符了,但是?表示匹配1次或0次, 所以最后一行也选中了一个逗号字符。
花括号-匹配指定次数
花括号表示 前面的字符匹配 指定的次数
。
比如 ,下面的文本
表达式 油{3}
就表示匹配 连续的 油 字 3次
表达式 油{3,4}
就表示匹配 连续的 油 字 至少3次,至多 4 次
就只能匹配 后面的,如下所示:
贪婪模式和非贪婪模式
我们要把下面的字符串中的所有html标签都提取出来,
得到这样的一个列表
很容易想到使用正则表达式 <.*>
写出如下代码
source = '<html><head><title>Title</title>'
import re
p = re.compile(r'<.*>')
print(p.findall(source))
但是运行结果,却是
怎么回事? 原来 在正则表达式中, '*', '+', '?' 都是贪婪地,使用他们时,会尽可能多的匹配内容,
所以, <.*>
中的 星号(表示任意次数的重复),一直匹配到了 字符串最后的 </title>
里面的e。
解决这个问题,就需要使用非贪婪模式,也就是在星号后面加上 ?
,变成这样 <.*?>
代码改为
再运行一遍,就可以了
对元字符的转义
反斜杠 \
在正则表达式中有多种用途。
比如,我们要在下面的文本中搜索 所有点前面的字符串,也包含点本身
如果,我们这样写正则表达式 .*.
, 聪明的你肯定发现不对劲。
因为 点 是一个 元字符, 直接出现在正则表达式中,表示匹配任意的单个字符, 不能表示 . 这个字符本身的意思了。
怎么办呢?
如果我们要搜索的内容本身就包含元字符,就可以使用 反斜杠进行转义。
这里我们就应用使用这样的表达式: .*\.
示例,Python程序如下
content = '''苹果.是绿色的
橙子.是橙色的
香蕉.是黄色的'''
import re
p = re.compile(r'.*\.')
for one in p.findall(content):
print(one)
运行结果如下
匹配某种字符类型
反斜杠后面接一些字符,表示匹配 某种类型
的一个字符。
比如
\d 匹配0-9之间任意一个数字字符,等价于表达式 [0-9]
\D 匹配任意一个非0-9数字的字符,等价于表达式 [^0-9]
\s 匹配任意一个空白字符,包括 空格、tab、换行符等,等价于表达式 [\t\n\r\f\v]
\S 匹配任意一个非空白字符,等价于表达式 [^ \t\n\r\f\v]
\w 匹配任意一个文字字符,包括大小写字母、数字、下划线,等价于表达式 [a-zA-Z0-9_]
缺省情况也包括 Unicode文字字符,如果指定 ASCII 码标记,则只包括ASCII字母
\W 匹配任意一个非文字字符,等价于表达式 [^a-zA-Z0-9_]
反斜杠也可以用在方括号里面,比如 [\s,.] 表示匹配 : 任何空白字符, 或者逗号,或者点
方括号-匹配几个字符之一
方括号表示要匹配 指定的几个字符之一 。
比如
[abc]
可以匹配 a, b, 或者 c 里面的任意一个字符。等价于 [a-c]
。
[a-c]
中间的 - 表示一个范围从a 到 c。
如果你想匹配所有的小写字母,可以使用 [a-z]
一些 元字符 在 方括号内 失去了魔法, 变得和普通字符一样了。
比如
[akm.]
匹配 a k m .
里面任意一个字符
这里 .
在括号里面不在表示 匹配任意字符了,而就是表示匹配 .
这个 字符
如果在方括号中使用 ^
, 表示 非
方括号里面的字符集合。
比如
content = 'a1b2c3d4e5'
import re
p = re.compile(r'[^\d]' )
for one in p.findall(content):
print(one)
[^\d]
表示,选择非数字的字符
输出结果为:
起始、结尾位置 和 单行、多行模式
^
表示匹配文本的 开头
位置。
正则表达式可以设定 单行模式
和 多行模式
如果是 单行模式
,表示匹配 整个文本
的开头位置。
如果是 多行模式
,表示匹配 文本每行
的开头位置。
比如,下面的文本中,每行最前面的数字表示水果的编号,最后的数字表示价格
如果我们要提取所有的水果编号,用这样的正则表达式 ^\d+
上面的正则表达式,使用在Python程序里面,如下所示
content = '''001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80'''
import re
p = re.compile(r'^\d+', re.M)
for one in p.findall(content):
print(one)
注意,compile 的第二个参数 re.M ,指明了使用多行模式,
运行结果如下
如果,去掉 compile 的第二个参数 re.M, 运行结果如下
就只有第一行了。
因为单行模式下,^ 只会匹配整个文本的开头位置。
$
表示匹配文本的 结尾
位置。
如果是 单行模式
,表示匹配 整个文本
的结尾位置。
如果是 多行模式
,表示匹配 文本每行
的结尾位置。
比如,下面的文本中,每行最前面的数字表示水果的编号,最后的数字表示价格
如果我们要提取所有的水果价格,用这样的正则表达式 \d+$
对应代码
content = '''001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80'''
import re
p = re.compile(r'\d+$', re.MULTILINE)
for one in p.findall(content):
print(one)
注意,compile 的第二个参数 re.MULTILINE ,指明了使用多行模式,
运行结果如下
如果,去掉 compile 的第二个参数 re.MULTILINE, 运行结果如下
就只有最后一行了。
因为单行模式下,$ 只会匹配整个文本的结束位置。
竖线-匹配其中之一
竖线表示 匹配 其中之一 。
比如 ,
特别要注意的是, 竖线在正则表达式的优先级是最低的, 这就意味着,竖线隔开的部分是一个整体
比如 绿色|橙
表示 要匹配是 绿色
或者 橙
,
而不是 绿色
或者 绿橙
括号-分组
括号称之为 正则表达式的 组选择。
组
就是把 正则表达式 匹配的内容 里面 其中的某些部分
标记为某个组。
我们可以在 正则表达式中 标记 多个
组
为什么要有组的概念呢?因为有时,我们需要提取 已经匹配的内容里面的 某个部分 。
前面,我们有个例子,从下面的文本中,选择每行逗号前面的字符串,也 包括逗号本身
。
就可以这样写正则表达式 ^.*,
。
但是,如果我们要求 不要包括逗号 呢?
当然不能直接 这样写 ^.*
因为最后的逗号 是 特征 所在, 如果去掉它,就没法找 逗号前面的了。
但是把逗号放在正则表达式中,又会包含逗号。
解决问题的方法就是使用 组选择符 : 括号。
我们这样写 ^(.*),
,结果如下
大家可以发现,我们把要从整个表达式中提取的部分放在括号中,这样 水果 的名字 就被单独的放在 组 group 中了。
对应的Python代码如下
content = '''苹果,苹果是绿色的
橙子,橙子是橙色的
香蕉,香蕉是黄色的'''
import re
p = re.compile(r'^(.*),', re.MULTILINE)
for one in p.findall(content):
print(one)
分组 还可以多次使用。
比如,我们要从下面的文本中,提取出每个人的 名字 和对应的 手机号
可以使用这样的正则表达式 ^(.+),.+(\d{11})
可以写出如下的代码
content = '''张三,手机号码15945678901
李四,手机号码13945677701
王二,手机号码13845666901'''
import re
p = re.compile(r'^(.+),.+(\d{11})', re.MULTILINE)
for one in p.findall(content):
print(one)
当有多个分组的时候,我们可以使用 (?P<分组名>...)
这样的格式,给每个分组命名。
这样做的好处是,更方便后续的代码提取每个分组里面的内容
比如
content = '''张三,手机号码15945678901
李四,手机号码13945677701
王二,手机号码13845666901'''
import re
p = re.compile(r'^(?P<name>.+),.+(?P<phone>\d{11})', re.MULTILINE)
for match in p.finditer(content):
print(match.group('name'))
print(match.group('phone'))
让点匹配换行
前面说过, 点
是 不匹配换行符
的,可是有时候,特征 字符串就是跨行的,比如要找出下面文字中所有的职位名称
<div class="el">
<p class="t1">
<span>
<a>Python开发工程师</a>
</span>
</p>
<span class="t2">南京</span>
<span class="t3">1.5-2万/月</span>
</div>
<div class="el">
<p class="t1">
<span>
<a>java开发工程师</a>
</span>
</p>
<span class="t2">苏州</span>
<span class="t3">1.5-2/月</span>
</div>
如果你直接使用表达式 class=\"t1\">.*?<a>(.*?)</a>
会发现匹配不上,因为 t1
和 <a>
之间有两个空行。
这时你需要 点也匹配换行符
,可以使用 DOTALL
参数
像这样
content = '''
<div class="el">
<p class="t1">
<span>
<a>Python开发工程师</a>
</span>
</p>
<span class="t2">南京</span>
<span class="t3">1.5-2万/月</span>
</div>
<div class="el">
<p class="t1">
<span>
<a>java开发工程师</a>
</span>
</p>
<span class="t2">苏州</span>
<span class="t3">1.5-2/月</span>
</div>
'''
import re
p = re.compile(r'class=\"t1\">.*?<a>(.*?)</a>', re.DOTALL)
for one in p.findall(content):
print(one)
回到开头的例子
有了上面的知识,我们再来看 本文开始的例子
从下面的文本里面抓取 所有职位的薪资。
Python3 高级开发工程师 上海互教教育科技有限公司上海-浦东新区2万/月02-18满员
测试开发工程师(C++/python) 上海墨鹍数码科技有限公司上海-浦东新区2.5万/每月02-18未满员
Python3 开发工程师 上海德拓信息技术股份有限公司上海-徐汇区1.3万/每月02-18剩余11人
测试开发工程师(Python) 赫里普(上海)信息科技有限公司上海-浦东新区1.1万/每月02-18剩余5人
我们使用的表达式是 ([\d.]+)万/每{0,1}月
为什么这么写呢?
[\d.]+
表示 匹配 数字或者点的多次出现 这就可以匹配像: 3 33 33.33 这样的 数字
万/每{0,1}月
是后面紧接着的,如果没有这个,就会匹配到别的数字, 比如 Python3 里面的3。
其中 每{0,1}月
这部分表示匹配 每月
每 这个字可以出现 0次或者1次。
聪明的你能想到,还可以用什么来表示这个 每{0,1}月
吗?
对啦,还可以用 每?月
因为问号表示 前面的字符匹配0次或者1次
切割字符串
字符串 对象的 split
方法只适用于 简单的字符串分割。 有时,你需要更加灵活的字符串切割。
比如,我们需要从下面字符串中提取武将的名字。
我们发现这些名字之间, 有的是分号隔开,有的是逗号隔开,有的是空格隔开, 而且分割符号周围还有不定数量的空格
这时,可以使用正则表达式里面的 split 方法:
正则表达式 [;,\s]\s*
指定了,分割符为 分号、逗号、空格 里面的任意一种均可,并且 该符号周围可以有不定数量的空格。
字符串替换
匹配模式替换
字符串 对象的 replace
方法只适应于 简单的 替换。 有时,你需要更加灵活的字符串替换。
比如,我们需要在下面这段文本中 所有的 链接中 找到所以 /avxxxxxx/
这种 以 /av
开头,后面接一串数字, 这种模式的字符串。
然后,这些字符串全部 替换为 /cn345677/
。
names = '''
下面是这学期要学习的课程:
<a href='https://www.bilibili.com/video/av66771949/?p=1' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是牛顿第2运动定律
<a href='https://www.bilibili.com/video/av46349552/?p=125' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是毕达哥拉斯公式
<a href='https://www.bilibili.com/video/av90571967/?p=33' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是切割磁力线
'''
被替换的内容不是固定的,所以没法用 字符串的replace方法。
这时,可以使用正则表达式里面的 sub 方法:
import re
names = '''
下面是这学期要学习的课程:
<a href='https://www.bilibili.com/video/av66771949/?p=1' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是牛顿第2运动定律
<a href='https://www.bilibili.com/video/av46349552/?p=125' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是毕达哥拉斯公式
<a href='https://www.bilibili.com/video/av90571967/?p=33' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是切割磁力线
'''
newStr = re.sub(r'/av\d+/', '/cn345677/' , names)
print(newStr)
sub 方法就是也是替换 字符串, 但是被替换的内容 用 正则表达式来表示 符合特征的所有字符串。
比如,这里就是第一个参数 /av\d+/
这个正则表达式,表示以 /av
开头,后面是一串数字,再以 /
结尾的 这种特征的字符串 ,是需要被替换的。
第二个参数,这里 是 '/cn345677/'
这个字符串,表示用什么来替换。
第三个参数是 源字符串。
指定替换函数
刚才的例子中,我们用来替换的是一个固定的字符串 /cn345677/
。
如果,我们要求,替换后的内容 的是原来的数字+6, 比如 /av66771949/
替换为 /av66771955/
。
怎么办?
这种更加复杂的替换,我们可以把 sub的第2个参数 指定为一个函数
,该函数的返回值,就是用来替换的字符串。
如下
import re
names = '''
下面是这学期要学习的课程:
<a href='https://www.bilibili.com/video/av66771949/?p=1' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是牛顿第2运动定律
<a href='https://www.bilibili.com/video/av46349552/?p=125' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是毕达哥拉斯公式
<a href='https://www.bilibili.com/video/av90571967/?p=33' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是切割磁力线
'''
# 替换函数,参数是 Match对象
def subFunc(match):
# Match对象 的 group(0) 返回的是整个匹配上的字符串,
src = match.group(0)
# Match对象 的 group(1) 返回的是第一个group分组的内容
number = int(match.group(1)) + 6
dest = f'/av{number}/'
print(f'{src} 替换为 {dest}')
# 返回值就是最终替换的字符串
return dest
newStr = re.sub(r'/av(\d+)/', subFunc , names)
print(newStr)
获取组内字符串,如下
Python 3.6 以后的版本 ,写法也可以更加简洁,直接像列表一样使用下标,如下
上面这个例子中:
正则表达式 re.sub 函数执行时, 每发现一个
匹配的子串, 就会:
- 实例化一个 match对象
这个match 对象包含了这次匹配的信息, 比如:整个字符串是什么,匹配部分字符串是什么,里面的各个group分组 字符串是什么
- 调用执行 sub函数的第2个参数对象,也就是调用回调函数subFunc
并且把刚才产生的 match 对象作为参数传递给 subFunc
课后练习
补充练习
VIP学员请联系老师获取补充练习题