第13章
使用HTTP代理
大家可能都有过给浏览器设置HTTP代理的经验,HTTP代理服务器可以比作客户端与Web服务器(网站)之间的一个信息中转站,客户端发送的HTTP请求和Web服务器返回的HTTP响应通过代理服务器转发给对方,如图13-1所示。
爬虫程序在爬取某些网站时也需要使用代理,例如:
● 由于网络环境因素,直接爬取速度太慢,使用代理提高爬取速度。
● 某些网站对用户的访问速度进行限制,爬取过快会被封禁ip,使用代理防止被封禁。
● 由于地方法律或政治原因,某些网站无法直接访问,使用代理绕过访问限制。
这一章我们来学习Scrapy爬虫如何使用代理进行爬取。
13.1 HttpProxyMiddleware
Scrapy内部提供了一个下载中间件HttpProxyMiddleware,专门用于给Scrapy爬虫设置代理。
13.1.1 使用简介
HttpProxyMiddleware默认便是启用的,它会在系统环境变量中搜索当前系统代理(名字格式为xxx_proxy的环境变量),作为Scrapy爬虫使用的代理。
假设我们现在有两台在云上搭建好的代理服务器:
http://116.29.35.201:8118
http://197.10.171.143:8118
为本机的Scrapy爬虫分别设置发送HTTP和HTTPS请求时所使用的代理,只需在bash中添加环境变量:
$ export http_proxy="http://116.29.35.201:8118" # 为HTTP请求设置代理
$ export https_proxy="http://197.10.171.143:8118" # 为HTTPS请求设置代理
配置完成后,Scrapy爬虫将会使用上面指定的代理下载页面,我们可以通过以下实验进行验证。
利用网站http://httpbin.org提供的服务可以窥视我们所发送的HTTP(S)请求,如请求源IP地址、请求头部、Cookie信息等。图13-2展示了该网站各种服务的API地址。
访问http(s)://httpbin.org/ip将返回一个包含请求源IP地址信息的json串,在scrapy shell中访问该url,查看请求源IP地址:
$ scrapy shell
...
>>> import json
>>> fetch(scrapy.Request('http://httpbin.org/ip')) # 发送HTTP请求
[scrapy] DEBUG: Crawled (200) (referer: None)
>>> json.loads(response.text)
{'origin': '116.29.35.201'}
>>> fetch(scrapy.Request('https://httpbin.org/ip')) # 发送HTTPS请求
[scrapy] DEBUG: Crawled (200) (referer: None)
>>> json.loads(response.text)
{'origin': '197.10.171.143'}
在上述实验中,分别以HTTP和HTTPS发送请求,使用json模块对响应结果进行解析,读取请求源IP地址(origin字段),其值正是代理服务器的IP。由此证明,Scrapy爬虫使用了我们指定的代理。
上面我们使用的是无须身份验证的代理服务器,还有一些代理服务器需要用户提供账号、密码进行身份验证,验证成功后才提供代理服务,使用此类代理时,可按以下格式配置:
$ export http_proxy="http://liushuo:12345678@113.24.36.24:7777"
13.1.2 源码分析
虽然使用HttpProxyMiddleware很简单,但大家最好对其工作原理有所了解,以下是HttpProxyMiddleware的源码:
import base64
from six.moves.urllib.request import getproxies, proxy_bypass
from six.moves.urllib.parse import unquote
try:
from urllib2 import _parse_proxy
except ImportError:
from urllib.request import _parse_proxy
from six.moves.urllib.parse import urlunparse
from scrapy.utils.httpobj import urlparse_cached
from scrapy.exceptions import NotConfigured
from scrapy.utils.python import to_bytes
class HttpProxyMiddleware(object):
def __init__(self, auth_encoding='latin-1'):
self.auth_encoding = auth_encoding
self.proxies = {}
for type, url in getproxies().items():
self.proxies[type] = self._get_proxy(url, type)
if not self.proxies:
raise NotConfigured
@classmethod
def from_crawler(cls, crawler):
auth_encoding = crawler.settings.get('HTTPPROXY_AUTH_ENCODING')
return cls(auth_encoding)
def _get_proxy(self, url, orig_type):
proxy_type, user, password, hostport = _parse_proxy(url)
proxy_url = urlunparse((proxy_type or orig_type, hostport, '', '', '', ''))
if user:
user_pass = to_bytes(
'%s:%s' % (unquote(user), unquote(password)),
encoding=self.auth_encoding)
creds = base64.b64encode(user_pass).strip()
else:
creds = None
return creds, proxy_url
def process_request(self, request, spider):
# ignore if proxy is already set
if 'proxy' in request.meta:
return
parsed = urlparse_cached(request)
scheme = parsed.scheme
# 'no_proxy' is only supported by http schemes
if scheme in ('http', 'https') and proxy_bypass(parsed.hostname):
return
if scheme in self.proxies:
self._set_proxy(request, scheme)
def _set_proxy(self, request, scheme):
creds, proxy = self.proxies[scheme]
request.meta['proxy'] = proxy
if creds:
request.headers['Proxy-Authorization'] = b'Basic ' + creds
分析代码如下:
● __init__方法
在HttpProxyMiddleware的构造器中,使用Python标准库urllib中的getproxies函数在系统环境变量中搜索系统代理的相关配置(变量名格式为[协议]_proxy的变量),调用self._get_proxy方法解析代理配置信息,并将其返回结果保存到self.proxies字典中,如果没有找到任何代理配置,就抛出NotConfigured异常,HttpProxyMiddleware被弃用。
● _get_proxy方法
解析代理配置信息,返回身份验证信息(后面讲解)以及代理服务器url。
● process_request方法
处理每一个待发送的请求,为没有设置过代理的请求(meta属性不包含proxy字段的请求)调用self._set_proxy方法设置代理。
● _set_proxy方法
为一个请求设置代理,以请求的协议(HTTP或HTTPS)作为键,从代理服务器信息字典self.proxies中选择代理,赋给request.meta的proxy字段。对于需要身份验证的代理服务器,添加HTTP头部Proxy-Authorization,其值是在_get_proxy方法中计算得到的。
经分析得知,在Scrapy中为一个请求设置代理的本质就是将代理服务器的url填写到request.meta['proxy']。
13.2 使用多个代理
利用HttpProxyMiddleware为爬虫设置代理时,对于一种协议(HTTP或HTTPS)的所有请求只能使用一个代理,如果想使用多个代理,可以在构造每一个Request对象时,通过meta参数的proxy字段手动设置代理:
request1 = Request('http://example.com/1', meta={'proxy': 'http://166.1.34.21:7117'})
request2 = Request('http://example.com/2', meta={'proxy': 'http://177.2.35.21:8118'})
request3 = Request('http://example.com/3', meta={'proxy': 'http://188.3.36.21:9119'})
按照与之前相同的做法,在scrapy shell进行实验,验证代理是否被使用:
$ scrapy shell
...
>>> from scrapy import Request
>>> req = Request('http://httpbin.org/ip', meta={'proxy': 'http://116.29.35.201:8118'})
>>> fetch(req)
[scrapy] DEBUG: Crawled (200) (referer: None)
>>> json.loads(response.text)
{'origin': '116.29.35.201'}
>>> req = Request('https://httpbin.org/ip', meta={'proxy': 'http://197.10.171.143:8118'})
>>> fetch(req)
[scrapy] DEBUG: Crawled (200) (referer: None)
>>> json.loads(response.text)
{'origin': '197.10.171.143'}
结果表明,Scrapy爬虫同样使用了指定的代理服务器。
使用手动方式设置代理时,如果使用的代理需要身份验证,还需要通过HTTP头部的Proxy-Authorization字段传递包含用户账号和密码的身份验证信息。可以参考HttpProxyMiddleware._get_proxy中的相关实现,按以下过程生成身份验证信息:
(1)将账号、密码拼接成形如'user:passwd'的字符串s1。
(2)按代理服务器要求对s1进行编码(如utf8),生成s2。
(3)再对s2进行Base64编码,生成s3。
(4)将s3拼接到固定字节串b'Basic '后面,得到最终的身份验证信息。
示例代码如下:
>>> from scrapy import Request
>>> import base64
>>> req = Request('http://httpbin.org/ip', meta={'proxy': 'http://116.29.35.201:8118'})
>>> user = 'liushuo'
>>> passwd = '12345678'
>>> user_passwd = ('%s:%s' % (user, passwd)).encode('utf8')
>>> user_passwd
b'liushuo:12345678'
>>> req.headers['Proxy-Authorization'] = b'Basic ' + base64.b64encode(user_passwd)
>>> fetch(req)
...
13.3 获取免费代理
现在,我们了解了如何为Scrapy爬虫设置代理,接下来的一个话题便是如何获取代理服务器。如果你感觉购买云服务器(亚马逊云或阿里云服务器等)自行搭建代理服务器的成本太高(但可靠、可控),那么可以通过google或baidu找到一些提供免费代理服务器信息的网站,例如:
http://proxy-list.org(国外)
https://free-proxy-list.net(国外)
http://www.xicidaili.com
http://www.proxy360.cn
http://www.kuaidaili.com
以http://www.xicidaili.com为例,图13-3所示为该网站“国内高匿代理”分类下的页面。
从中可以看出,该网站提供了大量的免费代理服务器信息,如果只需要少量的代理,从中选择几个就可以了,不过通常直觉告诉我们“多多益善”。接下来编写爬虫,爬取“国内高匿代理”分类下前3页的所有代理服务器信息,并验证每个代理是否可用。
创建Scrapy项目,取名为proxy_example:
$ scrapy startproject proxy_example
该网站会检测用户发送的HTTP请求头部中的User-Agent字段,因此我们需要伪装成某种常规浏览器,在配置文件添加如下代码:
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/41.0.2272.76'
实现XiciSpider爬取代理服务器信息,并过滤不可用代理,代码如下:
# -*- coding: utf-8 -*-
import scrapy
from scrapy import Request
import json
class XiciSpider(scrapy.Spider):
name = "xici_proxy"
allowed_domains = ["www.xicidaili.com"]
def start_requests(self):
#爬取http://www.xicidaili.com/nn/前3 页
foriin range(1, 4):
yield Request('http://www.xicidaili.com/nn/%s' % i)
def parse(self, response):
for sel in response.xpath('//table[@id="ip_list"]/tr[position()>1]'):
# 提取代理的IP、port、scheme(http or https)
ip = sel.css('td:nth-child(2)::text').extract_first()
port = sel.css('td:nth-child(3)::text').extract_first()
scheme = sel.css('td:nth-child(6)::text').extract_first().lower()
# 使用爬取到的代理再次发送请求到http(s)://httpbin.org/ip,验证代理是否可用
url = '%s://httpbin.org/ip' % scheme
proxy = '%s://%s:%s' % (scheme, ip, port)
meta = {
'proxy': proxy,
'dont_retry': True,
'download_timeout': 10,
# 以下两个字段是传递给check_available 方法的信息,方便检测
'_proxy_scheme': scheme,
'_proxy_ip': ip,
}
yield Request(url, callback=self.check_available,
meta=meta, dont_filter=True)
def check_available(self, response):
proxy_ip = response.meta['_proxy_ip']
# 判断代理是否具有隐藏IP 功能
if proxy_ip == json.loads(response.text)['origin']:
yield {
'proxy_scheme': response.meta['_proxy_scheme'],
'proxy': response.meta['proxy'],
}
解释上述代码如下:
● 在start_requests方法中请求http://www.xicidaili.com/nn下的前3页,以parse方法作为页面解析函数。
● 在parse方法中提取一个页面中所有的代理服务器信息,这些代理未必都是可用的,因此使用爬取到的代理发送请求到http(s)://httpbin.org/ip验证其是否可用,以check_available方法作为页面解析函数。
● 能执行到check_available方法,意味着response对应请求所使用的代理是可用的。在check_available方法中,通过响应json串中的origin字段可以判断代理是否是匿名的(隐藏ip),返回匿名代理。
运行爬虫,将可用的代理服务器保存到json文件中,供其他程序使用:
$ scrapy crawl xici_proxy -o proxy_list.json
...
$ cat proxy_list.json
[
{"proxy": "http://110.73.10.37:8123", "proxy_scheme": "http"},
{"proxy": "http://171.38.142.24:8123", "proxy_scheme": "http"},
{"proxy": "http://111.155.124.84:8123", "proxy_scheme": "http"},
{"proxy": "http://203.88.210.121:138", "proxy_scheme": "http"},
{"proxy": "http://182.88.191.195:8123", "proxy_scheme": "http"},
{"proxy": "http://121.31.151.231:8123", "proxy_scheme": "http"},
{"proxy": "http://203.93.0.115:80", "proxy_scheme": "http"},
{"proxy": "http://222.85.39.29:808", "proxy_scheme": "http"},
{"proxy": "http://175.155.25.26:808", "proxy_scheme": "http"},
{"proxy": "http://111.155.124.72:8123", "proxy_scheme": "http"},
{"proxy": "http://122.5.81.153:8118", "proxy_scheme": "http"},
{"proxy": "https://171.37.153.24:8123", "proxy_scheme": "https"},
{"proxy": "http://110.73.1.68:8123", "proxy_scheme": "http"},
{"proxy": "http://122.228.179.178:80", "proxy_scheme": "http"},
{"proxy": "http://121.31.151.226:8123", "proxy_scheme": "http"},
{"proxy": "http://171.38.171.168:8123", "proxy_scheme": "http"},
{"proxy": "http://218.22.219.133:808", "proxy_scheme": "http"},
{"proxy": "https://171.38.130.188:8123", "proxy_scheme": "https"},
{"proxy": "http://111.155.116.219:8123", "proxy_scheme": "http"},
{"proxy": "http://121.31.149.209:8123", "proxy_scheme": "http"},
{"proxy": "http://60.169.78.218:808", "proxy_scheme": "http"},
{"proxy": "http://171.38.158.227:8123", "proxy_scheme": "http"},
{"proxy": "http://121.31.150.224:8123", "proxy_scheme": "http"},
{"proxy": "http://111.155.124.78:8123", "proxy_scheme": "http"},
{"proxy": "http://182.90.83.104:8123", "proxy_scheme": "http"}
]
如结果所示,我们成功获取到了20多个可用的免费代理。
13.4 实现随机代理
本章开始部分曾提到,某些网站为防止爬虫爬取会对接收到的请求进行监测,如果在短时间内接收到了来自同一IP的大量请求,就判定该IP的主机在使用爬虫程序爬取网站,因而将该IP封禁(拒绝请求)。爬虫程序可以使用多个代理对此类网站进行爬取,此时单位时间的访问量会被多个代理分摊,从而避免封禁IP。
下面我们基于HttpProxyMiddleware实现一个随机代理下载中间件。
在middlewares.py中实现RandomHttpProxyMiddleware,代码如下:
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
from collections import defaultdict
import json
import random
class RandomHttpProxyMiddleware(HttpProxyMiddleware):
def __init__(self, auth_encoding='latin-1', proxy_list_file=None):
if not proxy_list_file:
raise NotConfigured
self.auth_encoding = auth_encoding
# 分别用两个列表维护HTTP和HTTPS的代理,{'http': [...], 'https': [...]}
self.proxies = defaultdict(list)
# 从json文件中读取代理服务器信息,填入self.proxies
with open(proxy_list_file) as f:
proxy_list = json.load(f)
for proxy in proxy_list:
scheme = proxy['proxy_scheme']
url = proxy['proxy']
self.proxies[scheme].append(self._get_proxy(url, scheme))
@classmethod
def from_crawler(cls, crawler):
# 从配置文件中读取用户验证信息的编码
auth_encoding = crawler.settings.get('HTTPPROXY_AUTH_ENCODING', 'latain-1')
# 从配置文件中读取代理服务器列表文件(json)的路径
proxy_list_file = crawler.settings.get('HTTPPROXY_PROXY_LIST_FILE')
return cls(auth_encoding, proxy_list_file)
def _set_proxy(self, request, scheme):
# 随机选择一个代理
creds, proxy = random.choice(self.proxies[scheme])
request.meta['proxy'] = proxy
if creds:
request.headers['Proxy-Authorization'] = b'Basic ' + creds
解释上述代码如下:
● 仿照HttpProxyMiddleware构造器实现RandomHttpProxyMiddleware构造器,首先从代理服务器列表文件(配置文件中指定)中读取代理服务器信息,然后将它们按协议(HTTP或HTTPS)分别存入不同列表,由self.proxis字典维护。
● _set_proxy方法负责为每一个Request请求设置代理,覆写_set_proxy方法(覆盖基类方法)。对于每一个request,根据请求协议获取self.proxis中的代理服务器列表,然后从中随机抽取一个代理,赋值给request.meta['proxy']。
在配置文件settings.py中启用RandomHttpProxyMiddleware,并指定所要使用的代理服务器列表文件(json文件),添加代码如下:
DOWNLOADER_MIDDLEWARES = {
# 置于HttpProxyMiddleware(750)之前
'proxy_example.middlewares.RandomHttpProxyMiddleware': 745,
}
# 使用之前在http://www.xicidaili.com/网站爬取到的代理
HTTPPROXY_PROXY_LIST_FILE='proxy_list.json'
最后编写一个TestRandomProxySpider测试该中间件,重复向http(s)://httpbin.org/ip发送请求,根据响应中的请求源IP地址信息判断代理使用情况:
# -*- coding: utf-8 -*-
import scrapy
from scrapy import Request
import json
class TestRandomProxySpider(scrapy.Spider):
name = "test_random_proxy"
def start_requests(self):
for _ in range(100):
yield Request('http://httpbin.org/ip', dont_filter=True)
yield Request('https://httpbin.org/ip', dont_filter=True)
def parse(self, response):
print(json.loads(response.text))
运行爬虫,观察输出:
$ scrapy crawl test_random_proxy
[scrapy] DEBUG: Crawled (200) <GET https://httpbin.org/ip> (referer: None)
{'origin': '171.38.130.188'}
[scrapy] DEBUG: Crawled (200) <GET http://httpbin.org/ip> (referer: None)
{'origin': '182.90.83.104'}
[scrapy] DEBUG: Crawled (200) <GET https://httpbin.org/ip> (referer: None)
{'origin': '171.38.130.188'}
[scrapy] DEBUG: Crawled (200) <GET http://httpbin.org/ip> (referer: None)
{'origin': '203.88.210.121'}
[scrapy] DEBUG: Crawled (200) <GET http://httpbin.org/ip> (referer: None)
{'origin': '203.88.210.121'}
...
结果表明,RandomHttpProxyMiddleware工作良好,Scrapy爬虫随机地使用了多个代理。
13.5 项目实战:爬取豆瓣电影信息
最后,我们来完成一个使用代理爬取的实战项目。豆瓣网的电影专栏是国内权威电影评分网站,其中包括海量影片信息,在浏览器中访问https://movie.douban.com,并选择一个分类(如“豆瓣高分”),可看到如图13-4所示的影片列表页面。
单击其中一部影片,进入其页面(简称影片页面),如图13-5所示。
在影片页面中可以看到一部影片的基本信息,如导演、编剧、主演、类型等,我们可以编写爬虫在豆瓣电影中爬取大量影片信息。
13.5.1 项目需求
爬取豆瓣电影中“豆瓣高分”分类下的所有影片信息,需要爬取一部影片的信息字段如下:
● 导演
● 编剧
● 主演
● 类型
● 制片国家/地区
● 语言
● 上映日期
● 片长
● 又名
由于豆瓣网对爬取速度做了限制,高速爬取可能会被封禁IP,因此使用代理进行爬取。
13.5.2 页面分析
首先分析影片列表页面,页面中的每一部电影都是通过JavaScript脚本加载的。单击页面最下方的“加载更多”,可以在Chrome开发者工具中捕获到jQuery发送的HTTP请求(加载更多影片),该请求返回了一个json串,如图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中可以看出,影片的信息在<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部影片信息。
13.6 本章小结
本章学习了Scrapy爬虫如何使用代理进行爬取,首先介绍了两种设置代理的方法:
● 使用下载中间件HttpProxyMiddleware(自动)。
● 在构造Request对象时通过meta参数设置(手动)。
前者使用简单,只需通过环境变量配置即可;后者可在某些特殊场景下使用。我们以如何使用多个代理进行了举例,随后讲解了在网络上获取免费代理的方法,并利用获取的免费代理实现了一个随机代理中间件。最后,运用本章所学的知识完成一个实战项目,使用代理爬取了豆瓣网中的电影信息。