Headless Chromeでファイルをダウンロードする

node.jsweb

Chrome DevTools Protocolに ExperimentalだがPage.setDownloadBehavior というのがあったので、これを呼んでファイルをダウンロードしてみた。

今回は公式のDevToolsのNode API、Puppeteerを使うが、 setDownloadBehaviorを送るAPIはまだなく、直接clientを取ってsendするので他のライブラリでもやることは変わらないと思う。 Puppeteerのインストールの際にChromiumも入る。setDownloadBehaviorは現行Chromeの60では対応していないようだが、62が入ったのでなんとかなりそう。

$ yarn add puppeteer
$ find . -name "*chrome*"
./node_modules/puppeteer/.local-chromium/mac-497674/chrome-mac
./node_modules/puppeteer/.local-chromium/mac-497674/chrome-mac/Chromium.app/Contents/Versions/62.0.3198.0/Chromium Framework.framework/Versions/A/Resources/chrome_100_percent.pak
./node_modules/puppeteer/.local-chromium/mac-497674/chrome-mac/Chromium.app/Contents/Versions/62.0.3198.0/Chromium Framework.framework/Versions/A/Resources/chrome_200_percent.pak

ちなみに、このChromeをLambda上で実行しようとすると失敗する。

Lambda上でPuppeteer/Headless Chromeを動かすStarter Kitを作った


ChromeでChromeをダウンロードしてみる。

const puppeteer = require('puppeteer'),
      fs        = require('fs');

const headless     = true,
      downloadPath = './Download';

(async () => {
  const browser = await puppeteer.launch({headless: headless});
  
  const page = await browser.newPage();
  await page._client.send(
    'Page.setDownloadBehavior',
    {behavior : 'allow', downloadPath: downloadPath}
  );

  await page.goto('https://www.google.co.jp/chrome/browser/desktop/index.html', {waitUntil: 'networkidle'});
  await page.click('a.download-button');  /* Chromeをダウンロード         */
  await page.click('button#eula-accept'); /* 利用規約に同意してインストール */

  await waitDownloadComplete(downloadPath)
        .catch((err) => console.error(err));
 
  console.log('finished');
  browser.close();  
})();

ファイルがダウンロードできたかどうかは.crdownloadのありなしで判定している。

const waitDownloadComplete = async (path, waitTimeSpanMs = 1000, timeoutMs = 60 * 1000) => {
  return new Promise((resolve, reject) => {

    const wait = (waitTimeSpanMs, totalWaitTimeMs) => setTimeout(
      () => isDownloadComplete(path).then(
        (completed) => {
          if (completed) { 
            resolve();
          } else {

            const nextTotalTime = totalWaitTimeMs + waitTimeSpanMs;
            if (nextTotalTime >= timeoutMs) {
              reject('timeout');
            }

            const nextSpan = Math.min(
              waitTimeSpanMs,
              timeoutMs - nextTotalTime
            );
            wait(nextSpan, nextTotalTime);
          }           
        }
      ).catch(
        (err) => { reject(err); }
      ),
      waitTimeSpanMs
    );
    
    wait(waitTimeSpanMs, 0);
  }); 
}

const isDownloadComplete = async (path) => {
  return new Promise((resolve, reject) => {
    fs.readdir(path, (err, files) => {
      if (err) {
        reject(err);
      } else {
        if (files.length === 0) {
          resolve(false);
          return;
        }
        for(let file of files){

          // .crdownloadがあればダウンロード中のものがある
          if (/.*\.crdownload$/.test(file)) { 
            resolve(false);
            return;
          }
        }
        resolve(true);
      }
    });
  });
}

Headlessだと何もでてこないのでうまくいったか良くわからないが、 指定したパスを見にいったらちゃんと保存されていた。


機能的には近い立ち位置のNightmareJSの方も、 v1のPhantomJS、現行v2のElectronを経て v3ではHeadless Chromeになるかもしれない。 速いし、ウィンドウがないのでxvfb(X virtual framebuffer)必要ないし良さそうなんだが、 現在のChrome DevTools ProtocolではNightmareの既存APIをサポートできなかったり、 Puppeteerとの住み分けはどうするのって話になっているみたいだ。

現状Nightmare自体にダウンロード機能は含まれていないが、 Electronのwill-downloadイベントを ハンドリングする nightmare-download-managernightmare-inline-download といったライブラリがある。