本节中,我们利用requests库和正则表达式来抓取猫眼电影TOP100的相关内容。requests比urllib使用更加方便,而且目前我们还没有系统学习HTML解析库,所以这里就选用正则表达式来作为解析工具。
- 本节目标
本节中,我们要提取出猫眼电影TOP100的电影名称、时间、评分、图片等信息,提取的站点URL为http://maoyan.com/board/4,提取的结果会以文件形式保存下来。 - 准备工作
安装request库:
pip install requests
- 抓取分析
我们需要抓取的目标站点为http://maoyan.com/board/4,打开之后便可以查看到榜单信息。点击第2页,可以发现页面的URL变成http://maoyan.com/board/4?offset=10,比之前的URL多了一个参数,那就是offset=10
,而目前显示的结果是排行11-20名的电影,初步推断这是一个偏移量的参数。再点击下一页,发现页面的URL变成了http://maoyan.com/board/4?offset=20,参数offset
变成了20,而显示的结果是排行21-30的电影。
由此可以总结出规律,offset
代表偏移量值,如果偏移量为n,则显示的电影序号就是n+1到n+10,每页显示10个。所以,如果想获取TOP100电影,只需要分开请求10次,而10次的offset参数分别设置为0、10、20、…90即可,这样获取不同的页面之后,再用正则表达式提取出相关信息,就可以得到TOP100的所有电影信息了。 - 抓取首页
接下来用代码实现这个过程。首先抓取第一页的内容。我们实现了get_one_page()方法,并给它传入url参数。然后将抓取的页面结果返回,再通过main()方法调用。初步代码实现如下:
import requests
def get_one_page(url):
response = requests.get(url)
if response.status_code == 200:
return response.text
return None
def main():
url = 'http://maoyan.com/board/4'
html = get_one_page(url)
print(html)
main()
这样运行之后,就可以成功获取首页的源代码了。获取源代码后,就需要解析页面,提取出我们想要的信息。
- 正则提取
表达式 | 含义 |
---|---|
\w | 匹配字母、数字及下划线 |
\W | 匹配不是字母、数字及下划线的字符 |
\s | 匹配任意空白字符,等价于[\t\n\r\f] |
\S | 匹配任意非空字符 |
\d | 匹配任意数字,等价于[0-9] |
\D | 匹配任意非数字的字符 |
\A | 匹配字符串开头 |
\Z | 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串 |
\z | 匹配字符串结尾,如果存在换行,同时还会匹配换行符 |
\G | 匹配最后匹配完成的位置 |
\n | 匹配一个换行符 |
\t | 匹配一个制表符 |
^ | 匹配一行字符串的开头 |
$ | 匹配一行字符串的结尾 |
. | 匹配任意字符,除了换行符,当re.DOTALL(如re.S)标记被指定时,则可以匹配包括换行符的任意字符 |
[...] | 用来表示一组字符,单独列出,比如[amk]匹配a、m或k |
[^...] | 不在[]中的字符,比如[^abc]匹配除了a、b、c之外的字符 |
* | 匹配0个或多个表达式 |
+ | 匹配1个或多个表达式 |
? | 匹配0个或1个前面的正则表达式定义的片段,非贪婪方式 |
{n} | 精确匹配n个前面的表达式 |
{n, m} | 匹配n到m次由前面正则表达式定义的片段,贪婪方式 |
a|b | 匹配a或b |
( ) | 匹配括号内的表达式,也表示一个组,group(1),group(2)... |
接下来,回到网页看一下页面的真实源码。在开发者模式下的Network监听组件中查看源代码,意,这里不要在Elements选项卡中直接查看源码,因为那里的源码可能经过JavaScript操作而与原始请求不同,而是需要从Network选项卡中Response部分查看原始请求得到的源码。查看其中一个条目的源代码如下图所示:
源代码.png可以看到,一部电影信息对应的源代码是一个
dd
节点,我们用正则表达式来提取这里面的一些电影信息。首先,需要提取它的排名信息。而它的排名信息是在class
为board-index
的i节点内,这里利用非贪婪匹配来提取i节点内的信息,正则表达式写为:
<dd>.*?board-index.*?>(.*?)</i>
#使用()将想子字符串括起来。()实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用group()方法传入分组索引即可获取提取的结果。
随后需要提取电影的图片。可以看到,后面有a
节点,其内部有两个img
节点。经过检查后发现,第二个img
节点的data-src
属性是图片的链接。这里提取第二个img
节点的data-src
属性,正则表达式可以改写如下:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)"
再往后,需要提取电影的名称,它在后面的p
节点内,class
为name
。所以,可以用name
做一个标志位,然后进一步提取到其内a
节点的正文内容,此时正则表达式改写如下:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>
再提取主演、发布时间、评分等内容时,都是同样的原理。最后,正则表达式写为:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>
这样一个正则表达式可以匹配一个电影的结果,里面匹配了7个信息。接下来,通过调用findall()
方法提取出所有的内容。
接下来,我们再定义解析页面的方法parse_one_page()
,主要是通过正则表达式来从结果中提取出我们想要的内容,实现代码如下:
def parse_one_page(html):
pattern = re.compile(
'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>',
re.S)
items = re.findall(pattern, html)
print(items)
此时数据比较杂乱,我们再将匹配结果处理一下,遍历提取结果并生成字典,方法改写如下:
def parse_one_page(html):
pattern = re.compile(
'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>',
re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'index': item[0],
'image': item[1],
'title': item[2].strip(),
'actor': item[3].strip()[3:] if len(item[3]) > 3 else '',
'time': item[4].strip()[5:] if len(item[4]) > 5 else '',
'score': item[5].strip() + item[6].strip()
}
注:1.当函数含有yield,会被系统默认为是一个生成器
2.执行到yield p, 返回p值以及整个生成器处于暂停的状态,并跳出当前函数,执行到调用返回值p的语句
3.当再次执行到这个含有yield的生成器函数时,会自动立即执行到上次暂停的位置继续执行,也就是从yield p这个语句继续执行。
- 写入文件
随后,我们将提取的结果写入文件,这里直接写入到一个文本文件中。这里通过JSON库的dumps()
方法实现字典的序列化,并指定ensure_ascii
参数为False,这样可以保证输出结果是中文形式而不是Unicode编码。代码如下:
def write_to_json(content):
with open('result.txt', 'a') as f:
print(type(json.dumps(content)))
f.write(json.dumps(content, ensure_ascii=False,).encode('utf-8'))
通过调用write_to_json()方法即可实现将字典写入到文本文件的过程,此处的content参数就是一部电影的提取结果,是一个字典。
- 整合代码
最后,实现main()方法来调用前面实现的方法,将单页的电影结果写入到文件。相关代码如下:
def main():
url = 'http://maoyan.com/board/4'
html = get_one_page(url)
for item in parse_one_page(html):
write_to_json(item)
到此为止,我们就完成了单页电影的提取,也就是首页的10部电影可以成功提取并保存到文本文件中了。
- 分页爬取
因为我们需要抓取的是TOP100的电影,所以还需要遍历一下,给这个链接传入offset
参数,实现其他90部电影的爬取,此时添加如下调用即可:
if __name__ == '__main__':
for i in range(10):
main(offset=i * 10)
这里还需要将main()
方法修改一下,接收一个offset
值作为偏移量,然后构造URL进行爬取。实现代码如下:
def main(offset):
url = 'http://maoyan.com/board/4?offset=' + str(offset)
html = get_one_page(url)
for item in parse_one_page(html):
print(item)
write_to_file(item)
到此为止,我们的猫眼电影TOP100的爬虫就全部完成了,再稍微整理一下,完整的代码如下:
import json
import requests
from requests.exceptions import RequestException
import re
import time
def get_one_page(url):
try:
response = requests.get(url)
if response.status_code == 200:
return response.text
return None
except RequestException:
return None
def parse_one_page(html):
pattern = re.compile('<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name"><a'
+ '.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>'
+ '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'index': item[0],
'image': item[1],
'title': item[2],
'actor': item[3].strip()[3:],
'time': item[4].strip()[5:],
'score': item[5] + item[6]
}
def write_to_file(content):
with open('result.txt', 'a', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')
def main(offset):
url = 'http://maoyan.com/board/4?offset=' + str(offset)
html = get_one_page(url)
for item in parse_one_page(html):
print(item)
write_to_file(item)
if __name__ == '__main__':
for i in range(10):
main(offset=i * 10)
time.sleep(1)
现在猫眼多了反爬虫,如果速度过快,则会无响应,所以这里又增加了一个延时等待。
网友评论