Scrapy框架的使用之Scrapy對接Splash

崔慶才丨靜覓發表於2018-05-18

在上一節我們實現了Scrapy對接Selenium抓取淘寶商品的過程,這是一種抓取JavaScript動態渲染頁面的方式。除了Selenium,Splash也可以實現同樣的功能。本節我們來了解Scrapy對接Splash來進行頁面抓取的方式。

一、準備工作

請確保Splash已經正確安裝並正常執行,同時安裝好Scrapy-Splash庫。

二、新建專案

首先新建一個專案,名為scrapysplashtest,命令如下所示:

scrapy startproject scrapysplashtest複製程式碼

新建一個 Spider,命令如下所示:

scrapy genspider taobao www.taobao.com複製程式碼

三、新增配置

可以參考Scrapy-Splash的配置說明進行一步步的配置,連結如下:https://github.com/scrapy-plugins/scrapy-splash#configuration。

修改settings.py,配置SPLASH_URL。在這裡我們的Splash是在本地執行的,所以可以直接配置本地的地址:

SPLASH_URL = 'http://localhost:8050'複製程式碼

如果Splash是在遠端伺服器執行的,那此處就應該配置為遠端的地址。例如執行在IP為120.27.34.25的伺服器上,則此處應該配置為:

SPLASH_URL = 'http://120.27.34.25:8050'複製程式碼

還需要配置幾個Middleware,程式碼如下所示:

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}複製程式碼

這裡配置了三個Downloader Middleware和一個Spider Middleware,這是Scrapy-Splash的核心部分。我們不再需要像對接Selenium那樣實現一個Downloader Middleware,Scrapy-Splash庫都為我們準備好了,直接配置即可。

還需要配置一個去重的類DUPEFILTER_CLASS,程式碼如下所示:

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'複製程式碼

最後配置一個Cache儲存HTTPCACHE_STORAGE,程式碼如下所示:

HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'複製程式碼

四、新建請求

配置完成之後,我們就可以利用Splash來抓取頁面了。我們可以直接生成一個SplashRequest物件並傳遞相應的引數,Scrapy會將此請求轉發給Splash,Splash對頁面進行渲染載入,然後再將渲染結果傳遞回來。此時Response的內容就是渲染完成的頁面結果了,最後交給Spider解析即可。

我們來看一個示例,如下所示:

yield SplashRequest(url, self.parse_result,
    args={
        # optional; parameters passed to Splash HTTP API
        'wait': 0.5,
        # 'url' is prefilled from request url
        # 'http_method' is set to 'POST' for POST requests
        # 'body' is set to request body for POST requests
    },
    endpoint='render.json', # optional; default is render.html
    splash_url='<url>',     # optional; overrides SPLASH_URL
)複製程式碼

這裡構造了一個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': {
            # set rendering arguments here
            'html': 1,
            'png': 1,
            # 'url' is prefilled from request url
            # 'http_method' is set to 'POST' for POST requests
            # 'body' is set to request body for POST requests
        },
        # optional parameters
        'endpoint': 'render.json',  # optional; default is render.json
        'splash_url': '<url>',      # optional; overrides SPLASH_URL
        'slot_policy': scrapy_splash.SlotPolicy.PER_DOMAIN,
        'splash_headers': {},       # optional; a dict with headers sent to Splash
        'dont_process_response': True, # optional, default is False
        'dont_send_headers': True,  # optional, default is False
        'magic_response': False,    # optional, default is True
    }
})複製程式碼

SplashRequest物件通過args來配置和Request物件通過meta來配置,兩種方式達到的效果是相同的。

本節我們要做的抓取是淘寶商品資訊,涉及頁面載入等待、模擬點選翻頁等操作。我們可以首先定義一個Lua指令碼,來實現頁面載入、模擬點選翻頁的功能,程式碼如下所示:

function main(splash, args)
  args = {
    url="https://s.taobao.com/search?q=iPad",
    wait=5,
    page=5
  }
  splash.images_enabled = false
  assert(splash:go(args.url))
  assert(splash:wait(args.wait))
  js = string.format("document.querySelector('#mainsrp-pager div.form > input').value=%d;document.querySelector('#mainsrp-pager div.form > span.btn.J_Submit').click()", args.page)
  splash:evaljs(js)
  assert(splash:wait(args.wait))
  return splash:png()
end複製程式碼

我們定義了三個引數:請求的連結url、等待時間wait、分頁頁碼page。然後禁用圖片載入,請求淘寶的商品列表頁面,通過evaljs()方法呼叫JavaScript程式碼,實現頁碼填充和翻頁點選,最後返回頁面截圖。我們將指令碼放到Splash中執行,正常獲取到頁面截圖,如下圖所示。

Scrapy框架的使用之Scrapy對接Splash

翻頁操作也成功實現,如下圖所示即為當前頁碼,和我們傳入的頁碼page引數是相同的。

Scrapy框架的使用之Scrapy對接Splash

我們只需要在Spider裡用SplashRequest對接Lua指令碼就好了,如下所示:

from scrapy import Spider
from urllib.parse import quote
from scrapysplashtest.items import ProductItem
from scrapy_splash import SplashRequest

script = """
function main(splash, args)
  splash.images_enabled = false
  assert(splash:go(args.url))
  assert(splash:wait(args.wait))
  js = string.format("document.querySelector('#mainsrp-pager div.form > input').value=%d;document.querySelector('#mainsrp-pager div.form > span.btn.J_Submit').click()", args.page)
  splash:evaljs(js)
  assert(splash:wait(args.wait))
  return splash:html()
end
"""

class TaobaoSpider(Spider):
    name = 'taobao'
    allowed_domains = ['www.taobao.com']
    base_url = 'https://s.taobao.com/search?q='

    def start_requests(self):
        for keyword in self.settings.get('KEYWORDS'):
            for page in range(1, self.settings.get('MAX_PAGE') + 1):
                url = self.base_url + quote(keyword)
                yield SplashRequest(url, callback=self.parse, endpoint='execute', args={'lua_source': script, 'page': page, 'wait': 7})複製程式碼

我們把Lua指令碼定義成長字串,通過SplashRequestargs來傳遞引數,介面修改為execute。另外,args引數裡還有一個lua_source欄位用於指定Lua指令碼內容。這樣我們就成功構造了一個SplashRequest,對接Splash的工作就完成了。

其他的配置不需要更改,Item、Item Pipeline等設定與上節對接Selenium的方式相同,parse()回撥函式也是完全一致的。

五、執行

接下來,我們通過如下命令執行爬蟲:

scrapy crawl taobao複製程式碼

執行結果如下圖所示。

Scrapy框架的使用之Scrapy對接Splash

由於Splash和Scrapy都支援非同步處理,我們可以看到同時會有多個抓取成功的結果。在Selenium的對接過程中,每個頁面渲染下載是在Downloader Middleware裡完成的,所以整個過程是阻塞式的。Scrapy會等待這個過程完成後再繼續處理和排程其他請求,這影響了爬取效率。因此使用Splash的爬取效率比Selenium高很多。

最後我們再看看MongoDB的結果,如下圖所示。

Scrapy框架的使用之Scrapy對接Splash

結果同樣正常儲存到MongoDB中。

六、本節程式碼

本節程式碼地址為:https://github.com/Python3WebSpider/ScrapySplashTest。

七、結語

因此,在Scrapy中,建議使用Splash處理JavaScript動態渲染的頁面。這樣不會破壞Scrapy中的非同步處理過程,會大大提高爬取效率。而且Splash的安裝和配置比較簡單,通過API呼叫的方式實現了模組分離,大規模爬取的部署也更加方便。



相關文章