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
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でも入力するみたいな感じ、うまくいった部分はそのままコードに貼り付けるなどできるので楽
最後に作業時の画像を貼って終わりにする