notebook

都内でWEB系エンジニアやってます。

PuppeteerでXPathを扱う

サンプルやPuppeteerの記事でよく見る$eval$$evalはCSSセレクタで対象の要素を指定してコールバックを渡したらその中ではブラウザのコンテキストで処理ができる(Elementが渡る)のでelement.hrefのように直接呼び出せる

便利ではあるがここ最近CSSセレクタで対象要素を指定するのがなかなか難儀なためそれなりに使い慣れているXPathで指定してよしなにやりたいなと考えるようになった

page.$eval, page.$$eval

CSSセレクタで対象要素を特定する

page.$eval

コールバックに渡る要素は単数

const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', e => e.outerHTML);

page.$$eval

コールバックに渡る要素は複数

const divCount = await page.$$eval('div', divs => divs.length);
const options = await page.$$eval('div > span.options', options => options.map(option => option.textContent));

要素の特定にXPathを使う

今回はこっちがやりたかった

page.$x('//a');

返ってくるのはPromise<Array<ElementHandle>>

そもそもコールバックは渡せないし扱うのはElementHandle

evalに渡すコールバック内で処理するものと違いElementHandleからテキストを抽出するのは少し面倒

evalで使うようにelement.hrefなど直接呼び出せない

同じようなのりで書くとfunction href not foundと怒られる

具体的な例としては少々雑だが次のようにする必要がある

const elementHandles = await page.$x('//a');
const url = await(await elementHandles[0].getProperty('href')).jsonValue();

参考

node.js - getting property from ElementHandle - Stack Overflow

stackoverflow.com

複数要素であれば次のようにする

  • 特定文言を含んだリンク先を抽出する
const links = await page.$x('//a[contains(@href, "hoge")]');
await Promise.all(links.map(async(e) => await (await e.getProperty('href')).jsonValue()));
  • タグ内のテキストを抽出する場合
const list = await page.$x('//div');
await Promise.all(list.map(async(e) => await (await e.getProperty('textContent')).jsonValue()))

こんな感じで取ってこれる

メソッド化

参考のサイトにもあるが毎度このあたりのコード書くのも微妙なのでメソッド化すると良さそう

const getProperty = async (elementHandle, property) => {
  return await ( await elementHandle.getProperty(property)).jsonValue();
}
  • 呼び出し側
const links = await page.$x('//a');
const hrefs = await Promise.all(links.map(async(e) => await getProperty(e, 'href')));

ElementHandle

調べていたらこのElementHandle 要素を操作するという観点から見るとなかなか便利

ElementHandleからclick()focus()などで操作できる

なので本来は操作の用途で使うのがベターそう

まとめ

PuppeteerでXPathを用いて要素を特定する場合

  • page.$xを使う
  • ElementHandleよしなにテキストを抽出できるイディオムをおさえておく
    • await(await elementHandle.getProperty('href')).jsonValue()

evalでコールバックの中でゴニョゴニョやってデータ取得するというのも要素特定がDevToolでさくっと確認できるので楽な場合もある

ただ、直近触ってみた感じだと個人的にはpage.$xを使い、XPathで要素を指定+ElementHandleでよしなにやるパターンがやりやすいなと感じた

おまけ

余談だがPuppeteerのAPIドキュメントを読んでいたらpage.$xの他にもpage.$page.$$というメソッドもあるよう

こちらはCSSセレクタで要素を特定し、ElementHandleが返ってくる

なのでXPathが嫌だけどElementHandle使いたいパターンのときはこちらを使うのが良い