notebook

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

Node REPL+Puppeteerでの開発Tips

Puppeteerでスクレイピングしていてやっと「これ楽だわ」みたいな感じのやり方が定着してきた気がするのでアウトプットしておく

ちなみにどこまでいってもなるべくClickなどの操作をせずにページ内容を読み取れるのがベターだと思っている

あと、REPLにかんしてはトップレベルでawaitを使うので--experimental-repl-awaitを付ける前提で書いている

よく使う処理

自分はXpathで要素を指定して値を取得するのが好みなのでよく使っている

書いているとawait()の対応がだんだんよくわからなくなってきて無駄に時間を消費していたのでよく使う処理を関数化している

ElementHandleから特定のプロパティの値を取得する

この時点でawaitが2つもあるのでまとめず書くと結構しんどい

const getProperty = async (elementHandle, property) => {
  return elementHandle ? "" : await ( await elementHandle.getProperty(property)).jsonValue();
}

ElementHandleから特定のXpathの特定のプロパティの値を取得する

elementHandleからさらにXPathとIndexとプロパティ名を指定して値を取ってくるような関数

$xで返ってくる値が配列なのでindexを指定する必要がある

// `$x`(xpath)でElementHandleを取得して特定のindexの特定のkeyのデータを取得するためのメソッド
const getByXpath = async (elementHandle, xpath, index, property) => {
  return await getProperty((await elementHandle.$x(xpath))[index], property);
}

HTMLの中身を取得する

画面で見ると値があるはずなのにスクリプトで実行すると取れないとき

実際HTMLどうなってんの? というときに使う

これのおかげで大分はかどった

const toHtml = async (elementHandle) => {
  return elementHandle ? await elementHandle.evaluate(e => e.outerHTML) : "";
}
  • 使用
const elementHandle = (await page.$x('//a[contains(@role, "link")]/time'))[0]
await toHtml(elementHandle);
// '<time datetime="2021-05-19T11:00:32.000Z">May 19</time>'

テキストの中身を取得する

対象としているelementHandleが画面上で何を指しているか把握するとき用

textContentを取得するための関数

この関数も定義しているおかげで大分捗るようになった

const toText = async (elementHandle) => {
  return elementHandle ? await elementHandle.evaluate(e => e.textContent) : "";
}
  • 使用
const elementHandle = (await page.$x('//a[contains(@role, "link")]/time'))[0]
await toText(elementHandle);
// 'May 19'

基本はDevtools見れば良い感はあるが実際にタグがどうなっているのかはコード書きながらコンソールで確認したほうが早い場合もある

というか個人的にはそういうサイクルのほうがやりやすい

意図したコードのはずなのになぜか値が取れないとかそういう場合の確認用に使っている

この辺をまとめて次のようなファイルを作って都度確認できるようにしたりしている

  • lib/puppeteer_helper.js
// `$x`(xpath)でElementHandleを取得して特定のindexの特定のkeyのデータを取得するためのメソッド
const getByXpath = async (elementHandle, xpath, index, property) => {
  return await getProperty((await elementHandle.$x(xpath))[index], property);
}

const getProperty = async (elementHandle, property) => {
  return elementHandle == undefined ? "" : await ( await elementHandle.getProperty(property)).jsonValue();
}

const toHtml = async (elementHandle) => {
  return elementHandle ? await elementHandle.evaluate(e => e.outerHTML) : "";
}

const toText = async (elementHandle) => {
  return elementHandle ? await elementHandle.evaluate(e => e.textContent) : "";
}


module.exports = {
  getByXpath,
  getProperty,
  toHtml,
  toText
} 
  • 呼び出し側
const ph = require('./lib/puppeteer_helper');
...
...

await ph.toHtml(elementHandle);
// '<time datetime="2021-05-19T11:00:32.000Z">May 19</time>'

モジュールのリロード

参考:

Node.js: how to reload module - Stack Overflow

stackoverflow.com

function nocache(module) {require("fs").watchFile(require("path").resolve(module), () => {delete require.cache[require.resolve(module)]})}

モジュールを修正してREPLに反映させたかったがNodeのREPLにはpryみたいにreload!はないよう

単純にrequireし直せばOKかと思ったら反映されなかったため調べてみると、参考に載っているようにcacheが効いているってことらしい

上記のnocacheを宣言してREPL中で実行し再度対象のモジュールをrequireすることで変更した内容を読み込めるようになる

nocache('./lib/puppeteer_helper')

REPLではこれでOK

headless: falseで起動する

const browser = await puppeteer.launch({headless: false, devtools: true, args: ['--no-sandbox', '--disable-setuid-sandbox']});

GUI起動できるようにしておくことで画面を見ながら開発を行える

自分はWSL2上で書いているのでXLaunch経由でGUIを起動させて確認できるようにしている

この辺整備したことで大分楽になった

REPLのログを表示する

次のようなコマンドを作ってREPLのhistoryに変更があったら履歴を再描画するようにしてサクッとコピー・ペーストできるようにした

  • watch_node_repl_history
#!/bin/bash

display_history() {
  eval `resize`
  head -n $(($LINES - 1)) $HOME/.node_repl_history | tac | bat --style='snip' --paging=never -l javascript --color=always
}

display_history
inotifywait --monitor --quiet --event MODIFY $HOME/.node_repl_history | while read d e f; do
  display_history
done

やっていることは下記

  • resizeで現在の表示領域のカラム数、行数を取得できるのでそれをevalで環境変数へ読み込む→表示量の上限にする
  • batというコマンドでコードのリストのみを色付きで表示
  • .node_repl_historyに変更があったときのみ実行

watchで--colorつければコマンド化いらないだろうと思っていたがbatと組み合わせると色がつかず

色々試行錯誤してみたもののあまり時間取るのももったいないと思い方針転換して上記のようにした

横に実行したコマンドリスト出しながらREPLでも入力するみたいな感じ、うまくいった部分はそのままコードに貼り付けるなどできるので楽

最後に作業時の画像を貼って終わりにする

f:id:swfz:20210610200046g:plain