目录
一、常用匹配规则介绍
正则表达式常用匹配规则(参考学习网址):正则表达式 – 语法 | 菜鸟教程
二、常用匹配方法
1.match
match方法中,第一个参数传入正则表达式,第二个参数是传入要匹配的字符串。
match方法会尝试从字符串的‘起始位置’开始匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配则返回None。实例如下:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
print(len(content)) #查看字符串的长度
result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}',content)
#result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}.*Demo$',content) #匹配所有字符
print(result)
print(result.group()) #返回匹配结果
print(result.span()) #输出范围
运行结果:
41 <re.Match object; span=(0, 25), match='Hello 123 4567 World_This'> Hello 123 4567 World_This (0, 25)
- 匹配目标
要想在一段文字中提取一部分内容,可以使用括号()将想要提取的子字符串括起来。()实际上标记了一个子表达式的开头和结束的位置,被标记的每个子表达式依次对应每个分组,调用group方法传入分组的索引就可以获取提取结果,实例如下:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld',content)
print(result)
print(result.group()) #输出完整的匹配结果
print(result.group(1)) #输出第一个被()包围的匹配结果
print(result.span())
运行结果:
<re.Match object; span=(0, 19), match='Hello 1234567 World'> Hello 1234567 World 1234567 (0, 19)
- 通用匹配
由于上面的正则表达式中比较复杂,只要出现空包字符都要\s匹配,出现数字就要\d匹配,在需要匹配内容较多时,这样的工作量较大。其实可以使用万能匹配".*",其中“.”可以匹配任意符,"*"代表匹配前面字符的无限次,两者组合就可以匹配任意字符了。接着上面例子,利用".*"改写一下正则表达式:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$',content)
print(result)
print(result.group()) #输出完整的匹配结果
print(result.span())
运行结果:
<re.Match object; span=(0, 41), match='Hello 123 4567 World_This is a Regex Demo'> Hello 123 4567 World_This is a Regex Demo (0, 41)
- 贪婪与非贪婪
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$',content)
print(result)
print(result.group(1))
运行结果:
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'> 7
可见,在贪婪匹配下,得到的只有数字7。因为在贪婪匹配下,".* "会匹配尽可能多的字符,而且".* "后面是\d+,也就是至少一个数字,并没有指定具体几个数字,所以,".* "就尽可能匹配多个数字将123456都匹配了,只留下满足条件的一个数字7,因此最后得到的内容也就只有数字7。
这显然有时会给我们带来不变,会莫名少了一部分的内容,接下来对比非贪婪匹配,看一下两者的效果如何:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*?(\d+).*?Demo$',content)
print(result)
print(result.group(1))
运行结果:
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'> 1234567
可见,在非贪婪匹配下,可以的到1234567整串数字。与贪婪匹配相反,非贪婪匹配则是匹配尽可能少的字符,在实际应用中,应该尽可能使用非贪婪匹配,以免出现匹配结果缺失的问题。
但是,如果匹配的是字符串结尾的内容,则使用".* "贪婪匹配会更好,因为它会匹配尽可能多的内容,将结尾内容匹配出来。而".*? "非贪婪匹配则匹配尽可能少的内容,具体实例如下:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result1 = re.match('^He.*?a\s(.*?)',content)
result2 = re.match('^He.*?a\s(.*)',content)
print('result1(非贪婪匹配)',result1.group(1))
print('result2(贪婪匹配)',result2.group(1))
运行结果:
result1(非贪婪匹配) result2(贪婪匹配) Regex Demo
- 修饰符
re.I:使匹配对大小写不敏感
re.L:实现本地化识别(local-aware)匹配
re.M:多行匹配,影响^和$
re.S:使匹配内容包括换行符在内的所有字符
re.U:根据Unicode字符集解析字符。这个标志会影响\w、\W、\b和\B
re.X:该标志能够给予你更灵活的格式,以便将正则表达式书写得更易于理解
在遇到字符串中有换行符的情况,如果跟上面一样匹配,则会出现报错的情况:
import re
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*?Demo$',content)
print(result.group(1))
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_448/501554157.py in <module>
4 '''
5 result = re.match('^He.*?(\d+).*?Demo$',content)
----> 6 print(result.group(1))
AttributeError: 'NoneType' object has no attribute 'group'
上面运行结果报错,也就是正则表达式没有匹配到这个字符串,返回结果为None,而又调用的group方法,所以导致AttributeError
因为.*?是匹配除换行符以外的任意字符,由于遇到换行符,则匹配失败。而只需加一个修饰符re.S,即可修正错误
import re
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*?Demo$',content,re.S)
print(result.group(1))
运行结果:
1234567
- 转义匹配
如果目标字符串包含除换行符以外的任意字符如.,则可在其前面加上反斜杠“\”即可实现匹配,实例如下:
import re
content = '(百度)www.baidu.com'
result = re.match('\(百度\)www\.baidu\.com',content)
print(result.group())
运行结果:
(百度)www.baidu.com
2.search
在上述match方法中,由于该方法是从字符串开头开始匹配的,意味着一旦开头不匹配,则整个匹配就失败了。match方法在使用时需要考虑目标字符串开头的内容,因此在做匹配时并不方便
search方法在匹配时会扫描整个字符串,然后返回第一个匹配成功的结果。也就是说,正则表达式可以是字符串中的一部分,不需要想match方法哪有匹配字符串开头。在匹配时,search方法会依次以每个字符作为开头扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容;如果扫描完还没有找到符合规则的字符串,就返回None。将上述代码的match方法改成search方法,如下:
import re
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.search('He.*?(\d+).*?Demo',content,re.S)
print(result)
print(result.group(1))
运行结果:
<re.Match object; span=(0, 40), match='Hello 1234567 World_This\nis a Regex Demo'> 1234567
为匹配方便,建议尽量使用search方法,下面再用search写几个正则表达式实例来实现相应信息提取:
html = '''<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐">沧海一声笑</a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦">往事随风</a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君">但愿人长久</a>
</li>
</ul>
</div>'''
import re
result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>',html,re.S)
if result:
print(result.group(1),result.group(2))
运行结果:
齐秦 往事随风
从结果可以看到,这是class为active的li节点内部的超链接包含歌手和歌名。如果吧active去掉又会怎样呢,改写代码如下:
import re
result = re.search('<li.*?singer="(.*?)">(.*?)</a>',html,re.S)
if result:
print(result.group(1),result.group(2))
运行结果:
任贤齐 沧海一声笑
去掉active标签后,从字符串开头开始搜索,此时符合条件的节点变成第二个li节点,后面的不再匹配,所以运行结果改变。这两次的匹配都用了re.S,使得.*?可以匹配换行,下面去掉re.S,改写代码如下:
import re
result = re.search('<li.*?singer="(.*?)">(.*?)</a>',html)
if result:
print(result.group(1),result.group(2))
运行结果:
beyond 光辉岁月
将re.S去掉后,匹配结果变成第四个li节点的内容,因为第二个和第三个li节点都包含换行符,而第四个li节点不包含换行符,故成功匹配。
3.findall
上述search方法,它能够返回与正则表达式相匹配的第一个字符串。但是如果想要获取与正则表达式相匹配的所有字符串,可以使用findall方法
html = '''<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐">沧海一声笑</a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦">往事随风</a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君">但愿人长久</a>
</li>
</ul>
</div>'''
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>',html,re.S)
print(type(results),results)
for result in results:
print(result) #输出位元组
print(result[0],result[1],result[2]) #输出字符串
运行结果:
<class 'list'> [('/2.mp3', '任贤齐', '沧海一声笑'), ('/3.mp3', '齐秦', '往事随风'), ('/4.mp3', 'beyond', '光辉岁月'), ('/5.mp3', '陈慧琳', '记事本'), ('/6.mp3', '邓丽君', '但愿人长久')] ('/2.mp3', '任贤齐', '沧海一声笑') /2.mp3 任贤齐 沧海一声笑 ('/3.mp3', '齐秦', '往事随风') /3.mp3 齐秦 往事随风 ('/4.mp3', 'beyond', '光辉岁月') /4.mp3 beyond 光辉岁月 ('/5.mp3', '陈慧琳', '记事本') /5.mp3 陈慧琳 记事本 ('/6.mp3', '邓丽君', '但愿人长久') /6.mp3 邓丽君 但愿人长久
由结果可知,将search方法换成findall方法返回结果是列表类型,需要通过遍历来依次获得每组内容。总的来说,如果只想获取匹配到的第一个字符串,可以用search方法,如果需要提取多个内容,则使用findall方法
4.sub
除了使用正则表达式提取信息,还可以借助它修改文本信息。例如想将一串文本你的数字去掉,用repalce方法未免太繁琐,此时可利用sub方法实现,第一个参数传入正则表达式匹配需要修改的信息,第二个参数传入替换成的内容,第三个参数是原字符串。实例入下:
import re
content = "121fef342dkjdsd87f7hiud3334"
content = re.sub('\d+','',content)
print(content)
运行结果:
fefdkjdsdfhiud
5.compile
compile可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用。实例如下:
import re
content1 = '2022/7/23晴'
content2 = '2022/7/24小雨'
content3 = '2022/7/25雷阵雨'
pattern = re.compile('\d{4}/\d{1}/\d{2}')
result1 = re.sub(pattern,'',content1)
result2 = re.sub(pattern,'',content2)
result3 = re.sub(pattern,'',content3)
print(result1,result2,result3)
三、基础爬虫案例实战
爬取的网址为:豆瓣读书,爬取的内容为:超链接、书名、作者、出版日期
import re
import requests
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"}
content = requests.get('https://book.douban.com/',headers = headers).text
pattern = re.compile('<li.*?cover">.*?href="(.*?)".*?title="(.*?)".*?more-meta">.*?author">(.*?)</span>.*?year">(.*?)</span>.*?</li>',re.S)
results = re.findall(pattern,content)
for result in results:
url,name,author,date=result
author = re.sub('\s','',author)
year = re.sub('\s','',year)
print(url,name,author,date)
正则表达式爬取网页内容效率会比较低,后续将继续学习pyquery和beautifulsoup等库爬取会更方便。