たゆたふ。

定まる所なく揺れ動き、いろいろやってみたメモ。など

サイトをチェックするツールを作ってみた

ボランティアで防災・減災関連の web サイトの情報を収集している。そのようなサイトは災害時にわっと作られるが、その後、閉鎖されたり、メンテナンスされなくなってしまうサイトも少なくない。同様な地方自治体のホームページも少なからずある。

時々そのようなサイトの状況をチェックするのに便利なツールがなかったので自分で作ることにした。

最初は HTTP でアクセスしてレスポンスをチェックするだけと思っていたので curl 等を使ってちょっとしたスクリプトで済ませようと思った。だた実際に確認してみると個人の作った Web サイトは思ったより続けられていないことが分かった。 同じドメインが全く違うサイトに変わっていたり、閉鎖されて Not Found ページが返ってくるのにレスポンスコードは 200 だったり。ドメインも管理者も変わってなさそうだけど、内容が全く変わっているものもあった。

また、文章ではなく画像に情報が書いてあったり、地図等の外部コンテンツを埋め込んでいるサイトも多く、レスポンスのテキストにキーワードが含まれるかどうかだけではどうも判別が難しい。ということで、チェックは目視ということになるが、いちいち URL にアクセスしていチェックしなくても良いようにスクリーンショットをとる機能を実装することにした。

その他、必要な機能を検討し、以下のようなツールを作ることとした。

  • URL が有効かどうか実際にアクセスして確認する
  • その際、リダイレクトにも追従できる
  • レスポンスのステータスコードが 200 ならスクリーンショットを取る
  • 単一の URL でなく、URL のリストを読ませて複数のサイトを一度にチェックできる
  • 結果を再利用しやすい形で出力する

最初は Phantom.js を使って作った

HTTP のリクエスト/レスポンスだけなら色々方法はあるが、スクリーンショットを取るとなると使えるツールが限られてくる。 実行中にブラウザがぱたぱた動くのも鬱陶しいし、できれば GUI を持たないサーバで動かせるようにしたい。 となると、Phantom.js を使うのが最も手軽かと思い、最初のプロトタイプを作った。 実際には、Phantom.js を使いやすくラップしてくれる CasperJS を利用した。

最低限、動くものを作って、さあ、使いやすくブラッシュアップしていこうかなと思った矢先、Chrome 59 にて ヘッドレスブラウジング がサポートされた。それに伴い、Phantom のメンテナがやる気を失ったらしい(参考:Phantom.jsのメンテナー、プロジェクトの将来に疑問を呈し、その座を降りる

ということで、PhantomJS をインストールしなくても使えるような実装のほうが良いし、いまやシェアトップとなった Chrome を使うほうが、スクリーンショットの結果も普段使っているブラウザとを乖離せず良かろうとこの実装は捨てることにした。

ヘッドレスChromeへの変更

Chrome をプログラムから操るには、DevTool のリモートプロトコルを使えば良いようで、そのためのライブラリchrome-remote-interface を利用して実装を変更した。 このライブラリ、Chrome DevTools Protocolの機能をほぼカバーしているけれど少々プリミティブ過ぎて、使いづらかった。

例えば、通信が1つづつハンドリングできる。ページのレスポンスコードを取るのにページからリンクされる画像や CSS その他へのリクエストの中からドキュメントへのリクエストを探して得る必要がある。それぞれリクエスト ID が取れるのでその ID で再度結果を取得する必要があるとか、とにかく煩雑でやりたいことに対して実装しないといけないことが多かった。おまけに、Chrome そのものを起動する機能は持っていないので、GoogleChrome/lighthouseを使って Chrome の起動を実装する必要があった。

このまま実装を続けても、この先の機能追加とか辛そうと思っていた矢先、GoogleChrome/puppeteer を知った。

puppeteer に切り替え

puppeteer だと、次のようにほんの数行書くだけで、Chrome をヘッドレスで起動して、URL にアクセスしてスクリーンショットを撮ってブラウザを終了してくれる。

// puppeteer でスクリーンショットのサンプル
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const response = await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});
  console.log(response.status)

  browser.close();
})();

同じことを chrome-remote-interface を利用すると次のようになる。スクリーンショットの保存も自前で、しかもこれには Chrome の起動/終了が含まれていない。

// chrome-remote-interfaceでのスクリーンショットのサンプル。
// Chromeの起動/終了は別途必要。
const CDP = require('chrome-remote-interface');
const fs = require('fs');

CDP(async (client) => {
    const {Page} = client;
    try {
        await Page.enable();
        await Page.navigate({url: 'https://example.com'});
        await Page.loadEventFired();
        const {data} = await Page.captureScreenshot();
        fs.writeFileSync('scrot.png', Buffer.from(data, 'base64'));
    } catch (err) {
        console.error(err);
    } finally {
        await client.close();
    }
}).on('error', (err) => {
    console.error(err);
});

また、puppeteer だと、レスポンスコードも簡単に取れるが、chrome-remote-interface だと更に Network クラスを使って Page.navigate のレスポンスコードを探して取り出す必要がある。

これだけ比べても、puppeteer に乗り換えるメリットを感じたし、Chrome チーム謹製というのも安心感があった。 それで、再度実装を変更した。

site-checker として公開

で、作ったツールをこの度、公開した。

我ながらセンスのないベタなネーミングに嫌気がさすが、これというのが思いつかなかったし、npm ライブラリとしても使われていない名前だったので、この名前にした。

次のコマンドでインストールできる。

$ npm install -g site-checker

インストールしたら、次のコマンドで、スクリーンショットが取れる。

$ site-checker -u http://hero.hatenablog.jp

できることは次の通り。

特に、フルページとエミュレーションは puppeteer に切り替えてとても簡単に実装できた。

詳しい使い方はこちらを参照 => 使い方 · HeRoMo/site-checker Wiki