Scrapy 对接 Splash

上一节我们了解了 Scrapy 对接 Selenium 的原理和实现流程,当然这是一种实现 Scrapy 爬取 JavaScript 渲染页面的方案,但方案不止这一种,利用 Splash 和 Pyppeteer 同样可以实现。

本节我们来了解一下 Scrapy 对接 Splash 爬取 JavaScript 渲染页面的流程。

准备工作

本节要爬取的目标网站和需求与上一节是一致的,在这里就不再展开介绍了。不同的是实现方案由 Selenium 切换为了 Splash,所以我们要实现 Scrapy 和 Splash 的对接。

要实现 Scrapy 和 Splash 的对接,我们需要借助于 Scrapy-Splash 库,另外还需要一个可以正常使用的 Splash 服务。

在开始本节的学习之前,请确保安装好 Scrapy 框架,另外请确保 Splash 已经正确安装并正常运行,另外我们还需要安装好 Scrapy-Splash 库,具体的安装过程可以参考: https://setup.scrape.center/scrapy-splash

对接原理

Scrapy 对接 Splash 和 Selenium 的原理是不同的,上一节对接 Selenium 是借助于 Downloader Middleware 实现的,在 Downloader Middleware 里,我们实现了 Chrome 浏览器渲染页面的过程,并构造了 HtmlResponse 返回给 Spider。

而 Splash 本身就是一个 JavaScript 页面渲染服务,我们只需要将需要渲染页面的 URL 发送给 Splash 就能得到对应的 JavaScript 渲染结果,而 Scrapy-Splash 则是提供了这个过程基本功能的封装,比如 Cookie 的处理,URL 的转换等。

下面我们来具体了解下它的用法。

对接实战

首先新建一个项目,名为 scrapysplashdemo,命令如下所示:

scrapy startproject scrapysplashdemo
bash

进入项目,新建一个 Spider,命令如下所示:

scrapy genspider book spa5.scrape.center
bash

这样我们便创建了初始的 Spider,然后创建一个同样的 BookItem,代码如下:

import scrapy
from scrapy.item import Item, Field


class BookItem(Item):
    name = Field()
    tags = Field()
    score = Field()
    cover = Field()
    price = Field()
python

接下来就需要进行 Scrapy-Splash 相关的配置了,可以参考 Scrapy-Splash 的配置说明: https://github.com/scrapy-plugins/scrapy-splash#contiguration。

修改 settings.py,配置 SPLASH_URL。这里的 Splash 运行在本地,所以可以直接配置本地的地址:

SPLASH_URL = 'http://localhost:8050'
bash

如果 Splash 是在远程服务器运行的,那么此处就应该配置为远程的地址,例如我配置了一个 Splash 集群,地址为 https://splash.scrape.center,用户名和密码均为 admin,则此处的配置应该是这样的:

SPLASH_URL = 'https://splash.scrape.center'
bash

另外还需要在 Spider 里面增加下面两个变量的定义以支持 HTTP Basic Authentication:

class BookSpider(Spider):
    http_user = 'admin'
    http_pass = 'admin'
python

接着配置几个 Middleware,代码如下所示:

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100
}
bash

这里配置了 3 个 Downloader Middleware 和一个 Spider Middleware,这是 Scrapy-Splash 的核心部分,我们不再需要像对接 Selenium 那样实现一个 Downloader Middleware,Scrapy-Splash 库都为我们准备好了,直接配置即可。

还需要配置一个去重的类 DUPEFILTER_CLASS,代码如下所示:

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
bash

最后配置一个 Cache 存储 HTTPCACHE_STORAGE,代码如下所示:

HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
bash

配置完成之后,我们就可以利用 Splash 来抓取页面了。我们可以直接生成一个 SplashRequest 对象并传递相应的参数,Scrapy 会将此请求转发给 Splash,Splash 对页面进行渲染加载,再将渲染结果传递回来。此时 Response 的内容就是渲染完成的结果了,最后交给 Spider 解析即可。

我们来看一个示例、代码如下所示:

yield SplashRequest(url, self.parse_result, args={
        'wait': 0.5, # 等待时间
    },
    endpoint='render.json', # 可选参数,splash 渲染终端
    splash_url='<url>',    # 可选参数,覆盖 SPLASH_URL
)
python

在这里构造了一个 SplashRequest 对象,前两个参数依然是请求的 URL 和回调函数,可以通过 args 传递一些渲染参数,例如等待时间 wait 等,还可以根据 endpoint 参数指定渲染接口,更多参数可以参考文档的说明: https://github.com/scrapy-plugins/scrapy-splash#requests

另外我们也可以生成 Request 对象,关于 Splash 的配置通过 meta 属性配置即可,代码如下:

yield scrapy.Request(url, self.parse_result, meta={
    'splash': {
        'args': {
            'html': 1,
            'png': 1,
        },
        # 以下为可选参数
        'endpoint': 'render.json', # 可选参数, Splash 渲染终端,默认为 render.json
        'splash_url': '<url>', # 可选参数, 覆盖 SPLASH_URL
        'splash_headers': {},  # 可选参数,发送给 Splash 渲染时候设置的 Headers
        'dont_process_response': True, # 可选参数,不处理 Response,默认是 False
        'dont_send_headers': True, # 可选参数,不发送 Headers,默认是 False
    }
})
python

通过 args 来配置 SplashRequest 对象与通过 meta 来配置 Request 对象,两种方式达到的效果是相同的。

我们可以首先定义一个 Lua 脚本,来实现页面加载,代码如下所示:

function main(splash, args)
    assert(splash:go(args.url))
    assert(splash:wait(5))
    return {
        html = splash:html(),
        png = splash:png(),
        har = splash:har()
    }
end
lua

逻辑非常简单,就是获取参数中的 url 属性并访问,然后等待 5 秒,最后把截图、HTML 代码、HAR 信息返回。

我们将脚本放到 Splash 中运行,同时设置目标 URL 为 https://spa5.scrape.center,如图 15-16 所示。

(图略)

点击 Render me 按钮,可以看到 Splash 中就出现了对应的渲染结果,如图 15-17 所示。

(图略)

测试成功之后,我们只需要在 Spider 里用 SplashRequest 对接 Lua 脚本就好了,代码如下所示:

script = """
function main(splash, args)
  assert(splash:go(args.url))
  assert(splash:wait(5))
  return splash:html()
end
"""

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

    def start_requests(self):
        start_url = f'{self.base_url}/page/1'
        yield SplashRequest(start_url, callback=self.parse_index, args={'lua_source': script}, endpoint='execute')
python

这里我们把 Lua 脚本定义成长字符串,通过 SplashRequest 的 args 来传递参数。另外,args 参数里有一个 lua_source 字段,它可以用于指定 Lua 脚本内容。于是我们成功构造了一个 SplashRequest,对接 Splash 的工作就完成了。

实现其他方法也一样,我们需要把 Request 都按照要求修改为 SplashRequest,相关代码改写如下:

def parse_index(self, response):
    items = response.css('.item')
    for item in items:
        href = item.css('.top a::attr(href)').extract_first()
        detail_url = response.urljoin(href)
        yield SplashRequest(detail_url, callback=self.parse_detail, priority=2,
                            args={'lua_source': script}, endpoint='execute')

    # next page
    match = re.search(r'page/(\d+)', response.url)
    if not match: return
    page = int(match.group(1)) + 1
    next_url = f'{self.base_url}/page/{page}'
    yield SplashRequest(next_url, callback=self.parse_index,
                        args={'lua_source': script}, endpoint='execute')

def parse_detail(self, response):
    name = response.css('.name::text').extract_first()
    tags = response.css('.tags button span::text').extract()
    score = response.css('.score::text').extract_first()
    price = response.css('.price span::text').extract_first()
    cover = response.css('.cover::attr(src)').extract_first()
    tags = [tag.strip() for tag in tags] if tags else []
    score = score.strip() if score else None
    item = BookItem(name=name, tags=tags, score=score, price=price, cover=cover)
    yield item
python

这里我们参考上一节的内容,将 SeleniumRequest 修改为了 SplashRequest,同时增加了 args 参数配置,其他的逻辑基本一致。这样一来,Scrapy 和 Splash 的对接就全部完成了。

接下来,我们通过如下命令运行爬取:

scrapy crawl book
bash

可以看到我们同样完成了结果的爬取,运行结果如下:

(略)
bash

由于 Splash 和 Scrapy 都支持异步处理,我们可以看到同时会有多个抓取成功的结果。另外在使用了 Splash 之后,爬虫的主体逻辑和 JavaScript 渲染流程是完全分开的,因此只要 Splash 能够承受对应的道染并发量,爬取效率还是不错的。

为了提高 Splash 的渲染能力,我们可以将 Splash 配置为集群,这样一来其渲染能力会成倍提升,具体的配置方案可以参考 https://setup.scrape.center/splash-cluster 单面的说明。

总结

本节中我们介绍了 Scrapy 对接 Splash 实现爬取 JavaScript 渲染页面的流程,同样不失为一个不错的方案。