これは,クローラー/Webスクレイピング Advent Calendar 2016の1日目の記事です.
JavaScriptを利用したページをスクレイピングするためには,スクリプトを実行し,ページを適切にレンダリングする必要があります.
本記事では,そのようなケースに便利なPythonライブラリscrapy-splash
を紹介します.
前置き
ScrapyやSplashを既にご存知の方は読み飛ばして下さい.
Scrapyとは?
Scrapyとは,Python製のクローリング・スクレイピングフレームワークです.フレームワークというだけあって,Scrapyにはクローリング・スクレイピングに便利なオプションがあらかじめ用意されています.
- Scrapyに用意されている便利なオプション例
- サイトクローリング間隔を設定
- robots.txtを解釈したクローリングを自動的に実行可能
Scrapyを利用することで,クローリングやスクレイピングの初心者でもあっても相手サイトに迷惑をかけることなく,安全にクローラー・スクレイパーを作成することができます. より詳細な使い方等については,PyCon JP 2016で紹介した資料がありますのでそちらをご参照下さい.
本記事では,pip
を利用してScrapy環境を用意しました.
$ pip install scrapy
Splashとは?
Splashとは,Scrapinghub社が開発しているオープンソースのJavaScriptレンダリングサービスです.
JavaScriptを実行してスクレイピングをする王道といえばPhantomJSやSeleniumを利用した方法があげられますが,Splashはこれらのツールとは違い,HTTP APIを経由してJavaScriptの実行や実行結果受取といったやりとりを行います.
- Splashの特徴
より詳細な説明が欲しい方は公式ドキュメントをご参照下さい. 本記事では,Splashのdockerイメージを利用してSplash環境を用意しました.
$ docker pull scrapinghub/splash $ docker run -p 8050:8050 scrapinghub/splash
Scrapy + SplashでJavaScript利用ページをスクレイピング
前置きが長くなってしまいましたが,実際にScrapyとSplashを利用してJavaScriptを利用したページをスクレイピングする手順について紹介します.例として,以下の記事で紹介されている「テレ朝の特定ページから関連ニュースをスクレイピングする処理」を行います.
以下のページをスクレイピングします.
スクレイピング箇所を確認するために,ページのHTMLソースを表示してみましょう.関連ニュースを表示している処理は125行目付近にあります.
<!-- 関連ニュース --> <div id="relatedNews"></div>
これを見ると,単純にHTMLソースを取得しただけでは欲しい情報を得ることが出来ないことが分かります. Webページに表示されている情報は,JavaScriptを実行して関連ニュースをレンダリングした結果であるため,スクレイピングして欲しい情報を得るためには,JavaScriptを実行する処理を間にはさむ必要があります.
具体的な処理の流れ
Scrapy + Splashを利用した具体的な処理の流れは以下の通りです.
※ScrapyとSplashが入った環境を事前にご用意下さい
※Scrapyの基本的な使い方については紹介しませんのでご了承下さい
実行環境は以下の通りです.
作成したデモプロジェクトはGithubに置いてあります.
1. scrapy-splash
の導入
ScrapyとSplashの連携には,scrapy-splash
というライブラリを利用します.
scrapy-splash
はpip
で簡単に導入出来ます.
$ pip install scrapy-splash # ver. 0.7
2. Scrapyプロジェクトの作成&各種ミドルウェアの有効化
asahi
という名前のScrapyプロジェクトを作成し,プロジェクトでscrapy-splash
が利用できるように各種設定を追記していきます.
# asahiプロジェクトを作成 $ scrapy startproject asahi $ cd asahi $ tree . . ├── asahi │ ├── __init__.py │ ├── __pycache__ │ ├── items.py │ ├── pipelines.py │ ├── settings.py │ └── spiders │ ├── __init__.py │ └── __pycache__ └── scrapy.cfg
scrapy-splash
を利用するために,以下の5つの項目を設定します.
- SPLASH_URL
- DOWNLOADER_MIDDLEWARES
- SPIDER_MIDDLEWARES
- DUPEFILTER_CLASS
- HTTPCACHE_STORAGE
それぞれ説明していきます.
まず最初にsettings.py
を開き,SPLACH_URL
を追記します.この設定を行うことで,Scrapy側にSplashサービスの場所を教えています.
SPLASH_URL = 'http://[自分の環境のURL]:8050/'
次にsettings.py
のDOWNLOADER_MIDDLEWARES
の項目に設定を書き加えます.(デフォルトではコメントアウトされています)
DOWNLOADER_MIDDLEWARES = { 'scrapy_splash.SplashCookiesMiddleware': 723, 'scrapy_splash.SplashMiddleware': 725, 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810, }
ここではWebページのダウンローダーの設定をしています.scrapy_splash
のミドルウェアをHttpProxyMiddleware
(デフォルト値: 750)よりも優先させる必要があるため,scrapy_splash
ミドルウェアの設定値は必ず750よりも小さい値を指定して下さい.(上記をそのまま利用して頂いて問題ありません)
同様にSPIDER_MIDDLEWARES
の項目に設定を書き加えます.(デフォルトではコメントアウトされています)
SPIDER_MIDDLEWARES = { 'scrapy_splash.SplashDeduplicateArgsMiddleware': 100, }
この項目を指定することで,Splashのcache_args
機能を有効化できます.この設定によりリクエスト毎に変更されない引数 (e.g. lua_source
) についてはSplash側に1度しか送信されなくなるため,リクエストキューのメモリ使用量が減り,リクエストに利用するネットワークトラフィックも削減出来ます.
最後にDUPEFILTER_CLASS
とHTTPCACHE_STORAGE
を追記します.
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter' HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
現状のScrapyにはリクエストのFingerprint計算をグローバルにオーバーライドする方法が提供されていないため,上記2つのオプションで対応する形になっています.(リクエストのFingerprintを確認することで,同一リソースへの多重アクセスを抑止出来ます)
※こちらは将来的には変更されるようです
3. スクレイピング項目の設定
items.py
を編集してスクレイピング項目(=関連ニュース)を設定します.
import scrapy class AsahiItem(scrapy.Item): related_news = scrapy.Field() # 関連ニュース
4. スクレイピング処理の記述
scrapy genspider
コマンドを利用してクローラーの雛形を作成します.
scrapy genspider news news.tv-asahi.co.jp
作成した雛形はspiders/news.py
に保存されているため,こちらを編集していきます.
Splashを経由してJavaScriptを解釈したページを取得するために,NewsSpider
クラスにstart_requests
メソッドを作成し,WebページへのリクエストをSplashを経由したもの(SplashRequest
クラスを使ったもの)に置き換えます.
※wait
パラメータでページレンダリングまでの待ち時間を設定出来ます
※args
引数にhttp_method
パラメータやbody
パラメータを設定することで,POSTリクエストにも対応可能です
def start_requests(self): yield SplashRequest(self.start_urls[0], self.parse, args={'wait': 0.5}, )
SplashRequest
で受け取ったresponse
はJavaScriptを解釈済みのHTMLなため,parse
メソッドの記述は通常のHTMLページをパースする場合と同様に書けます.
最終的なnews.py
の中身は以下のようになりました.
from ..items import AsahiItem from scrapy_splash import SplashRequest import scrapy class NewsSpider(scrapy.Spider): name = "news" allowed_domains = ["news.tv-asahi.co.jp"] start_urls = ['http://news.tv-asahi.co.jp/news_society/articles/000089004.html'] def start_requests(self): yield SplashRequest(self.start_urls[0], self.parse, args={'wait': 0.5}, ) def parse(self, response): for res in response.xpath("//div[@id='relatedNews']/div[@class='kanrennews']/ul/li"): item = AsahiItem() item['related_news'] = res.xpath(".//a/text()").extract()[0].strip() yield item
5. スクレイピングの実行
実際にスクレイピング処理を実行してみましょう.scrapy crawl
コマンドを利用してスクレイピングを実行し,結果をresult.csv
に保存します.
$ scrapy crawl news -o result.csv
実行結果は以下の通りになりました.
related_news 豊洲市場“盛り土問題” 歴代市場長ら18人処分へ 豊洲移転は早ければ“来年冬” 小池知事が表明 豊洲市場への移転 安全性確保されれば早くて来年冬 負担増す市場業者から切実な声 豊洲市場移転問題 豊洲市場 地下の大気から指針値超える水銀検出
無事に欲しかった結果を受け取れました!
おまけ
JavaScriptを利用したページについても,scrapy shell
コマンドを利用してインタラクティブにresponse
の結果を確認することができます.(URLの頭にhttp://localhost:8050/render.html?url=
をつけて,Splash経由でレスポンスを受け取っています)
scrapy shell 'http://localhost:8050/render.html?url=http://news.tv-asahi.co.jp/news_society/articles/000089004.html'