1. 程式人生 > >Scrapy筆記(12)- 抓取動態網站

Scrapy筆記(12)- 抓取動態網站

前面我們介紹的都是去抓取靜態的網站頁面,也就是說我們開啟某個連結,它的內容全部呈現出來。但是如今的網際網路大部分的web頁面都是動態的,經常逛的網站例如京東、淘寶等,商品列表都是js,並有Ajax渲染,下載某個連結得到的頁面裡面含有非同步載入的內容,這樣再使用之前的方式我們根本獲取不到非同步載入的這些網頁內容。

使用Javascript渲染和處理網頁是種非常常見的做法,如何處理一個大量使用Javascript的頁面是Scrapy爬蟲開發中一個常見的問題,這篇文章將說明如何在Scrapy爬蟲中使用scrapy-splash來處理頁面中得Javascript。

scrapy-splash簡介

scrapy-splash利用

Splash將javascript和Scrapy整合起來,使得Scrapy可以抓取動態網頁。

Splash是一個javascript渲染服務,是實現了HTTP API的輕量級瀏覽器,底層基於Twisted和QT框架,Python語言編寫。所以首先你得安裝Splash例項

安裝docker

官網建議使用docker容器安裝方式Splash。那麼首先你得先安裝docker

參考官方安裝文件,這裡我選擇Ubuntu 12.04 LTS版本安裝

升級核心版本,docker需要3.13核心

$ sudo apt-get update
$ sudo apt-get install linux-image-generic-lts-trusty
$ sudo reboot

安裝CA認證

$ sudo apt-get install apt-transport-https ca-certificates

增加新的GPGkey

$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

開啟/etc/apt/sources.list.d/docker.list,如果沒有就建立一個,然後刪除任何已存在的內容,再增加下面一句

deb https://apt.dockerproject.org/repo ubuntu-precise main

更新APT

$ sudo apt-get update
$ sudo apt-get purge lxc-docker
$ apt-cache policy docker-engine

安裝

docker engine

$ sudo apt-get install docker-engine

啟動docker服務

$ sudo service docker start

驗證是否啟動成功

$ sudo docker run hello-world

上面這條命令會下載一個測試映象並在容器中執行它,它會列印一個訊息,然後退出。

安裝Splash

拉取映象下來

$ sudo docker pull scrapinghub/splash

啟動容器

$ sudo docker run -p 5023:5023 -p 8050:8050 -p 8051:8051 scrapinghub/splash

現在可以通過0.0.0.0:8050(http),8051(https),5023 (telnet)來訪問Splash了。

安裝scrapy-splash

使用pip安裝

$ pip install scrapy-splash

配置scrapy-splash

在你的scrapy工程的配置檔案settings.py中新增

SPLASH_URL = 'http://192.168.203.92:8050'

新增Splash中介軟體,還是在settings.py中通過DOWNLOADER_MIDDLEWARES指定,並且修改HttpCompressionMiddleware的優先順序

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

預設情況下,HttpProxyMiddleware的優先順序是750,要把它放在Splash中介軟體後面

設定Splash自己的去重過濾器

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'

如果你使用Splash的Http快取,那麼還要指定一個自定義的快取後臺儲存介質,scrapy-splash提供了一個scrapy.contrib.httpcache.FilesystemCacheStorage的子類

HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'

如果你要使用其他的快取儲存,那麼需要繼承這個類並且將所有的scrapy.util.request.request_fingerprint呼叫替換成scrapy_splash.splash_request_fingerprint

使用scrapy-splash

SplashRequest

最簡單的渲染請求的方式是使用scrapy_splash.SplashRequest,通常你應該選擇使用這個

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
    slot_policy=scrapy_splash.SlotPolicy.PER_DOMAIN,  # optional
)

另外,你還可以在普通的scrapy請求中傳遞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
    }
})

Splash API說明,使用SplashRequest是一個非常便利的工具來填充request.meta['splash']裡的資料

  • meta[‘splash’][‘args’] 包含了發往Splash的引數。
  • meta[‘splash’][‘endpoint’] 指定了Splash所使用的endpoint,預設是render.html
  • meta[‘splash’][‘splash_url’] 覆蓋了settings.py檔案中配置的Splash URL
  • meta[‘splash’][‘splash_headers’] 執行你增加或修改發往Splash伺服器的HTTP頭部資訊,注意這個不是修改發往遠端web站點的HTTP頭部
  • meta[‘splash’][‘dont_send_headers’] 如果你不想傳遞headers給Splash,將它設定成True
  • meta[‘splash’][‘slot_policy’] 讓你自定義Splash請求的同步設定
  • meta[‘splash’][‘dont_process_response’] 當你設定成True後,SplashMiddleware不會修改預設的scrapy.Response請求。預設是會返回SplashResponse子類響應比如SplashTextResponse
  • meta[‘splash’][‘magic_response’] 預設為True,Splash會自動設定Response的一些屬性,比如response.headers,response.body

如果你想通過Splash來提交Form請求,可以使用scrapy_splash.SplashFormRequest,它跟SplashRequest使用是一樣的。

Responses

對於不同的Splash請求,scrapy-splash返回不同的Response子類

  • SplashResponse 二進位制響應,比如對/render.png的響應
  • SplashTextResponse 文字響應,比如對/render.html的響應
  • SplashJsonResponse JSON響應,比如對/render.json或使用Lua指令碼的/execute的響應

如果你只想使用標準的Response物件,就設定meta['splash']['dont_process_response']=True

所有這些Response會把response.url設定成原始請求URL(也就是你要渲染的頁面URL),而不是Splash endpoint的URL地址。實際地址通過response.real_url得到

Session的處理

Splash本身是無狀態的,那麼為了支援scrapy-splash的session必須編寫Lua指令碼,使用/execute

function main(splash)
    splash:init_cookies(splash.args.cookies)

    -- ... your script

    return {
        cookies = splash:get_cookies(),
        -- ... other results, e.g. html
    }
end

而標準的scrapy session引數可以使用SplashRequest將cookie新增到當前Splash cookiejar中

使用例項

接下來我通過一個實際的例子來演示怎樣使用,我選擇爬取京東網首頁的非同步載入內容。

京東網開啟首頁的時候只會將導航選單加載出來,其他具體首頁內容都是非同步載入的,下面有個”猜你喜歡”這個內容也是非同步載入的,我現在就通過爬取這個”猜你喜歡”這四個字來說明下普通的Scrapy爬取和通過使用了Splash載入非同步內容的區別。

首先我們寫個簡單的測試Spider,不使用splash:

class TestSpider(scrapy.Spider):
    name = "test"
    allowed_domains = ["jd.com"]
    start_urls = [
        "http://www.jd.com/"
    ]

    def parse(self, response):
        logging.info(u'---------我這個是簡單的直接獲取京東網首頁測試---------')
        guessyou = response.xpath('//div[@id="guessyou"]/div[1]/h2/text()').extract_first()
        logging.info(u"find:%s" % guessyou)
        logging.info(u'---------------success----------------')

然後執行結果:

2016-04-18 14:42:44 test_spider.py[line:20] INFO ---------我這個是簡單的直接獲取京東網首頁測試---------
2016-04-18 14:42:44 test_spider.py[line:22] INFO find:None
2016-04-18 14:42:44 test_spider.py[line:23] INFO ---------------success----------------

我找不到那個”猜你喜歡”這四個字

接下來我使用splash來爬取

import scrapy
from scrapy_splash import SplashRequest


class JsSpider(scrapy.Spider):
    name = "jd"
    allowed_domains = ["jd.com"]
    start_urls = [
        "http://www.jd.com/"
    ]

    def start_requests(self):
        splash_args = {
            'wait': 0.5,
        }
        for url in self.start_urls:
            yield SplashRequest(url, self.parse_result, endpoint='render.html',
                                args=splash_args)

    def parse_result(self, response):
        logging.info(u'----------使用splash爬取京東網首頁非同步載入內容-----------')
        guessyou = response.xpath('//div[@id="guessyou"]/div[1]/h2/text()').extract_first()
        logging.info(u"find:%s" % guessyou)
        logging.info(u'---------------success----------------')

執行結果:

2016-04-18 14:42:51 js_spider.py[line:36] INFO ----------使用splash爬取京東網首頁非同步載入內容-----------
2016-04-18 14:42:51 js_spider.py[line:38] INFO find:猜你喜歡
2016-04-18 14:42:51 js_spider.py[line:39] INFO ---------------success----------------

可以看出結果裡面已經找到了這個”猜你喜歡”,說明非同步載入內容爬取成功!