Puppeteerが原理主義すぎるのでラッパーモジュールを育てている話


Puppeteerの不思議な(不親切な?)挙動

Puppeteerで要素が可視状態になるまで待ちたいとき、次のように記述できます。

page.waitForSelector('#myId', {visible: true})

しかし、このノリで何でも取り出せると思うとハマることがあるので注意が必要です。具体的には次のようなHTMLに対して

<div style="visibility:hidden">foo</div>
<div>bar</div>
<div>baz</div>

Puppeteerで次のように記述したとしましょう。

page.waitForSelector('div', {visible: true})

これで<div>bar</div>が帰ってくるかと思いきや、これは期待通りに動きません。

実際には<div style="visibility:hidden">foo</div>がvisibleになるのを待ってしまってタイムアウトします。

どういうことか?

上記はPuppeteerの仕様です。これをバグとしてPRを投げた偉い人がいるんですが、Puppeteerの作者に却下されています。

作者によれば、「CSSセレクタとオプションの組み合わせで要素を指定するのはよくない、排他的に要素を限定するようなCSSセレクタを書くべきだ、PuppeteerのAPIはそういうポリシーで作っている」とのことです。

自分でラッパーモジュールを作ろう

作者のポリシーはわかるけど僕はルーズに書きたいんですよね…。

Puppeteer本体のAPIでは今後も便利関数は却下されそうなので、ルーズに書きたい人は自分専用のラッパーモジュールを育てていくのがいいと思います。

ちなみにPuppeteerではAPIの機能追加がしやすいように全APIが1ファイルからアクセス可能になっており、次のように簡単に機能追加できます。

const {ElementHandle} = require('puppeteer/lib/api');

ElementHandle.prototype.isVisible = async function () {
  return (await this.boundingBox() !== null);
}

こうすればElementHandle.isVisible()が増やせます。

最初の例であれば、私なら次のように書きます(意味は変わってしまいますが目的は達成できるのではないでしょうか)。

  await page.waitForSelector('div');
  visibleDivs = await page.$$('div').then(
    els => asyncFilter(els, el => el.isVisible())
  );
  // visibleDivsが0個ならビジーループ、1個以上なら処理継続

上記コード中のasyncFilter()は「JavaScriptで配列のfilterにasync関数を使いたい - Qiita」で紹介したものです。

参考URL

ちなみに私が作っているラッパーはnpmで公開しています。上のメソッド以外にも私が便利だと思った関数を追加しています。参考にして頂ければ幸いです。