PuppeteerでWebスクレイピングが捗る ―株マップ.comの銘柄スクリーニング結果を取得する―

趣味の株式投資で投資対象の選定にクォンツ・リサーチ社が運営する株マップ.comの銘柄スクリーニングを便利に使わせて貰っているけれど、これまで

  1. ブラウザのブックマークからスクリーニング結果にアクセス
  2. 表の文字列を手元のテキストエディタにコピペ
  3. 正規表現で扱いやすいデータに置換
  4. 手元に保存しているデータとの差分をチェックし、新たに注目すべき銘柄がないか確認
  5. 上記のフローを複数のスクリーニング条件について確認

という作業に数分かかっていて、Webスクレイピングでの自動化を試みたいと思った。

GASでできそうだったが…

まずGoogle Apps Script(GAS)のUrlFetchApp.fetchでサクッとスクレイピングできそうだったので試してみたけれど、株マップ.comのスクリーニングの表はHTMLのレスポンスに含まれておらずJSで描画されているので、UrlFetchApp.fetchの機能では取得できなかった。

ちなみに今回は試さなかったけれど、PhantomJs Cloudを使えばGASでもできるらしい。

Puppeteerという選択肢

JSの描画に対応できる選択肢としてPythonまたはRubySeleniumを使えばできそうだと分かったけれど、Seleniumは昔触って割と面倒だった印象があり、若干躊躇していると

Python Webスクレイピング テクニック集「取得できない値は無い」JavaScript対応@追記あり6/12 - Qiita

取得だけなら簡単だけどidがとことん無かったりnth-childだと壊れやすいんだよなあ。Chromeなら今ならpuppeteer が楽チンでオススメ。

2018/02/22 19:54

ブコメでPuppeteerを推す声があったのでチェックしてみたところ

  • ヘッドレスChromeを操作するGoogle製のNode.jsライブラリ
  • npm i puppeteerするだけで動くので環境構築が簡単
    • その他のNode.jsライブラリと組み合わせたプログラムの機能拡張も容易
  • APIドキュメントが公式でキッチリ揃っている

お手軽、高機能、Google製の三拍子揃った便利ライブラリだった。

株マップ.comをスクレイピングするプログラム

使い方に慣れるのにそこそこ苦労したけれど、目的のプログラムを作ることができた。

$ node main.js
[
  '1720', '1805', '1814', '1847', '1882',
  '1887', '1888', '2768', '3023', '3228',
  '3254', '3284', '3294', '3388', '3408',
  '4004', '4042', '4215', '5021', '5101',
  '5288', '5351', '5391', '5702', '6463',
  '7525', '7940', '8002', '8020', '8031',
  '8053', '8091', '8354', '8570', '8591',
  '8604', '8928', '8935', '8999', '9504',
  '9810'
]

上のサンプルプログラムを実行すると、このようにスクリーニングされた銘柄の証券コードの一覧の配列が表示される。GitHubにアップした実際のプログラムでは、fsライブラリを使って手元に保存した証券コードの一覧と比較してスクリーニング結果に注目すべき更新がないか確認できるようにしている。

Puppeteerを使う上での注意点

Promise、async/awaitの知識は必須

APIドキュメントを見れば分かるように、ほとんどのメソッドがPromiseを返していて、メソッドはawaitを付けて呼び出すのが基本になるのでPromiseを用いたJSの非同期処理の仕組みは理解しておかないとPuppeteerを使いこなすのは難しい。

Promiseについては、『JavaScript Promiseの本』が分かりやすい。取り敢えずPromiseの仕組みをザックリ理解したら中間は読み飛ばしてasync/awaitの5章に進むみたいな感じでパパッと読み飛ばしてもPuppeteerを使う上では十分な知識が得られた。

ElementHandleの性質に注意

const elementHandle = await page.$('#example-id')
const elementHandles = await page.$$('.example-class')

のようにpage.$page.$$メソッドを用いてjQueryのような記法でElementHandleを取得できるけれど、このPuppeteer独自クラスのElementHandleは、JSの通常のNode(DOM)とは異なるので、例えばNodeのinnerTextプロパティでデータを取得するというようなことができない。

Nodeとして扱いたいときは、evalがついている方のpage.$evalpage.$$evalメソッドを使うと良い。

const text = await page.$('#example-id', (node) => node.innerText)
const texts = await page.$$('.example-class', (nodes) => nodes.map((node) => node.innerText))

こんな感じで、page.$evaldocument.querySelectorpage.$$evalArray.from(document.querySelectorAll) *1の結果が第2引数の関数の引数として渡されるイメージだ。より柔軟に書けるpage.evaluateメソッドもある。

GASと連携した定期実行も可能か

Google Cloud FunctionsでPuppeteerが使えるので、GASと連携してスクレイピングを定期実行して結果をメール通知というようなこともできそうだ。

*1:querySelectorAllで返るNodeListはArrayではないため、 Array.fromで変換して使いやすくしていると思われる。