正则表达式
一、正则表达式的概念
正则表达式是包含文本和特殊字符的字符串
,该字符串描述一个可以识别各种字符串的模式
正则表达式的强大之处在于引入特殊字符来定义字符集、匹配子组和重复模式。正是由于这些特殊符号,使得正则表达式可以匹配字符串集合,而不仅仅只是某单个字符串
关于正则表达式没有什么理解上的难点,一般用过一次就知道它是拿来干嘛的,所以本篇文章更多的是一本方便大家不熟悉的时候可以即使查找的一篇手册,在你需要的时候打开看看即可
二、特殊字符和符号
1.匹配任意单个字符
.
符号匹配除了换行符\n
以外的任何字符
问:怎样才能匹配
.
符号
答:要显式匹配一个.
符号本身,必须使用反斜线转义句点符号的功能,如\.
2.从字符串起始或者结尾或者单词边界匹配
正则表达式模式 | 匹配的字符串 |
---|---|
^From | 任何以 From 作为起始的字符串 |
From$ | 任何以 From 作为结尾的字符串 |
^From$ | 任何由单独的字符串 From 构成的字符串 |
\bthe | 任何以 the 开始的字符串 |
\bthe\b | 仅仅匹配单词 the |
\Bthe | 任何包含但并不以 the 作为起始的字符串 |
3.创建字符集
虽然.
可以用于匹配任意符号,但某些时候你可能想要匹配某些特定字符
,所以 “科学家们” 又发明了[]
正则表达式模式 | 匹配的字符串 |
---|---|
b[aeiu]t | bat、bet、bit、but |
4.限定范围和否定
[]
两个符号中间用连字符-
连接,用于指定一个字符的范围
,[A-Z]
、[a-z]
或者[0-9]
分别用于表示大写字母、小写字母和数值数字(根据ASCII码
的范围而不仅仅限定用于字母和十进制数字上)- 如果脱字符
^
紧跟在[
后面,^
就表示不匹配给定字符集中的任何一个字符
正则表达式模式 | 匹配的字符串 |
---|---|
[^aeiou] | 匹配一个非元音字符 |
[“-a] | 在ASCII码中位于 " 和 a 之间的字符(即34-97之间的任意一个字符) |
5.使用闭包操作符实现存在性和频数匹配
‘
*
’ 匹配 0 次或者多次前面出现的正则表达式
‘+
’ 匹配 1 次或者多次前面出现的正则表达式
‘?
’ 匹配 0 次或者 1 次前面出现的正则表达式
‘{N}
’ 匹配 N 次前面出现的正则表达式
'{M, N}
'匹配 M~N 次前面出现的正则表达式
6.表示字符集的特殊字符
1.使用特殊字符
d
表示匹配任何十进制数字等价于[0-9]
2.使用特殊字符\w
能够用于表示全部字母数字的字符集,等价于[A-Za-z0-9]
3.使用特殊字符\s
可以用来表示空格字符
注:这些特殊字符的大写版本表示不匹配,如\D
表示任何非十进制数等价于[^0-9]
7.使用圆括号指定分组
我们不仅想要知道整个字符串是否匹配我们的模式,对于任何成功的匹配,我们可能想要看到这些匹配正则表达式模式的字符串究竟是什么,要实现这个目标,只要用一对圆括号包裹任何正则表达式
当使用正则表达式时,一对圆括号可以实现:
- 对正则表达式进行分组
- 匹配子组
三、Python中的正则表达式
re模块:核心函数和方法
1.使用 compile()函数编译正则表达式
在静态脚本语言中,我们缩写的.c文件或者.cpp文件要先被编译(compile)再执行(run),也就是我们通常说的先把英文菜单全部翻译成中文再交给中文厨师去做这道菜,同样的概念也适用于正则表达式——在模式匹配发生之前,正则表达式必须编译成正则表达式对象,这是因为正则表达式在执行过程中将进行多次比较操作,因此强烈建议使用预编译,模块函数会对已编译的对象进行缓存
后序介绍的re
模块的函数几乎等价于regex
对象的方法(对应的方法名和函数名都是相同的),尽管推荐大家使用预编译,但预编译并不是必须的,如果你先进行了预编译,预编译compile
后返回的是一个regex
对象,那么你就可以使用regex
对象的方法,如果你不进行预编译,就是用re
模块为你提供的函数
用了re.compile()
后,正则对象会得到保留,这样在需要多次运用这个正则对象的时候,效率会有较大的提升
2.match() 与 search()
2.1 使用 match()方法匹配字符串
当处理正则表达式时,除了正则表达式对象之外还有另一个对象类型:匹配对象,匹配对象有两个主要的方法:group()
和 groups()
match()
:函数试图从字符串的起始部分对模式进行匹配。如果匹配成功,就返回一个匹配对象
【匹配对象的 group()方法
能够用于显示那个成功的匹配】,匹配失败则返回 None
【匹配失败调用 group()
将会抛出 AttributeError
异常】
import re
m = re.match('food', 'food on the table')
print(m) # 匹配对象:<re.Match object; span=(0, 4), match='food'>
if m is not None:
print(m.group()) # food
2.2 使用 search()在一个字符串中查找模式(搜索与匹配的对比)
其实,想要搜索的模式出现在一个字符串中间部分的概率远大于出现在字符串起始部分的概率,search()
的工作方式与 match()
完全一致,不同之处在于 search()
会用它的字符串参数,在任意位置对给定正则表达式模式搜索第一次出现的匹配情况
import re
# m = re.match('foo', 'seafood') # 匹配失败
m = re.search('foo', 'seafood')
print(m) # <re.Match object; span=(3, 6), match='foo'>
if m is not None:
print(m.group()) # foo
3.匹配多个字符串
|
:择一匹配符,也叫管道符号,对于目标字符串,被|
分割的匹配模式将自左至右逐个被测试,一旦有一个测试成功,后面的将不再被测试,即使后面的匹配模式甚至可以匹配更长的串(可以理解|
是非贪婪的)
import re
text = re.match('bat|bet|bit', 'bat') # <re.Match object; span=(0, 3), match='bat'>
print(text.group()) # bat
text = re.match('bat|bet|bit', 'blt') # None
print(text.group()) # AttributeError: 'NoneType' object has no attribute 'group'
4.匹配任何单个字符
5.创建字符集([ ])
6.重复、特殊字符以及分组
我们之前讨论过使用圆括号来匹配和保存子组,以便于后续处理,请注意如何使用 group()
方法访问每个独立的子组以及 groups()
方法以获取一个包含所有匹配子组的元组
import re
m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
print(m.group()) # abc-123
print(m.group(1)) # abc
print(m.group(2)) # 123
print(m.groups()) # ('abc', '123')
请注意没有加圆括号的情形
import re
m = re.match('ab', 'ab') # 没有子组
print(m.group()) # ab
print(m.groups()) # ()
匹配字符串的起始和结尾以及单词边界
3. findall()和 finditer()
findall()
findall(pattern string)
查询字符串(string)
中某个pattern
全部的出现情况,如果匹配成功findall()
返回一个包含所有成功的匹配部分的列表【从左向右按出现顺序排列】,匹配失败则返回一个空列表,string
会被从左到右依次扫描,返回的列表也是从左到右一次匹配到的。如果 pattern
里含有组的话,那么会返回匹配到的组的列表;
import re
text = re.findall('\w+', 'hello world!')
print(text) # ['hello', 'world']
如果 pattern
里有多个组,那么各组会先组成一个元组,然后返回值将是一个元组的列表
import re
text = re.findall('(\d+)\.(\d+)\.(\d+)\.(\d+)', 'My IP is 192.168.0.2, and your is 192.168.0.3.')
print(text) # [('192', '168', '0', '2'), ('192', '168', '0', '3')]
finditer()
和上面的 findall()
类似,但返回的是 MatchObject
的实例的迭代器
import re
text = re.finditer('(\d+)\.(\d+)\.(\d+)\.(\d+)', 'My IP is 192.168.0.2, and your is 192.168.0.3.')
print(text) # <callable_iterator object at 0x0000018B5F7AB1F0>
print(next(text)) # <re.Match object; span=(9, 20), match='192.168.0.2'>
print(next(text)) # <re.Match object; span=(34, 45), match='192.168.0.3'>
熟悉迭代器原理的同学们应该知道,使用finditer()
比使用findall()
更节省内存,通过迭代的方法获取所有匹配的结果
4. sub()和 subn()
将某字符串中所有匹配正则表达式的部分进行某种形式的替换。除了使用正则表达式提取信息外,有时还需要借助它来修改文本
对于简单的字符串的替换,直接使用python
字符串内置的方法str.repalace()
即可
text = 'yeah, but no, but yeah, but no, but yeah'
text = text.replace('yeah', 'yep')
print(text) # yep, but no, but yep, but no, but yep
sub()
对于复杂模式,比如将将形式为 11/27/2012
的日期字符串改成 2012-11-27
import re
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
text = re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
print(text)
sub()
函数中第一个参数是被匹配的模式,第二个参数是替换模式(反斜杠数字比如 \3
指向前面模式的捕获组号),第三个参数是原字符串
subn()
subn()
和 sub()
一样,但 subn()
还返回一个表示替换的总数,返回一个包含两个元素的元组(替换后的字符串, 替换总数)
5.贪婪匹配与非贪婪匹配
.
可以匹配除换行符外的任意字符,*
代表匹配前面的字符无限次
import re
content = 'Hello 1234567 World This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content) # 贪婪匹配
print(result) # <re.Match object; span=(0, 40), match='Hello 1234567 World This is a Regex Demo'>
print(result.group(1)) # 7
在贪婪匹配下, *
会匹配尽可能多的字符则表达式中.*
后面是\d+
,也就是至少一个数字,并没有指定具体多少个数字,因此.*
就尽可能匹配多的字符,这里就把 123456
都匹配了,给\d+
留下一个可满足条件的数字
import re
content = 'Hello 1234567 World This is a Regex Demo'
result = re.match('^He.*?(\d+).*Demo$', content) # 非贪婪匹配
print(result) # <re.Match object; span=(0, 40), match='Hello 1234567 World This is a Regex Demo'>
print(result.group(1)) # 1234567
.*?
匹配到 Hello
后面的空白字符时,再往后的字符就是数字了,而\d+
恰好可以匹配,.*?
就不再进行匹配,交给后面的\d+
去匹配后面的数字,.*?
匹配了尽可能少的字符,\d+
的结果就是1234567
6.修饰符
修饰符 | 描述 |
---|---|
re.I/re.IGNORECASE | 使匹配对大小写不敏感 |
re.S/re.DOTALL | 使 . 能够匹配全部字符 |
re.I代码示例
import re
text = 'UPPER PYTHON, lower python, Mixed Python'
text = re.findall('python', text, flags=re.I)
print(text) # ['PYTHON', 'python', 'Python']
re.S代码示例
import re
content = '''Hello 1234567 World
This is a Regex Demo'''
content = re.match('^He.*?(\d+).*?Demo$', content, re.S)
print(content.group(1)) # 1234567
如果不加修饰符
re.S
则会导致匹配失败,因为.
是匹配除换行符外的任意字符,当遇到换行符时,.*?
就不能匹配了,而后面匹配字符Demo
又匹配不上剩余字符所以导致匹配失败
在网页匹配中,较为常用的有 re.S
和 re.I
爬虫小案例实战练习-正则表达式在网页爬取解析中的应用
爬取猫眼排名前100的电影相关信息
html源代码
<dd>
<i class="board-index board-index-1">
1
</i>
<a class="image-link" data-act="boarditem-click" data-val="{movieId:1200486}" href="/films/1200486" title="我不是药神">
<img alt="" class="poster-default" src="//s3.meituan.net/static-prod01/com.sankuai.movie.fe.mywww-files/image/loading_2.e3d934bf.png"/>
<img alt="我不是药神" class="board-img" data-src="https://p0.pipi.cn/mmdb/d2dad59253751bd236338fa5bd5a27c710413.jpg?imageView2/1/w/160/h/220"/>
</a>
<div class="board-item-main">
<div class="board-item-content">
<div class="movie-item-info">
<p class="name">
<a data-act="boarditem-click" data-val="{movieId:1200486}" href="/films/1200486" title="我不是药神">
我不是药神
</a>
</p>
<p class="star">
主演:徐峥,周一围,王传君
</p>
<p class="releasetime">
上映时间:2018-07-05
</p>
</div>
<div class="movie-item-number score-num">
<p class="score">
<i class="integer">
9.
</i>
<i class="fraction">
6
</i>
</p>
</div>
</div>
</div>
</dd>
注:正则表达式的模式没必要和博主写的一模一样,但你要确保你能定位到你想要的数据位置
- 提取排名信息的正则表达式模式为:
<dd>.*?board-index.*?>(.*?)</i>
- 提取电影图片信息的正则表达式模式为:
<dd>.*?board-index.*?>(.*?)</i>.*?src="(.*?)"
,匹配该模式的第二个分组 - 提取电影的名称(它在后面的
p结点
内,class
为name
),提取电影名称的正则表达式模式为<dd>.*?board-index.*?>(.*?)</i>.*?src="(.*?)".*?name.*?a.*?>(.*?)</a>
,匹配该模式下的第三个分组 - 其余关于主演、发布时间、评分等内容的提取这里不做过多介绍,这里博主直接给出对应的完整的正则表达式
<dd>.*?board-index.*?>(\d+)</i>.*?src="(.*?)".*?name"><a.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>
,匹配的分组按顺序依次是:电影排名信息、电影图片信息、电影名称信息、主演信息、上映时间信息、评分信息
import re
import time
import requests
def get_one_page(url):
headers = { # 伪造请求头,模拟浏览器发起请求
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.text
return None
def parse_one_page(html):
pattern = re.compile('<dd>.*?board-index.*?>(\d+)</i>.*?src="(.*?)".*?name"><a'
+ '.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>'
+ '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)
items = re.findall(pattern, html) # 注意items返回数据结构
for item in items:
yield {
'电影排名': item[0],
'电影海报': item[1],
'名称': item[2].strip(),
'主演': item[3].strip()[3:] if len(items[3]) > 3 else '',
'上映时间': item[4].strip()[5:] if len(items[4]) > 5 else '',
'评分': item[5].strip() + item[6].strip()
}
if __name__ == '__main__':
for i in range(10):
offset = i * 10
url = 'https://www.maoyan.com/board/4?offset=' + str(offset) # https://www.maoyan.com/board/4
html = get_one_page(url) # 获取网页的html源代码
for item in parse_one_page(html):
print(item)
time.sleep(1) # 模拟延时防止频繁请求被网站禁止访问
上面代码中唯一难点即对于正则表达式模式的理解和编写
注意:爬取中有时候猫眼会将你的请求定位到猫眼验证中心,这个时候你需要手动在浏览器刷新页面重新访问,拖动验证码识别通过,在重新运行以上脚本即可
总结
到此为止,正则表达式的基本用法就介绍完了,熟悉正则表达式的基本使用就能帮助你从重复的文本检索,替换修改等重复繁琐的工作中抽离出来,还不赶快学起来,最后如果你觉得本文对你有用的话,还请点赞收藏~,你们的支持就是博主更新文章的最大动力!!!