amacbee's blog

Pythonの話とかデータの話とか酒の話とか

scrapy-splashを使ってJavaScript利用ページを簡単スクレイピング

これは,クローラー/Webスクレイピング Advent Calendar 2016の1日目の記事です.

JavaScriptを利用したページをスクレイピングするためには,スクリプトを実行し,ページを適切にレンダリングする必要があります. 本記事では,そのようなケースに便利なPythonライブラリscrapy-splashを紹介します.

前置き

ScrapyやSplashを既にご存知の方は読み飛ばして下さい.

Scrapyとは?

Scrapyとは,Python製のクローリング・スクレイピングフレームワークです.フレームワークというだけあって,Scrapyにはクローリング・スクレイピングに便利なオプションがあらかじめ用意されています.

  • Scrapyに用意されている便利なオプション例
    • サイトクローリング間隔を設定
    • robots.txtを解釈したクローリングを自動的に実行可能

Scrapyを利用することで,クローリングやスクレイピングの初心者でもあっても相手サイトに迷惑をかけることなく,安全にクローラースクレイパーを作成することができます. より詳細な使い方等については,PyCon JP 2016で紹介した資料がありますのでそちらをご参照下さい.

speakerdeck.com

本記事では,pipを利用してScrapy環境を用意しました.

$ pip install scrapy

Splashとは?

Splashとは,Scrapinghub社が開発しているオープンソースJavaScriptレンダリングサービスです.

github.com

JavaScriptを実行してスクレイピングをする王道といえばPhantomJSやSeleniumを利用した方法があげられますが,Splashはこれらのツールとは違い,HTTP APIを経由してJavaScriptの実行や実行結果受取といったやりとりを行います.

  • Splashの特徴
    • スタート/ストップ時のオーバーヘッドがない
    • 複数のWebページを並列に処理可能
      • 同一Webサイトの複数のページをレンダリングする場合はキャッシュを共有出来る
    • Jupyter用のSplash Kernelをサポートしており,Jupyter Notebookを活用して便利なデバッグが可能

より詳細な説明が欲しい方は公式ドキュメントをご参照下さい. 本記事では,Splashのdockerイメージを利用してSplash環境を用意しました.

$ docker pull scrapinghub/splash
$ docker run -p 8050:8050 scrapinghub/splash

Scrapy + SplashでJavaScript利用ページをスクレイピング

前置きが長くなってしまいましたが,実際にScrapyとSplashを利用してJavaScriptを利用したページをスクレイピングする手順について紹介します.例として,以下の記事で紹介されている「テレ朝の特定ページから関連ニュースをスクレイピングする処理」を行います.

以下のページをスクレイピングします.

news.tv-asahi.co.jp

スクレイピング箇所を確認するために,ページのHTMLソースを表示してみましょう.関連ニュースを表示している処理は125行目付近にあります.

<!-- 関連ニュース -->
            <div id="relatedNews"></div>

これを見ると,単純にHTMLソースを取得しただけでは欲しい情報を得ることが出来ないことが分かります. Webページに表示されている情報は,JavaScriptを実行して関連ニュースをレンダリングした結果であるため,スクレイピングして欲しい情報を得るためには,JavaScriptを実行する処理を間にはさむ必要があります.

具体的な処理の流れ

Scrapy + Splashを利用した具体的な処理の流れは以下の通りです.
※ScrapyとSplashが入った環境を事前にご用意下さい
※Scrapyの基本的な使い方については紹介しませんのでご了承下さい

  1. scrapy-splashの導入
  2. Scrapyプロジェクトの作成&各種ミドルウェアの有効化
  3. スクレイピング項目の設定
  4. スクレイピング処理の記述
  5. スクレイピングの実行

実行環境は以下の通りです.

  • OS X EI Caption (10.11.6)
  • Python 3.5.2
    • Scrapy 1.2.1
  • Splash 2.3

作成したデモプロジェクトはGithubに置いてあります.

github.com

1. scrapy-splashの導入

ScrapyとSplashの連携には,scrapy-splashというライブラリを利用します.

github.com

scrapy-splashpipで簡単に導入出来ます.

$ 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.pyDOWNLOADER_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_CLASSHTTPCACHE_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で受け取ったresponseJavaScriptを解釈済みの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'

参考