「ステュディオス」な生活

「ステュディオス」=何かを面白がり、熱中することにより生き生きしている状態。 日々の「ステュディオス」を求めて…

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

ボランティアで防災・減災関連の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の機能をほぼほぼカバーしているけれど少々プリミティブ過ぎて、使いづらかった。

例えば、通信が一つ一つハンドリングできるので、ページのレスポンスコードを取るのに、ページからリンクされる画像や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