第13章
使用HTTP代理

大家可能都有过给浏览器设置HTTP代理的经验,HTTP代理服务器可以比作客户端与Web服务器(网站)之间的一个信息中转站,客户端发送的HTTP请求和Web服务器返回的HTTP响应通过代理服务器转发给对方,如图13-1所示。

阅读 ‧ 电子书库

图13-1

爬虫程序在爬取某些网站时也需要使用代理,例如:

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

● 由于网络环境因素,直接爬取速度太慢,使用代理提高爬取速度。

● 某些网站对用户的访问速度进行限制,爬取过快会被封禁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地址:

阅读 ‧ 电子书库

图13-2

     $ 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所示为该网站“国内高匿代理”分类下的页面。

阅读 ‧ 电子书库

图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-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部影片信息。

13.6 本章小结

本章学习了Scrapy爬虫如何使用代理进行爬取,首先介绍了两种设置代理的方法:

● 使用下载中间件HttpProxyMiddleware(自动)。

● 在构造Request对象时通过meta参数设置(手动)。

前者使用简单,只需通过环境变量配置即可;后者可在某些特殊场景下使用。我们以如何使用多个代理进行了举例,随后讲解了在网络上获取免费代理的方法,并利用获取的免费代理实现了一个随机代理中间件。最后,运用本章所学的知识完成一个实战项目,使用代理爬取了豆瓣网中的电影信息。