Puppeteerで特定要素の値が変わるまでウェイトを掛ける
業務で本格的にPuppeteerにて処理の自動化を実装していますが、時々問題になるのが「ウェブサービス側の処理が重くて、タイムアウトする」という症状。直近でぶつかったのは、SalesforceのTeamSpiritにて特定のレポートをダウンロードするシーンにて、クエリが重くなかなか処理が終わらず、待機してるPuppeteerがタイムアウトで処理を中断してしまう(特定の要素がその処理が終わらないと有効にならない為)。
幸い、TeamSpiritの場合、クエリの処理中はIDがStatusの項目があるので、コレを基準に処理が終わるまで無限ループで待機させる事でエラーを回避することが出来ました。今回はこの特定要素の値を元に無限ループでウェイトを掛ける方法を作ってみました。
目次
今回使用するライブラリ
今回のダウンロード上の問題点
今回対象にしてるTeamSpiritは、様々な勤怠レポートを作成しデータをCSVでダウンロードすることが出来ます。しかし、作成するレポートによっては以下のような問題が含まれているため、Puppeteerで自動ダウンロードを実装する場合、時々エラーが発生して止まるケースがあります。
- レポートの処理中はIDがstatusの要素内の文字列が「処理中」となっている
- 処理中が完了やその他の表記に変わるまで、期間条件の開始と終了のテキストボックスが使えない(値を入れようとするとエラーとなる)
- ページのロードが終わった段階で完了表記になっていないとNGなので、await page.waitForNavigation({ waitUntil: "networkidle2" });などでは、対処しきれない
- 開始日のtbody #colDt_sの要素が完了前に入れようとしてエラーが発生して止まってしまう
コンソールに表示されるエラーとしては、以下のようなものが表示されます。
1 2 3 4 5 6 7 8 9 10 11 |
(node:14240) UnhandledPromiseRejectionWarning: TimeoutError: waiting for selector `tbody #colDt_s` failed: timeout 30000ms exceeded at new WaitTask (C:\Users\googl\Documents\hosei\node_modules\puppeteer-core\lib\cjs\puppeteer\common\DOMWorld.js:509:34) at DOMWorld.waitForSelectorInPage (C:\Users\googl\Documents\hosei\node_modules\puppeteer-core\lib\cjs\puppeteer\common\DOMWorld.js:420:26) at Object.internalHandler.waitFor (C:\Users\googl\Documents\hosei\node_modules\puppeteer-core\lib\cjs\puppeteer\common\QueryHandler.js:31:77) at DOMWorld.waitForSelector (C:\Users\googl\Documents\hosei\node_modules\puppeteer-core\lib\cjs\puppeteer\common\DOMWorld.js:313:29) at Frame.waitForSelector (C:\Users\googl\Documents\hosei\node_modules\puppeteer-core\lib\cjs\puppeteer\common\FrameManager.js:841:51) at Page.waitForSelector (C:\Users\googl\Documents\hosei\node_modules\puppeteer-core\lib\cjs\puppeteer\common\Page.js:2252:33) at puppetrun (C:\Users\googl\Documents\hosei\index.js:2547:20) at runMicrotasks (<anonymous>) at runNextTicks (internal/process/task_queues.js:58:5) at listOnTimeout (internal/timers.js:523:9) |
図:レポート生成状況がポイント
コードと解説
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//idがstatusの項目が処理中でなくなったら次へ進む(タイムアウト回避策) await page.waitForSelector('#status'); var checkword = await page.evaluate((selector) => { return document.querySelector(selector).innerHTML; }, '#status'); while (checkword == "処理中"){ //ウェイト発生 console.log("2秒間ウェイトを実行") //2秒間ウェイト await page.waitForTimeout(2000); //再度、要素を取得し直す checkword = await page.evaluate((selector) => { return document.querySelector(selector).innerHTML; }, '#status'); } //日付入力欄に指定(ここがよくタイムアウトする) try{ await page.waitForSelector('tbody #colDt_s') await page.click('tbody #colDt_s') await page.$eval('tbody #colDt_s', (el, startday) => { return el.value = startday; }, startday); await page.waitForSelector('tbody #colDt_e') await page.click('tbody #colDt_e') await page.$eval('tbody #colDt_e', (el, endday) => { return el.value = endday; }, endday); }catch(e){ //ページのリロード await page.reload({ waitUntil: "networkidle2" }); await page.waitForSelector('tbody #colDt_s') await page.click('tbody #colDt_s') await page.$eval('tbody #colDt_s', (el, startday) => { return el.value = startday; }, startday); await page.waitForSelector('tbody #colDt_e') await page.click('tbody #colDt_e') await page.$eval('tbody #colDt_e', (el, endday) => { return el.value = endday; }, endday); } |
解説
- 対象のページの読み込みが完了したら、まずはIDがstatusの要素内の文字列を取得し、checkwordに格納する
- Whileループにてcheckwordの値が処理中の場合は無限ループとしておく
- 処理中であった場合には、page.waitForTimeoutにて2秒間意図的にウェイトを掛ける
- 再度、冒頭と同じくIDがStatusの要素内の文字列を取得し、checkwordに格納する
- checkwordの値が「処理中」でなくなったら処理を抜けて、開始日のテキストボックスに日付を入れる処理へ移る
- 現在はこのコードのおかげでとりあえず、完了するまできちんと待ってくれるので、ストップすることはありません。
上記のコードではページがネットワーク不良で止まった場合に備えて、page.reloadを含めていますが、この処理は今回のテーマとは別の問題の対処用です。
page.waitForがDeprecatedな件
ウェブを見ていると、意図的に指定秒数でウェイトを掛ける場合には、page.waitFor(3000)といった形でウェイトを掛けるといった表記がありますが、現在すでにこのメソッドはDeprecatedになっており使えません。現在は、page.waitForTimeout(3000);に置き換えて、利用する事になります。
単純に置き換えるだけでOKなので、今後はこのメソッドを使います。
尚、本件に関するIssueについては、こちらのエントリーにて記述されているので、参照してみてください。
要素のDisableが解除されるまで待機する方法
前述のような複雑な仕組み以外にもこちらに掲載されていたような手法で、例えば特定の要素が自動的にある秒数経過するとDisable⇒Disableじゃなくなるといった場合、これを待機させて、解除されたらそれをクリックといったことがしたい場合があります。Disableかどうかを常にループで監視するのではなく、以下のような解除されるまで待機といった事が可能です。
1 2 3 4 5 6 |
//対象のエレメントがDisableじゃなくなるまで待機 await page.waitForSelector('#btnStInput:not([disabled]',{timeout:90000}); //対象の要素をクリック await page.waitForSelector('#btnStInput') await page.click('#btnStInput') |
自分はこのテクニックをTeamSpiritの勤怠打刻の自動化で利用しています。要素の操作で#要素のID:not([disabled]といった形で指定するのがポイント。
図:この部分をクリックするのに必要