趣味の株式投資で投資対象の選定にクォンツ・リサーチ社が運営する株マップ.comの銘柄スクリーニングを便利に使わせて貰っているけれど、これまで
- ブラウザのブックマークからスクリーニング結果にアクセス
- 表の文字列を手元のテキストエディタにコピペ
- 正規表現で扱いやすいデータに置換
- 手元に保存しているデータとの差分をチェックし、新たに注目すべき銘柄がないか確認
- 上記のフローを複数のスクリーニング条件について確認
という作業に数分かかっていて、Webスクレイピングでの自動化を試みたいと思った。
GASでできそうだったが…
まずGoogle Apps Script(GAS)のUrlFetchApp.fetch
でサクッとスクレイピングできそうだったので試してみたけれど、株マップ.comのスクリーニングの表はHTMLのレスポンスに含まれておらずJSで描画されているので、UrlFetchApp.fetch
の機能では取得できなかった。
ちなみに今回は試さなかったけれど、PhantomJs Cloudを使えばGASでもできるらしい。
Puppeteerという選択肢
JSの描画に対応できる選択肢としてPythonまたはRubyでSeleniumを使えばできそうだと分かったけれど、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.$eval
、page.$$eval
メソッドを使うと良い。
const text = await page.$('#example-id', (node) => node.innerText) const texts = await page.$$('.example-class', (nodes) => nodes.map((node) => node.innerText))
こんな感じで、page.$eval
はdocument.querySelector
、page.$$eval
はArray.from(document.querySelectorAll)
*1の結果が第2引数の関数の引数として渡されるイメージだ。より柔軟に書けるpage.evaluate
メソッドもある。
GASと連携した定期実行も可能か
Google Cloud FunctionsでPuppeteerが使えるので、GASと連携してスクレイピングを定期実行して結果をメール通知というようなこともできそうだ。
*1:querySelectorAllで返るNodeListはArrayではないため、 Array.fromで変換して使いやすくしていると思われる。