在上一節我們實現了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中執行,正常獲取到頁面截圖,如下圖所示。
翻頁操作也成功實現,如下圖所示即為當前頁碼,和我們傳入的頁碼page
引數是相同的。
我們只需要在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指令碼定義成長字串,通過SplashRequest
的args
來傳遞引數,介面修改為execute
。另外,args
引數裡還有一個lua_source
欄位用於指定Lua指令碼內容。這樣我們就成功構造了一個SplashRequest
,對接Splash的工作就完成了。
其他的配置不需要更改,Item、Item Pipeline等設定與上節對接Selenium的方式相同,parse()
回撥函式也是完全一致的。
五、執行
接下來,我們通過如下命令執行爬蟲:
scrapy crawl taobao複製程式碼
執行結果如下圖所示。
由於Splash和Scrapy都支援非同步處理,我們可以看到同時會有多個抓取成功的結果。在Selenium的對接過程中,每個頁面渲染下載是在Downloader Middleware裡完成的,所以整個過程是阻塞式的。Scrapy會等待這個過程完成後再繼續處理和排程其他請求,這影響了爬取效率。因此使用Splash的爬取效率比Selenium高很多。
最後我們再看看MongoDB的結果,如下圖所示。
結果同樣正常儲存到MongoDB中。
六、本節程式碼
本節程式碼地址為:https://github.com/Python3WebSpider/ScrapySplashTest。
七、結語
因此,在Scrapy中,建議使用Splash處理JavaScript動態渲染的頁面。這樣不會破壞Scrapy中的非同步處理過程,會大大提高爬取效率。而且Splash的安裝和配置比較簡單,通過API呼叫的方式實現了模組分離,大規模爬取的部署也更加方便。