Scrapy 实战

通过本章前面几节的学习,我们已经了解了 Scrapy 的基本用法,规则化爬虫,JavaScript 渲染页面的爬取,而在之前的章节,我们还学习了运用代理池,账号池等规避反爬措施。本节中我们就来综合一下前面所学的知识,完成一个 Scrapy 实战项目,加深对 Scrapy 的理解。

本节目标

本节我们需要爬取的站点为 https://antispider7.scrape.center/ ,这个站点需要登录才能爬取,登录之后(测试账号的用户名和密码均为 admin)我们便可以看到类似图 15-21 所示的页面。

图略

这里是一些书籍信息,我们需要进入每一本书对应的详情页,将该本书的信息爬取下来,总数将近一万本。

不过,这个站点设置了一些反爬措施。它限制单个账号 5 分钟内最多访问页面 10 次,超过的话账号会被封禁;另外该站点限制了单个 IP 的访问频率,同样是 5 分钟内最多访问 10 次,超过这个频率,IP 便会被封禁。因此,该站点从账号层面、IP层面都做了限制,如果仅用一个账号和一个 IP,那么在短时间内是无法完成爬取的。

总而言之,这个限制算是相对严格了,要爬取这个站点,我们就需要结合之前的知识综合实现。现在主要面临两个问题。

  • 封禁账号:前文我们已经讲解了账号池的用法,利用账号池,我们可以采用分流策略大天降低单个账号的请求频率,从而降低账号被封禁的概率。

  • 封禁 IP:前文我们已经讲解了代理池的用法,利用代理池,我们可以维护大量 IP,每次请求都随机切换一个 IP,这样一来就可以解决 IP 被封禁的问题。

因此,本节我们需要实现以下几点功能。

  • 利用 Scrapy 实现站点的爬取逻辑。

  • 对接账号池,突破账号访问频率的限制。

  • 对接代理池,突破代理访问频率的限制。

准备

在本节开始之前,请确保你已经安装好了 Scrapy 框架,并准备好了前面章节所讲解的代理池和账号池并可以成功运行,具体说明如下:

  • 对于代理池,可以参考 https://github.com/Python3WebSpider/ProxyPool 里面的说明来安装,具体的原理和实现可以参考本书 9.2 节。

  • 对于账号池,可以参考 https://github.com/Python3WebSpider/AccountPool 里面的说明来安装,具体的原理和实现可以参考本书 10.4 节。另外注意本节我们需要基于 10.4 节所述的内容对账号池进行进一步改写,所以建议下载 antispider6 这个分支的账号池代码,可以直接使用如下命令更新对应代码:

git clone --single-branch --branch antispider6 https://github.com/Python3webSpider/AccountPool.git

这样下载的代码就是 antispider6 这个分支的代码,本节我们需要基于它来扩展 antispider7 这个站点的账号池逻辑。

分析

首先我们来分析一下这个站点如何来爬取,直接爬取页面还是利用 Ajax 接口?

打开 https://antispider7.scrape.center/ ,首先页面会提示需要登录,我们可以先使用用户名 admin,密码 admin 登录。然后分析页面的呈现逻辑,综合前面的知识,我们可以轻易分析出来每一页的列表数据是通过 Ajax 加载的,接口如图 15-22 所示。

图略

接着切换到 Preview 选项卡看返回结果,如图 15-23 所示,

图略

这里我们可以看到返回结果只包含了 id、name、score、cover、authors 这几个字段,明显还不全。我们进入书籍详情页面来看一下,例如进入 https://antispider7.scrape.center/detail/26607683 这个页面,可以看到更全的信息,如图 15-24 所示。

图略

这里我们可以看到还有标签、定价、出版时间、ISBN、评价等内容,分析其数据来源发现这些也是通过 Ajax 接口加载的,如图 15-25 所示。

图略

因此,要爬取全部数据,我们需要从列表页接口获取书籍的 ID,然后根据 ID 从详情页接口爬取每一本书的详情。

到现在,爬取逻辑就已经梳理清楚了。接下来我们看看怎样解决权限的问题,分析一下 Reqeust Headers,可以看到有一个 authorization 字段,以 jwt 开头,内容类似下面这样:

省略

另外 Requests Headers 也有 cookie 字段,里面包含了 Session ID 相关的信息。

为了验证其认证方式,我们可以对 authorization 和 cookie 进行删减测试,比如去掉 cookie 字段,仅使用 authorization 仍然可以成功获取数据,那就证明其权限认证需要 authorization 字段而不一定需要 cookie。

最后经验证可以得到,其权限认证是基于 JWT 的,我们仅使用 authorization 就可以成功获取数据。如此一来,权限认证我们就大体清楚了。

接下来我们就来实现该站点的爬取流程吧!

实战

下面我们就开始利用 Scrapy 实现对示例网站 https://antispider7.scrape.center/ 的爬取了。

  • 主逻辑实现

首先我们可以利用 Scrapy 实现一下基本的爬取逻辑,首先新建一个 Scrapy 项目,名字叫作 scrapycompositedemo,创建命令如下:

scrapy startproject scrapycompositedemo

接下来进入项目,然后新建一个 Spider,名称为 book,命令如下:

scrapy genspider book antispider7.scrape.center

然后我们来定义一个 Item,定义需要爬取的字段,这里我们直接和详情页接口返回的字段一致就好了,在 items.py 里面定义一个 BookItem,代码如下:

from scrapy import Field, Item


class BookItem(Item):
    authors = Field()
    catalog = Field()
    comments = Field()
    cover = Field()
    id = Field()
    introduction = Field()
    isbn = Field()
    name = Field()
    page_number = Field()
    price = Field()
    published_at = Field()
    publisher = Field()
    score = Field()
    tags = Field()
    translators = Field()

定义好 BookItem 之后,我们便可以将爬取结果转换成一个个 BookItem 了。

然后我们来实现主要的爬取逻辑,这里我们先直接实现爬取 Ajax 接口的逻辑,在 book.py 里面改写代码如下:

from scrapy import Request, Spider

class BookSpider(Spider):
    name = 'book'
    allowed_domains = ['antispider7.scrape.center']
    base_url = 'https://antispider7.scrape.center'
    max_page = 512

    def start_requests(self):
        for page in range(1, self.max_page + 1):
            url = f'{self.base_url}/api/book/?limit=18&offset={(page - 1) * 18}'
            yield Request(url, callback=self.parse_index)

    def parse_index(self, response):
        print(response)

这单我们构造了 512 页的列表页 Ajax 请求,指定 limit 和 offset 参数,offset 根据页码动态计算,构造 URL 之后生成 Request,然后回调方法设置为 parse_index 方法,打印输出 response 对象。

这里我们仅仅是实现了基本的请求逻辑,并没有加任何模拟登录操作,运行结果会是怎样的呢?我们来尝试下,运行该 Spider,命令如下:

scrapy crawl book

我们会得到如下的运行结果:

省略

可以看到状态码都是 401,而 401 就是代表未授权的意思,这就是因为没有登录造成的。

  • 模拟登录

前面我们也已经分析过了怎样以登录身份请求接口,其实就是在 Request Headers 中加上 authorization 这个字段就好了,怎么来实现呢?在前面我们也已经学习了 Downloader Middleware 的用法,它可以在 Request 被下载执行前对 Request 做一些处理,所以这里我们可以借助于 Downloader Middleware 来实现。

接下来我们在 middlewares.py 里面添加一个 Downloader Middleware,代码如下:

class AuthorizationMiddleware(object):
    authorization = 'jwt xxxxx'

    async def process_request(self, request, spider):
        request.headers['authorization'] = self.authorization

这里实现了一个 AuthorizationMiddleware 类,并实现了一个 process_request 方法,在这个方法里,我们为 request 变量的 headers 属性添加了 authorization 字段。注意这里 authorization 的值你可以改写成自已的:当前的 authorization 可能已经无法使用了,请登录站点并分析 Ajax 接口,复制 authorization 字段并替换。

定义之后我们还需要开启对 AuthorizationMiddleware 的调用,在 settings.py 里面添加代码如下:

DOWNLOADER_MIDDLEWARES = {
   'scrapycompositedemo.middlewares.AuthorizationMiddleware': 543,
}

好,这样我们就开启了 AuthorizationMiddleware 了,重新运行一下 Spider,可以看到如下结果:

省略

不幸的事情又发生了,最初的几次请求结果的状态码是 200,代表爬取成功,说明模拟登录已经成功了可是后续的请求状态码文变成了 403,403 代表禁止访问,其实这就是因为爬取频率过高当前账号或 IP 已经被禁正访问了,因为这个站点有 IP 和单个账号请求频率限制。

出现现在这个情况,如果我们不知道当前站点的反爬策略,一般得经过一些实验来找出来其中的封禁规律。比如这时候可以通过一些控制变量法的实验来进行验证,

  • 如果想验证是不是 IP 被封禁,我们可以尝试更换当前计算机的 IP 或者使用代理来更换 IP 重新进行请求,如果这时候可以正常请求了,那就证明是 IP 被封禁了。

  • 如果想验证是不是账号被封禁,可以尝试更换账号重新进行请求,如果这时候可以正常请求了那就证明是账号被封禁了。

所以一般在不知道封禁原因的情况下,可以多进行尝试。在这里我就不再进行尝试了,这个站点就是既封禁IP、支封禁账号,有双重反爬。

好,那我们就来一个个解决吧。

  • 解决封IP问题

在这里我们重新将前面所讲的代理池运行起来,代理池运行之后,便可以通过 URL 来获取一个随机代理,例如访问 http://localhost:5555/random 就可以获取一个随机代理,如图 15-26 所示。

这样我们就可以把该代理对接到爬虫项目中了。我们可以再实现一个 Downloader Middleware,实现如下:

import aiohttp
import logging

class ProxyMiddleware(object):
    proxypool_url = 'http://localhost:5555/random'
    logger = logging.getLogger('middlewares.proxy')

    async def process_request(self, request, spider):
        async with aiohttp.ClientSession() as client:
            response = await client.get(self.proxypool_url)
            if not response.status == 200:
                return
            proxy = await response.text()
            self.logger.debug(f'set proxy {proxy}')
            request.meta['proxy'] = f'http://{proxy}'

这里我们实现了一个 ProxyMiddleware,它的主要逻辑就是请求该代理池然后获取其返回内容,返回的内容便是一个代理地址。接着我们直接将代理赋值给 request 的 meta 属性的 proxy 字段即可。

值得注意的是,由于 Scrapy 2.0 及以上版本支持 asyncio,所以这里我们获取代理使用的是 aiohttp,可以更方便地实现异步操作,可以看到我们给 process_request 方法加上了 async 关键字,这样在方法内便可以使用 asyncio 的相关特性了。

为了开启 Scrapy 对 asyncio 的支持,我们需要手动配置一下 TWISTED_REACTOR,在 settings.py 里面添加设置如下:

TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'

接着我们再开启 ProxyMiddleware 的调用,配置如下:

DOWNLOADER_MIDDLEWARES = {
   'scrapycompositedemo.middlewares.AuthorizationMiddleware': 543,
   'scrapycompositedemo.middlewares.ProxyMiddleware': 544,
}

好,这样我们就可以在每次发起一个请求的时候随机切换代理池中的代理了,IP 被封禁的问题就解决了。

  • 解决封账号问题

不过这样可没完,实际运行仍然会出现 403 状态码,这是因为账号也被封禁了。为了解决账号封禁的问题,我们需要进一步对接一个账号池。

关于账号池的原理,就不再过多叙述了。接下来我们需要基于 10.4 节的账号池对该站点进行扩展,使其可以应用于目标站点。

首先我们需要在 setting.py 里面修改配置,改成 antispider7 站点,改写 Generator 和 Tester 的类的配置,修改如下:

总结

本节是 Scrapy 综合实战练习,为了解决与反爬虫相关的问题,我们综合了代理池、账号池和 Scrapy 的一些优化设置,完成了对站点反爬虫的绕过和数据的爬取。

在进行对其他站点的实际爬取时,可以借鉴本节的思路,希望天家好好体会。

本节代码的参考来源如下。