PythonでJavaScriptを使ったWebサイトをスクレイピングする

# 概要

JavaScriptでDOMを作ってるサイトをPythonを使ってスクレイピングしたので、手順をメモ。

大雑把には、[Scrapy](http://scrapy.org/)と[Selenium](http://www.seleniumhq.org/)を組み合わせてやった。


## Scrapy

[Scrapy](http://scrapy.org/)は、クローラーを実装するためのフレームワーク。

クローラーを*Spider*のサブクラス、スクレイピングした情報を*Item*のサブクラス、スクレイピングした情報に対する処理を*Pipeline*のサブクラス、という風にフレームワークが決めたインターフェースを満たすクラスとしてクローラーを実装する。

*scrapy*というコマンドが提供されてて、このコマンドを使って、作ったクローラーの一覧を見たり、クローラーを起動したりできる。


## Selenium

[Selenium](http://www.seleniumhq.org/)は、ブラウザをプログラムから制御するためのツール（でいいのかな？）。Pythonも含めたいろんな言語で使える。
よくWebサイト/アプリの自動テスト文脈でよく出てくる。
これを使えば、JavaScript実行して、動的に生成されたDOMも含めたHTMLのソースをスクレイピングできる。

元はJavaScript経由でブラウザを制御してたみたいだけど、今はブラウザに直接メッセージを送ってブラウザを制御してくれる。

ぼくの環境（OSX）で試したら、Safari、Chromeだと拡張機能みたいなのをインストールしないと使えなかった。
Firefoxだとそのまま使える。
[PhantomJS](http://phantomjs.org/)を入れれば、ウィンドウ無しでスクレイピングもできるので、サーバー上で使っても大丈夫。


## なんでScrapyを使うのか

Scrapyを使わなくても、Seleniumだけでもスクレイピングはできるんだけど、

- 並行処理で複数ページをスクレイピングしてくれる（マルチスレッド？プロセス？）、
- 何ページクロールしたかとか、エラーは何回起こったかとか、ログをいい感じにまとめてくれる、
- クロール対象ページの重複を回避してくれる、
- クロールの間隔とか、いろんな設定オプションを提供してくれる、
- CSS、XPathを組み合わせて、DOMから情報を抜ける（組み合わせが結構便利！）、
- クロール結果を、JSONとかXMLで吐き出せる、
- いい感じのプログラム設計でクローラーを書ける（下手に自分で設計するより良いと思う）、

など、思いつくScrapyを使うメリットはこんな感じ。

最初は、フレームワークの勉強が面倒くさくて、Seleniumと[PyQuery](http://pyquery.readthedocs.org/en/latest/)だけでクローラーを実装してたけど、ログとかエラー処理とか書いてたら、車輪の再発明感が強くなってきてやめました。

実装前に[Scrapyのドキュメント](http://doc.scrapy.org/en/1.0/)を、最初からSettingsのページまでと、*Architecture overview*、*Downloader Middleware*あたりを読みました。


# ScrapyとSeleniumを組み合わせて使う

元ネタは[このstack overflow](http://stackoverflow.com/questions/31174330/passing-selenium-response-url-to-scrapy/31186730#31186730)。

Scrapyのアーキテクチャーはこんな感じ（[Scrapyのドキュメント](http://doc.scrapy.org/en/1.0/topics/architecture.html)より）。

![Scrapy architecture overview](http://doc.scrapy.org/en/1.0/_images/scrapy_architecture.png)

*Downloader Middlewares*を、カスタマイズして、ScrapyのSpiderがSeleniumを使ってスクレイピングするように調整する。

## Donwloader Middlewareの実装

Downloader Middlewareは、*process_request*を実装した普通のクラスとして実装する。

```py:selenium_middleware.py
# -*- coding: utf-8 -*-
import os.path

from urlparse import urlparse

import arrow

from scrapy.http import HtmlResponse
from selenium.webdriver import Firefox


driver = Firefox()


class SeleniumMiddleware(object):
    
    def process_request(self, request, spider):
        
        driver.get(request.url)

        return HtmlResponse(driver.current_url,
            body = driver.page_source,
            encoding = 'utf-8',
            request = request)


def close_driver():
    driver.close()
```

このDownload Middlewareを登録すると、*Spider*が、ページをスクレイピングする前に、*process_request*を呼び出してくれる。
詳しくは、[こちら](http://doc.scrapy.org/en/1.0/topics/downloader-middleware.html)。

*HtmlResponse*のインスタンスを返してるんで、それ以降のDownload Middlewareは呼び出されない。
優先順位の設定によっては、[デフォルトのDonwload Middleware](http://doc.scrapy.org/en/1.0/topics/settings.html#downloader-middlewares-base)（robots.txtを解析処理など）は呼び出されないので注意。

上では、Firefoxを使ってるけど、PhantomJSを使いたい時は、*driver*変数のとこを書き換える。


## Download Middlewareの登録

使いたい*Spider*クラスに、*SeleniumMiddleware*を登録する。

```py:some_spider.py
# -*- coding: utf-8 -*-
import scrapy

from ..selenium_middleware import close_driver


class SomeSpider(scrapy.Spider):
    name = "some_spider"
    allowed_domains = ["somedomain"]
    start_urls = (
        'http://somedomain/',
    )
    custom_settings = {
        "DOWNLOADER_MIDDLEWARES": {
            "some_crawler.selenium_middleware.SeleniumMiddleware": 0,
        },
        "DOWNLOAD_DELAY": 0.5,
    }
    

    def parse(self, response):
        # クローラーの処理

    def closed(self, reason):
        close_driver()
```

*custom_settings*の*DOWNLOADER_MIDDLEWARES*のとこが設定。
スクレイピングに使ったFirefoxのウィンドウを消すために、*close*メソッドの処理を書いてる。