13.5 项目实战:爬取豆瓣电影信息
最后,我们来完成一个使用代理爬取的实战项目。豆瓣网的电影专栏是国内权威电影评分网站,其中包括海量影片信息,在浏览器中访问https://movie.douban.com,并选择一个分类(如“豆瓣高分”),可看到如图13-4所示的影片列表页面。
图13-4
单击其中一部影片,进入其页面(简称影片页面),如图13-5所示。
在影片页面中可以看到一部影片的基本信息,如导演、编剧、主演、类型等,我们可以编写爬虫在豆瓣电影中爬取大量影片信息。
图13-5
13.5.1 项目需求
爬取豆瓣电影中“豆瓣高分”分类下的所有影片信息,需要爬取一部影片的信息字段如下:
导演
编剧
主演
类型
制片国家/地区
语言
上映日期
片长
又名
由于豆瓣网对爬取速度做了限制,高速爬取可能会被封禁IP,因此使用代理进行爬取。
13.5.2 页面分析
首先分析影片列表页面,页面中的每一部电影都是通过JavaScript脚本加载的。单击页面最下方的“加载更多”,可以在Chrome开发者工具中捕获到jQuery发送的HTTP请求(加载更多影片),该请求返回了一个json串,如图13-6所示。
图13-6
复制图中请求的url,使用scrapy shell进行访问,查看其中json串的内容:
$ scrapy shell 'https://movie.douban.com/j/search_subjects?type=movie&tag=%E8%B1%86%E7%93%A3%E9%AB%98 %E5%88%86&sort=recommend&page_limit=20&page_start=20' ... >>> import json >>> res = json.loads(response.body.decode('utf8')) >>> res {'subjects': [ {'cover': 'https://img1.doubanio.com/view/movie_poster_cover/lpst/public/p511146957.jpg', 'cover_x': 1538, 'cover_y': 2159, 'id': '1292001', 'is_beetle_subject': False, 'is_new': False, 'playable': False, 'rate': '9.2', 'title': '海上钢琴师', 'url': 'https://movie.douban.com/subject/1292001/'}, {'cover': 'https://img1.doubanio.com/view/movie_poster_cover/lpst/public/p2360940399.jpg', 'cover_x': 1500, 'cover_y': 2145, 'id': '25986180', 'is_beetle_subject': False, 'is_new': False, 'playable': False, 'rate': '8.2', 'title': '釜山行', 'url': 'https://movie.douban.com/subject/25986180/'}, {'cover': 'https://img1.doubanio.com/view/movie_poster_cover/lpst/public/p2404978988.jpg', 'cover_x': 703, 'cover_y': 1000, 'id': '26580232', 'is_beetle_subject': False, 'is_new': False, 'playable': False, 'rate': '8.7', 'title': '看不见的客人', 'url': 'https://movie.douban.com/subject/26580232/'}, ...省略中间部分... {'cover': 'https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p1454261925.jpg', 'cover_x': 2181, 'cover_y': 3120, 'id': '6786002', 'is_beetle_subject': False, 'is_new': False, 'playable': False, 'rate': '9.1', 'title': '触不可及', 'url': 'https://movie.douban.com/subject/6786002/'}, {'cover': 'https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p2411622136.jpg', 'cover_x': 1000, 'cover_y': 1500, 'id': '26354572', 'is_beetle_subject': False, 'is_new': False, 'playable': True, 'rate': '8.2', 'title': '欢乐好声音', 'url': 'https://movie.douban.com/subject/26354572/'}, {'cover': 'https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p1280323646.jpg', 'cover_x': 1005, 'cover_y': 1437, 'id': '1299398', 'is_beetle_subject': False, 'is_new': False, 'playable': False, 'rate': '8.9', 'title': '大话西游之月光宝盒', 'url': 'https://movie.douban.com/subject/1299398/'} ]}
如上所示,返回结果(json)中的'subjects'字段是一个影片信息列表,一共有20项,每一项都是一部影片的信息,其中可以找到影片片名(title)、评分(rate)以及影片页面url等信息。
连续单击加载按钮,捕获更多jQuery发送的HTTP请求,可以总结出其url的规律:
type参数:类型,movie代表电影。
tag参数:分类标签,当前为“豆瓣高分”。
page_start:从第几部影片开始加载,即结果列表中第一部影片在服务器端的序号。
page_limit:期望获取的影片信息的数量,当前为20。
我们可以通过分析出的API,每次获取固定数量的影片信息,从中提取每一个影片页面的url,例如:
先获取20部影片信息:
[BASE_URL]?type=movie&tag=豆瓣高分&sort=recommend&page_limit=20&page_start=0
再获取20部影片信息:
[BASE_URL]?type=movie&tag=豆瓣高分&sort=recommend&page_limit=20&page_start=20
再获取20部影片信息:
[BASE_URL]?type=movie&tag=豆瓣高分&sort=recommend&page_limit=20&page_start=40
…
直到返回结果中的影片信息列表为空,说明没有影片了。
接下来分析影片页面。在scrapy shell中下载任意一个影片页面,并调用view函数在浏览器中查看页面,如图13-7所示。
图13-7
从图13-7中可以看出,影片的信息在<div id="info">中,其中每一个信息字段名("导演","编剧"等)都位于一个<span class="pl">中,比较容易提取,但字段的值很难找到统一的规律,我们可以使用XPath的string函数将<div id="info">中的所有文本提取到一个字符串,然后用提取到的字段名分割该字符串,得到其中值的部分。
首先提取包含所有信息的字符串:
>>> info = response.css('div#info').xpath('string(.)').extract_first() >>> print(info) 导演:涅提·蒂瓦里 编剧:比于什·古普塔 / 施热亚·简 主演:阿米尔·汗 / 法缇玛·萨那·纱卡 / 桑亚·玛荷塔 / 阿帕尔夏克提·库拉那 / 沙克希·坦 沃 / 泽伊拉·沃西姆 / 苏哈妮·巴特纳格尔 / 里特维克·萨霍里 / 吉里什·库卡尼 类型:剧情 / 传记 / 运动 制片国家/地区:印度 语言:印地语 上映日期:2017-05-05(中国大陆) / 2016-12-23(印度) 片长:161分钟(印度) / 140 分钟(中国大陆) 又名:我和我的冠军女儿(台) / 打死不离3 父女(港) / 摔跤吧!老爸 / 摔跤家族 / ???? / Wrestling Competition IMDb链接: tt5074352
再提取所有字段名到一个列表:
>>> fields = [s.strip().replace(':', '') for s in \ ... response.css('div#info span.pl::text').extract()] >>> fields ['导演','编剧','主演','类型','制片国家/地区','语言','上映日期','片长','又名','IMDb 链接']
使用字段名对info进行分割,得到所有值的列表:
>>> import re >>> values = [re.sub('\s+', ' ', s.strip()) for s in \ ... re.split('\s*(?:%s):\s*' % '|'.join(fields), info)][1:] >>> values ['涅提·蒂瓦里', '比于什·古普塔 / 施热亚·简', '阿米尔·汗 / 法缇玛·萨那·纱卡 / 桑亚·玛荷塔 / 阿帕尔夏克提·库拉那 / 沙克希·坦沃 / 泽伊拉·沃西姆 / 苏哈妮·巴特纳格尔 / 里特维克·萨霍里 / 吉里什·库卡尼', '剧情 / 传记 / 运动', '印度', '印地语', '2017-05-05(中国大陆) / 2016-12-23(印度)', '161 分钟(印度) / 140 分钟(中国大陆)', '我和我的冠军女儿(台) / 打死不离3 父女(港) / 摔跤吧!老爸 / 摔跤家族 / ???? / Wrestling Competition', 'tt5074352']
最后,使用以上两个列表构造影片信息字典:
>>> dict(zip(fields, values)) {'IMDb链接': 'tt5074352', '上映日期': '2017-05-05(中国大陆) / 2016-12-23(印度)', '主演':'阿米尔·汗 / 法缇玛·萨那·纱卡 / 桑亚·玛荷塔 / 阿帕尔夏克提·库拉那 / 沙克 希·坦沃 / 泽伊拉·沃西姆 / 苏哈妮·巴特纳格尔 / 里特维克·萨霍里 / 吉里什·库卡尼', '制片国家/地区': '印度', '又名':'我和我的冠军女儿(台) / 打死不离3 父女(港) / 摔跤吧!老爸 / 摔跤家族 / ???? / Wrestling Competition', '导演':'涅提·蒂瓦里', '片长': '161 分钟(印度) / 140 分钟(中国大陆)', '类型':'剧情 / 传记 / 运动', '编剧': '比于什·古普塔 / 施热亚·简', '语言':'印地语'}
经上述操作,我们得到了除片名和评分之外的所有信息,片名和评分信息可以在json串中获取。
到此,页面分析的工作完成了。
13.5.3 编码实现
创建Scrapy项目,取名为douban_movie:
$ scrapy startproject douban_movie
在页面分析中,我们详细阐述了爬取过程,现在可以轻松实现MoviesSpider了,代码如下:
# -*- coding: utf-8 -*- import scrapy from scrapy import Request import json import re from pprint import pprint class MoviesSpider(scrapy.Spider): BASE_URL = 'https://movie.douban.com/j/search_subjects?type=movie&tag=%s&sort= recommend&page_limit=%s&page_start=%s' MOVIE_TAG = '豆瓣高分' PAGE_LIMIT = 20 page_start = 0 name = "movies" start_urls = [BASE_URL % (MOVIE_TAG, PAGE_LIMIT, page_start)] def parse(self, response): # 使用json 模块解析响应结果 infos = json.loads(response.body.decode('utf-8')) # 迭代影片信息列表 for movie_info in infos['subjects']: movie_item = {} # 提取"片名"和"评分", 填入item. movie_item['片名'] = movie_info['title'] movie_item['评分'] = movie_info['rate'] # 提取影片页面url,构造Request 发送请求,并将item通过meta 参数传递给影片 页面解析函数 yield Request(movie_info['url'], callback=self.parse_movie, meta={'_movie_item': movie_item}) # 如果json 结果中包含的影片数量小于请求数量,说明没有影片了,否则继续搜索 if len(infos['subjects']) == self.PAGE_LIMIT: self.page_start += self.PAGE_LIMIT url = self.BASE_URL % (self.MOVIE_TAG, self.PAGE_LIMIT, self.page_start) yield Request(url) def parse_movie(self, response): # 从meta 中提取已包含"片名"和"评分"信息的item movie_item = response.meta['_movie_item'] # 获取整个信息字符串 info = response.css('div.subject div#info').xpath('string(.)').extract_first() # 提取所有字段名 fields= [s.strip().replace(':', '') for s in \ response.css('div#info span.pl::text').extract()] # 提取所有字段的值 values = [re.sub('\s+', ' ', s.strip()) for s in \ re.split('\s*(?:%s):\s*' % '|'.join(fields), info)][1:] # 将所有信息填入item movie_item.update(dict(zip(fields, values))) yield movie_item
为了使用代理进行爬取,我们将之前实现的RandomHttpProxyMiddleware复制到该项目中。
在配置文件settings.py中添加如下配置:
# 我们的爬取不符合豆瓣爬取规则,强制爬取 ROBOTSTXT_OBEY = False # 伪装成常规浏览器 USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) Chrome/42.0.2311.90 Safari/537.36' # 可选。设置下载延迟,防止代理被封禁IP,依据代理数量而定 DOWNLOAD_DELAY = 0.5 # 启用随机代理中间件 DOWNLOADER_MIDDLEWARES = { 'douban_movie.middlewares.RandomHttpProxyMiddleware': 745, } # 指定所要使用的代理服务器列表文件 HTTPPROXY_PROXY_LIST_FILE = 'proxy_list.json'
为了爬取稳定,使用在云服务器上自行搭建的代理服务器,手工编辑代理服务器列表文件proxy_list.json:
[ {"proxy_scheme": "https", "proxy": "http://116.29.35.201:8118"}, {"proxy_scheme": "https", "proxy": "http://197.10.171.143:8118"}, {"proxy_scheme": "https", "proxy": "http://112.78.43.67:8118"}, {"proxy_scheme": "https", "proxy": "http://124.59.42.145:8118"} ]
最后,运行爬虫,将结果保存到文件moveis.json:
$ scrapy crawl movies -o movies.json
在Python中观察爬取结果,代码如下:
>>> import json >>> movies = json.load(open('movies.json')) >>> for movie in movies: ... print(movie['片名'], movie['评分'], movie['导演']) ... 忠犬八公的故事 9.2 拉斯·霍尔斯道姆 楚门的世界 9.0 彼得·威尔 怦然心动 8.9 罗伯·莱纳 泰坦尼克号 9.2 詹姆斯·卡梅隆 血战钢锯岭 8.7 梅尔·吉布森 驴得水 8.3 周申 / 刘露 星际穿越 9.1 克里斯托弗·诺兰 盗梦空间 9.2 克里斯托弗·诺兰 阿甘正传 9.4 罗伯特·泽米吉斯 千与千寻 9.2 宫崎骏 三傻大闹宝莱坞 9.1 拉吉库马尔·希拉尼 霸王别姬 9.5 陈凯歌 你的名字 8.5 新海诚 金刚狼3:殊死一战 8.3 詹姆斯·曼高德 疯狂动物城 9.2 拜伦·霍华德 / 瑞奇·摩尔 / 杰拉德·布什 爱乐之城 8.3 达米恩·查泽雷 大话西游之月光宝盒 8.9 刘镇伟 触不可及 9.1 奥利维·那卡什 / 艾力克·托兰达 无间道 9.0 刘伟强 / 麦兆辉 让子弹飞 8.7 姜文 ...省略中间部分... 哈尔的移动城堡 8.8 宫崎骏 阿凡达 8.6 詹姆斯·卡梅隆 教父 9.2 弗朗西斯·福特·科波拉 罗马假日 8.9 威廉·惠勒 龙猫 9.1 宫崎骏 火星救援 8.4 雷德利·斯科特 超能陆战队 8.6 唐·霍尔 / 克里斯·威廉姆斯 欢乐好声音 8.2 加斯·詹宁斯 / 克里斯托夫·卢尔德莱 少年派的奇幻漂流 9.0 李安 釜山行 8.2 延尚昊 大话西游之大圣娶亲 9.2 刘镇伟 这个杀手不太冷 9.4 吕克·贝松 海上钢琴师 9.2 朱塞佩·托纳多雷 宣告黎明的露之歌 8.1 汤浅政明 肖申克的救赎 9.6 弗兰克·德拉邦特 摔跤吧!爸爸 9.2 涅提·蒂瓦里 >>> len(movies) 500
如上所示,我们成功爬取了“豆瓣高分”分类下的500部影片信息。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论