Node.jsからヘッドレスGoogle Chromeを使ってみる
こんにちは。
開発チームのワイルド担当、まんだいです。
以前、Google Chrome 59でヘッドレスモードが標準搭載されたのでいじってみる | 株式会社ビヨンドというエントリーで記事を書いたところ、予想以上にアクセスや反響をいただきました。
Google Chromeのバージョンアップは、裏で勝手にアップデートされるので普段はあまり意識しないかも知れませんので、これからもマメにチェックしていきたいなと思います。
今回は、Node.jsから、Google Chromeをヘッドレスモードで起動し、chrome-remote-interfaceモジュールを経由して制御してみたいと思います。
サンプルプログラムが動かない!
前回の記事では、コマンドライン上でGoogle Chromeをヘッドレスモードで起動し、画面をキャプチャしたりする方法を見てみましたが、シェルスクリプトだけでは他のシステムとの連携も取りにくいので、Node.jsから起動させ、そのままコンテンツを取得する部分をやってみます。
基本的には、ヘッドレス Chrome ことはじめ | Web | Google Developersの記事を参考に進めていたのですが、どうにもうまく動かなかったので、こちらのコードをベースにしつつ、少し改造してみたのが、以下のソースです。
const ChromeLauncher = require('chrome-launcher'); const CDP = require('chrome-remote-interface'); const url = 'https://beyondjapan.com'; var launcherKill = (client, launcher, args = null) => { launcher.kill(); console.log(args); } ChromeLauncher.launch({ port: 9222, chromeFlags: [ // '--disable-gpu', // 必要に応じて有効化 '--headless', // '--no-sandbox', // 必要に応じて有効化 ], }).then(launcher => { CDP(client => { const {Page, DOM} = client; Promise.all([ Page.enable(), DOM.enable(), ]).then(() => { Page.navigate({url : url}); Page.loadEventFired(() => { DOM.getDocument((error, params) => { if (error){ console.log(params); launcher.kill(); return; } const opts = { nodeId : params.root.nodeId, selector : 'a' }; DOM.querySelectorAll(opts, (error, params) => { if (error){ console.log(params); launcher.kill(); return; } var promises = []; var getDomAttribute = (DOM, nodeId, count) => { return new Promise((resolve, reject)=>{ const opts = { nodeId : nodeId }; DOM.getAttributes(opts, (error, params) => { if (!error) console.log(count, params.attributes[1]); resolve(); }) }) }; console.log(params.nodeIds.length); params.nodeIds.forEach(elm => { promises.push(getDomAttribute(DOM, elm, promises.length)); }); Promise.all(promises).then(()=>{ launcher.kill(); }) }) }) }) }) }).on('error', error => { console.error(error) launcher.kill(); }) })
処理の流れとしては、
- chrome-launcherモジュールを利用して、Google Chromeを起動
- 起動できたら、chrome-remote-interfaceモジュールを利用して、DevToolsプロトコルに接続
- DevToolsクライアントオブジェクトから、指定のURLへ移動し、データを取得
- DOMから、全Aタグを取得して、hrefの値を標準出力に表示する
となっています。
ちゃんと調べきれていないのですが、49行目辺りのDOM.getAttributesメソッドが非同期で動いてそうで、forEachを回した直後にGoogle Chromeを停止するとDOMの内容が読み取れない現象が発生しました。
今回は、Promiseを使って、全てのDOMが読み取れたのを待ってから、Google Chromeを停止するようにしています。
エラーが出た
スクリプトを起動すると、以下のようなエラーが出た場合は、Google Chromeの起動オプションを少しいじる必要があります。
No usable sandbox! Update your kernel or see https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md for more inform ation on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox.
Google Chromeのサンドボックス環境を利用するためのライブラリに、Google Chromeの実行ファイルへのパスがハードコーディングされているバージョンがあるらしく、サンドボックスが利用できない状態の様子。
正しくこのエラーを回避するには、sandbox/linux/suid/sandbox.cc にハードコードされているパスを変更してビルドし直す必要があるようです。
また、緊急の場合やその場しのぎでいい場合は、起動時のオプション「--no-sandbox」をアンコメントすることで、起動できるようになります。
この場合、サンドボックスが利用しない環境でGoogle Chromeを起動することになるので、セキュリティリスクが高くなることにご注意ください。
さて、何に使う?(まとめ)
DevToolsからGoogle Chromeに直接干渉できるので、今までよりリアルなスクレイピングができるようになりそうです。
JavaScriptの動作も含めた試験もやりようによってはできそうなので、こういった試験もコードベースでバックグラウンドで自動実行できるのは、便利になるのではないでしょうか。
ウェブサイトの構成上、curlなどでは監視しきれなかったコンテンツも正常に稼働しているか監視できる仕組みも作れそうです。
以上です。