夕蛙のなく頃に

データアナリストとして学んだことや趣味で勉強し始めたIoTをアウトプットする

Selenium + Scrapy でJavaScriptを使ったサイトをスクレイピングする

アドベントカレンダーをリアルタイムで追っておらず、今更興味あるテーマを見ようと思いました。

adventarに登録されているカレンダー数が多かったので、記事投稿数が20以上のタイトルから探すべく、いざスクレイピング

adventar.org

JavaScriptが使われていたので、初めてSelenium + Scrapy の組み合わせを使ってみました。

目次

実行環境

$ pyenv version
anaconda3-5.1.0

$ python --version
Python 3.6.4 :: Anaconda, Inc.

$ pip list
chromedriver-binary                79.0.3945.36.0
Scrapy                             1.6.0
selenium                           3.141.0

環境構築

SeleniumとScrapyをインストールします。

$ pip install scrapy
$ pip install selenium

今回はSeleniumにてChromeを自動操作するので、ChromeDriverもインストールします。

$ pip install chromedriver-binary

ChromeのバージョンとChromeDriverのバーションを合わせる

PCで使っているChromeのバージョンとChromeDriverのバージョンを合わせないと、Selenium使用時に以下のようなエラーが出ると思います。

Traceback (most recent call last):
  File "/Users/frogdusk/.pyenv/versions/anaconda3-5.1.0/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 76, in start
    stdin=PIPE)
  File "/Users/frogdusk/.pyenv/versions/anaconda3-5.1.0/lib/python3.6/subprocess.py", line 709, in __init__
    restore_signals, start_new_session)
  File "/Users/frogdusk/.pyenv/versions/anaconda3-5.1.0/lib/python3.6/subprocess.py", line 1344, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'chromedriver': 'chromedriver'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/frogdusk/develop/workspace/crawler/adventar/selenium_middleware.py", line 6, in <module>
    driver = Chrome()
  File "/Users/frogdusk/.pyenv/versions/anaconda3-5.1.0/lib/python3.6/site-packages/selenium/webdriver/chrome/webdriver.py", line 73, in __init__
    self.service.start()
  File "/Users/frogdusk/.pyenv/versions/anaconda3-5.1.0/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 83, in start
    os.path.basename(self.path), self.start_error_message)
selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

そこでPCで使っているChromeのバージョンを確認し、79の場合はそれに対応するものを入れます。

$ pip install 'chromedriver-binary>=79,<80'

Downloader Middleware実装

scrapyのアーキテクチャ アーキテクチャの概要 — Scrapy 1.2.2 ドキュメント

f:id:frogdusk:20200108141449p:plain:w500

インターネットからページをダウンロードする際に、Downloader Middlewaresを経由して、Downloaderにリクエストを送信します。

ここでSeleniumを挟みたいので、そのようなDownloader Middlewareを実装します。

重要なのは、 必要な要素が揃うまで待機 する部分です。 今回はclass='item'という要素が出てくるまで待機する仕様にしています。

$ cat selenium_middleware.py
# -*- coding: utf-8 -*-
from scrapy.http import HtmlResponse
from selenium.webdriver import Chrome
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By


# driver_location = 'chromedriverが入っているパスを指定'
driver_location = '/Users/frogdusk/.pyenv/versions/anaconda3-5.1.0/lib/python3.6/site-packages/chromedriver_binary/chromedriver'
driver = Chrome(driver_location)


class SeleniumMiddleware(object):
    def process_request(self, request, spider):
        driver.get(request.url)
                # 必要な要素が揃うまで15秒を上限として待機
        WebDriverWait(driver, 15).until(EC.presence_of_all_elements_located((By.CLASS_NAME, 'item')))
        return HtmlResponse(driver.current_url,
                            body=driver.page_source,
                            encoding='utf-8',
                            request=request)


def close_driver():
    driver.close()

chromeでHTML要素を確認する

今回はitemと指定しましたが、それを確認する方法です。

確認したい要素を右クリックして、 検証 をクリックします。
Chromeデベロッパーツールが起動し、HTMLのelementsを一発で見ることができます。

f:id:frogdusk:20200108130047p:plain:w500 f:id:frogdusk:20200108130105p:plain:w500

Spider実装

custom_settingで、Downloader Middlewareに作成したmiddlewareを指定しています。

adsでカレンダー一覧を取得し、それぞれのカレンダーについて、タイトル・URL・記事投稿数を出力するようにしました。

$ cat adventar_spider.py
# -*- coding: utf-8 -*-
import scrapy
from selenium_middleware import close_driver


class AdventarSpider(scrapy.Spider):
    name = 'adventar_spider'
    allowed_domain = 'adventar.org'
    base_url = 'https://adventar.org'
    start_urls = [
        'https://adventar.org/calendars?year=2019'
        ]
    custom_settings = {
        "DOWNLOADER_MIDDLEWARES": {
            "selenium_middleware.SeleniumMiddleware": 0
        },
        "DOWNLOAD_DELAY": 1
    }

    def parse(self, response):
        ads = response.xpath('//*[@id="__layout"]/div/div/main/div/div/ul/li')
        for ad in ads:
            title = ad.xpath('a/text()').extract_first()
            url = ad.xpath('a/@href').extract_first()
            value = ad.xpath('div/span[@class="indicator"]/span/@data-value').extract_first()
            yield {
                'title': title,
                'url': self.base_url + url,
                'value': value
            }

    def closed(self, reason):
        close_driver()

結果確認

$ scrapy runspider adventar_spider.py -o output.csv
...

$ head output.csv
title,url,value
テスト,https://adventar.org/calendars/4928,0
何度も同じ話するオタクのための,https://adventar.org/calendars/4927,1
Cerevo,https://adventar.org/calendars/4926,25
色卵,https://adventar.org/calendars/4919,1
Tokyo Scenery,https://adventar.org/calendars/4918,2
ギルドハウスをモデルに浦佐でシェアハウス,https://adventar.org/calendars/4917,0
Loco Partners20新卒,https://adventar.org/calendars/4916,19
Thank you my idols,https://adventar.org/calendars/4915,6
セミナーカレンダー,https://adventar.org/calendars/4914,0

無事スクレイピングできていることを確認できました!

参考記事