アドベントカレンダーをリアルタイムで追っておらず、今更興味あるテーマを見ようと思いました。
adventarに登録されているカレンダー数が多かったので、記事投稿数が20以上のタイトルから探すべく、いざスクレイピング。
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 ドキュメント
インターネットからページをダウンロードする際に、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を一発で見ることができます。
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
無事スクレイピングできていることを確認できました!