Creating screenshot with Firefox + selenium + Node.js

これのつづきです。

前回はヘッドレスモードでブラウザからスクリーンショットを取りました。これで特に問題はなかったのですが、欲が出てきてしまい、実現するにはブラウザからオプションを指定するだけではできなくなりました。
ということで、node.js から selenium-webdriver を使ってのスクリーンショット生成の自動化のコードを書くことにしました。

実現するには、下記が必要です。

試用環境は、Windows10 Pro(1809) + WSL(ubuntu 1604) + Node.js + FireFox(ubuntu) です。
WSLでのFireFoxのインストールは、apt install firefox で普通にできます。また日本語フォントは、一つ前にも書きましたが、/mnt/c/Windows/Fonts ディレクトリのシンボリックリンクを/usr/share/fontsへ作って フォントキャッシュを更新。

また、FireFoxのwebdriverのインストールです。が、これはダウンロードしてきたものをパスの通ったディレクトリ(例えば /usr/local/binとか)に配置すればOK。

$ wget -nd https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz
$ tar xvzf geckodriver-v0.24.0-linux64.tar.gz
$ sudo cp geckodriver /usr/local/bin/ && sudo chmod 0755 /usr/local/bin/geckodriver

適当にディレクトリを作り、まずはこの中で作ります。
selenium-webdriver のインストールです。

$ npm install selenium-webdriver

ググると、簡単なサンプルコードが出てきますが、ページ全体のスクリーンショットを得るためには、ウィンドウの幅・高さを設定しないといけないみたいで・・・下記コードでは、document.bodyのページコンテンツを保存するために、ページロードが終わった後、document.documentElement.scrollHeightで得られた高さを Window.setRect するようにしました。(29行目付近)
幅を1024ピクセルにハードコードしてますが・・・手抜きです💦 コマンドラインのパース一切手抜きです。すみません。

また、余計なお世話的な機能なんですが、CSSセレクタを使用して、指定要素だけのスクリーンショットも取れるようにしました。これは適当に findElement()してその要素に対して takeScreenshot()メソッドをコールしてやるだけ。(35行目付近)

selenium-webdriverのAPIリファレンスを読めば大概のことはできると思います。ブラウザの各種設定を行うabout:configと同じことをしたい場合は、firefoxドライバのインスタンス生成時に、firefox.Options.setPreferenceインスタンスメソッドで変更もしくは追加したOptionsをsetFirefoxOptionsで設定してあげればいいですし(66行目付近)、いつも使っているプロファイルをコピペして、setProfileメソッドでコピペしたプロファイルのディレクトリパスを指定してあげればいい。(試してはいないけど。。。)

下記コードでは ユーザーエージェントを変えられるようにもしてます(ハードコードですけど。。。)

/*******************************************************************************

  Create screenshot for specified url.  if success, output PNG binary data to STDOUT.
  Script file name : screenshot.js

  usage:
  $ node screenshot.js URL [CSS SELECTOR] > screenshot.png

  * if you want to get jpeg file ,use ImageMagick(convert) with pipe.

  for example,
  $ node screenshot.js URL | convert - screenshot.jpg
  
*******************************************************************************/
const { Builder, By, Key, promise, until } = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');
const { createInterface } = require('readline');
let userAgent = null;

async function takeScreenshot(drv,s) 
{
  await drv.wait(async () => {
    const readyState = await drv.executeScript('return document.readyState;');
    return readyState === 'complete';
  });
  let png = null;
  if(s === null)
  {
    const dHeight = await drv.executeScript('return document.documentElement.scrollHeight;');
    await drv.manage().window().setRect({'width': 1024,'height': dHeight});
    png = await drv.takeScreenshot();
  }
  else
  {
    const el = await drv.findElement(By.css(s));
    png = await el.takeScreenshot();
  }
  
  await drv.quit();
  return png;
}

(function() {
  let len = process.argv.length;
  if(len <= 2)
  {
    console.error('too few command option');
    process.exit(1);
  }

  let url = process.argv[2];
  if(!url.match(/^https?:\/\//))
    {
      console.error('specified argv[1] is not url format.');
      process.exit(1)
    }

  let selector = null;
  if(len > 3)
    selector = process.argv[3];

  var firefoxOptions = new firefox.Options();
  firefoxOptions.headless();

  if('string' === typeof(userAgent) && userAgent !== '')
    firefoxOptions.setPreference('general.useragent.override',userAgent);

  let drv = new Builder()
            .forBrowser('firefox')
            .setFirefoxOptions(firefoxOptions)
            .build();

  drv.get(url);

  // takeScreenshot(drv,selector).then((png) => writeSync(1,png,0,'base64'));
  // writeFileSync を使うと、パイプで convertコマンドに出力を渡すとき、エラーになってしまうので、
  // Bufferを作って process.stdout で出力するようにしました。
  takeScreenshot(drv,selector).then((png) => {
    const buf = Buffer.from(png,'base64');
    process.stdout.write(buf);
  });

})();

(75行目付近)上記コメントにも残しましたが、得られたPNGデータを標準出力に書き込むとき、fs.writeSyncを使うとエラーになってしまいます。

$ node screenshot.js 'http://localhost/hogehoge/' | convert PNG:- -resize 600x screenshot.jpg
(node:14444) UnhandledPromiseRejectionWarning: Error: ESPIPE: invalid seek, write
    at writeSync (fs.js:568:3)
    at takeScreenshot.then (/mnt/c/Users/ddk5010/Desktop/temp/screenshot.js:75:39)
    at process._tickCallback (internal/process/next_tick.js:68:7)
(node:14444) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function witho
ut a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:14444) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will te
rminate the Node.js process with a non-zero exit code.
convert: improper image header `/tmp/magick-14445jzJc3SLocfrG' @ error/png.c/ReadPNGImage/3940.
convert: no images defined `screenshot.jpg' @ error/convert.c/ConvertImageCommand/3210.

Node.js固有の問題なのか、WSLが悪さをしているのか分かりません。パイプじゃなくて単にファイルにリダイレクトさせてあげるとエラーは出ません。僕には原因がわからないので、とりあえず、process.stdout のStreamに書き込むとうまく動きました。
一応エラーでググって見たのですが、よくわかんなかったです。いまいち非同期処理が理解てきていないのかも。

selenium-webdriverの takeScreenshotメソッドでは、PNGファイルが取得できますが、「jpgファイルが欲しい!」「リサイズしたもが欲しい!」とかだと、ImageMagickのconvertコマンドに頼る方がよりUNIXライクな方法ではないでしょうか。。。node.jsでも画像処理のモジュールを組みこめばワンストップでできそうですけど。。。

# リサイズしてJPGファイルに
$ node screenshot.js https://www.instagram.com/xxxx/ | convert - -resize 600x insta.jpg

# 特定のセレクタの画像を取得
$ node screenshot.js https://www.yahoo.co.jp/ "#navi" > yahoo-navi.png

エラーハンドリングしてないので、エラーが起こったら適当に 例外処理入れてね。

google-chromeを使う場合もwebdriverのインスタンスを作成するところ以外(具体的には上記コードの async function takeScreenshot()のところ)はほとんど同じ手順ではないかと思います。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください