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 单面的说明。