分类目录:《Python爬虫从入门到精通》总目录
解析库使用篇:
解析库re的使用:正则表达式
解析库XPath的使用
解析库Beautiful Soup的使用
解析库pyquery的使用
相关实战:爬取猫眼电影排行Top100
正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。对于爬虫,基于正则表达式,从HTML里提取想要的信息就非常方便了。
正则表达式有特定的语法规则的。写好正则表达式后,就可以拿它去一个长字符串里匹配查找了。不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。对于网页来说,如果想找出网页源代码里有多少URL,用匹配URL的正则表达式去匹配即可。下图就列出了正则表达式常用的匹配规则。

正则表达式不是Python独有的,它可以用在其他编程语言中。在Python中,re 库提供了整个正则表达式的实现,利用这个库,可以在Python中使用正则表达式。在Python中写正则表达式几乎都用这个库,下面就来了解它的一些常用方法。
match()
match() 传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串。match() 方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果,如果不匹配,就返回None 。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('Blog:\w{11}', content)
print(result.group())
运行结果:
Blog:hy592070616
这里首先声明了一个字符串 'Blog:hy592070616 Corporation:HUAWEI' ,其中包含英文字母、空格、数字、冒号等。接下来,我们写一个正则表达式'Blog:\w{11}' 来匹配这个字符串。正则表达式中Blog: 就是待匹配字符串的开头,\w 通过查上表可知是匹配字母、数字及下划线,\w 后面跟着{11} 表示匹配11个\w 。根据这个规则就可以从字符串 'Blog:hy592070616 Corporation:HUAWEI' 中匹配出字符串Blog:hy592070616 。同样,我们可以加入\s 来匹配空格,\d 来匹配数字以匹配整个字符串。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('\w{4}:\w{2}\d{9}\s\w{11}:\w{6}', content)
print(result.group())
运行结果:
Blog:hy592070616 Corporation:HUAWEI
当然,做这种简单的任务我们可以用\S 来匹配任意非空字符达到相同的效果。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('\S{16}\s\S{18}', content)
print(result.group())
上面几种正则表达式比较复杂,出现空白字符我们就写\s 匹配,出现数字我们就用\d 匹配,这样的工作量非常大。其实完全没必要这么做,在上表中有一个万能匹配.* 。其中,. 可以匹配任意字符(除换行符),* 代表字符无限次,所以它们组合在一起就可以匹配任意字符了。有了.* ,我们就不用挨个字符地匹配了。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('B.*I', content)
print(result.group())
正则表达式B.*I 中的B 和I 代表了字符串Blog:hy592070616 Corporation:HUAWEI 首位两个字符,其它的字符均用.* 来进行匹配。我们就可以得到相同的结果:
Blog:hy592070616 Corporation:HUAWEI
如果想从字符串中提取一部分内容,可以使用() 将想提取的子字符串括起来。() 实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用group() 方法传入分组的索引即可获取提取的结果。比如希望从我的博客名hy592070616 中提取我的QQ号592070616 就可以使用() 。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('\w{4}:\w\w(\d+)', content)
print(result.group(1))
运行结果:
592070616
可以看到,我们成功得到了592070616 。这里用的是group(1) ,它与group() 有所不同。group() 会输出完整的匹配结果,而group(1) 会输出第一个被() 包围的匹配结果。假如正则表达式后面还有() 包括的内容,那么可以依次用group(2) 、group(3) 等来获取。
若使用.* 匹配时,有时候匹配到的并不是我们想要的结果。比如还是从我的博客名hy592070616 中提取我的QQ号592070616 。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('B.*(\d+)', content)
print(result.group(1))
运行结果为:
6
很明显,我们希望提取的是592070616 ,而此处由于我们用了万能匹配符.* 在语句中,正则表达式就会匹配59207061 ,所以在括号中我们就只能提取到一个数字6 。这里就涉及一个贪婪匹配与非贪婪匹配的问题了。在贪婪匹配下,.* 会匹配尽可能多的字符。正则表达式中.* 后面是\d+ ,也就是至少一个数字,并没有指定具体多少个数字,因此,.* 就尽可能匹配多的字符,这里就把59207061 给匹配了,给\d+ 留下一个可满足条件的数字6 。这很明显会给我们带来很大的不便。有时候,匹配结果会莫名其妙少了一部分内容。其实,这里只需要使用非贪婪匹配就好了。非贪婪匹配的写法是.*? ,我们需要将将第一个.* 改成了.*? 就可以转变为非贪婪匹配。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.match('B.*?(\d+)', content)
print(result.group(1))
结果如下:
592070616
match() 还有个可选参数修饰符,修饰符可以扩展正则表达式的匹配范围。

比如,在字符串Blog:hy592070616 Corporation:HUAWEI 中加入换行,将其变为:
Blog:hy592070616
Corporation:HUAWEI
用之前的方法就没有办法对其进行匹配:
import re
content = '''Blog:hy592070616
Corporation:HUAWEI
'''
result = re.match('B.*I', content)
print(result.group())
这里的result 就会返回一个None ,因为.* 无法匹配换行符。我们只需在match() 中的第三个参数加一个修饰符re.S ,即可修正这个错误。
import re
content = '''Blog:hy592070616
Corporation:HUAWEI
'''
result = re.match('B.*I', content, re.S)
print(result.group())
这样就可以正常返回结果:
Blog:hy592070616
Corporation:HUAWEI
search()
match() 方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。而search() 方法在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果。也就是说,正则表达式可以是字符串的一部分,在匹配时,search() 方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,就返回None 。比如我们要匹配字符串Blog:hy592070616 Corporation:HUAWEI 中的HUAWEI :
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.search('Corporation:(\w+)', content)
print(result.group(1))
就可以得到HUAWEI 结果,而不需要将字符串前部全部匹配。
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="邓丽君"><i class="fa fa-user"></i>但愿人长久</a>
</li>
</ul>
</div>
我们希望提取歌手和歌名的信息,则可以:
import re
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="邓丽君"><i class="fa fa-user"></i>但愿人长久</a>
</li>
</ul>
</div>'''
result = re.findall('singer="(\w+).*>(\w+)</a>', html)
print(result)
我们可以看到,大部分的歌手和歌名是singer="歌手">歌名</a> 的形式,则我们可以写正则表达式singer="(\w+)">(\w+)</a> 。但是我们发现有一条 singer="邓丽君"><i class="fa fa-user"></i>但愿人长久</a> 中有其他信息,使用上述的正则表达式这无法匹配出(邓丽君,但愿人长久)这个信息。基于这个问题,我们可以将正则表达式修改为singer="(\w+).*>(\w+)</a> (上述代码中的正则表达式),就可以顺利匹配出所有信息了。
[('任贤齐', '沧海一声笑'), ('齐秦', '往事随风'), ('beyond', '光辉岁月'), ('陈慧琳', '记事本'), ('邓丽君', '但愿人长久')]
sub()
除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,这时就可以借助sub()方法。比如,我想将字符串Blog:hy592070616 Corporation:HUAWEI 中的Corporation:HUAWEI 去掉,就可以匹配Corporation:HUAWEI 并用空的字符串替换。
import re
content = 'Blog:hy592070616 Corporation:HUAWEI'
result = re.sub('Corporation:.*', '',content)
print(result)
结果如下:
Blog:hy592070616
compile()
除了前面提到的处理字符串的方法以外,最后再介绍一下compile() 方法,这个方法可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用。
import re
content1 = '2019-01-01 12:00'
content2 = '2019-01-02 12:30'
content3 = '2019-01-03 13:00'
pattern = re.compile('(\d+)-(\d+)-(\d+)\s.*')
result_1 = re.match(pattern, content1)
result_2 = re.match(pattern, content2)
result_3 = re.match(pattern, content3)
print('Year:' + result_1.group(1) + ' Month:' + result_1.group(2) + ' Day:' + result_1.group(3))
print('Year:' + result_2.group(1) + ' Month:' + result_2.group(2) + ' Day:' + result_2.group(3))
print('Year:' + result_3.group(1) + ' Month:' + result_3.group(2) + ' Day:' + result_3.group(3))
这里为了找到各个字符串中的年月日信息构建了正则表达式对象(\d+)-(\d+)-(\d+)\s.* 以复用,输出结果如下:
import re
content1 = '2019-01-01 12:00'
content2 = '2019-01-02 12:30'
content3 = '2019-01-03 13:00'
pattern = re.compile('(\d+)-(\d+)-(\d+)\s.*')
result_1 = re.match(pattern, content1)
result_2 = re.match(pattern, content2)
result_3 = re.match(pattern, content3)
print('Year:' + result_1.group(1) + ' Month:' + result_1.group(2) + ' Day:' + result_1.group(3))
print('Year:' + result_2.group(1) + ' Month:' + result_2.group(2) + ' Day:' + result_2.group(3))
print('Year:' + result_3.group(1) + ' Month:' + result_3.group(2) + ' Day:' + result_3.group(3))
|