【Puppeteer】Chromeのお手軽自動化(3)待機による安定動作
こんにちは、自動化エンジニアをしています。kozuです。
PCでのインターネットのブラウザは何を使用しているでしょうか。仕事の業務システムではIEが使われることはありますが、特に指定がない場合はほとんどの人がChromeを使用していると思います。
Chromeを自動操作する方法はいくつかありますが、Node.jsのライブラリ「Puppeteer」について開発方法を紹介します。実際のWebサイトで自動操作と情報を取得するプログラムの開発を通して、自動化ツールの開発について学ぶことができます。
PuppeteerというライブラリはGoogleのChromeを開発しているチームで開発されています。そのため信頼性が高く、最近でも頻繁に更新されており将来的にも期待できます。環境の準備が楽で実装も簡単であるため、個人的には他のライブラリやツールよりも好んで使用しています。本連載では、Node.jsの環境の準備からコードの実装について順を追って解説しようと思います。
この章では、待機の実装方法について解説します。安定した動作をさせるために非常に重要なポイントになり開発したツールが使い物になるかどうかの分かれ道になりますので、確実に理解するようにしてください。今回は画像をPDFに変換するオンラインツールを対象にします。
操作手順としては次の通りです。人が操作すれば簡単ですが、プログラムによる自動操作ではそれぞれの操作の間に待機する必要があり、適切に待機を行わないと最後まで操作することができません。
①URLのページにアクセス
②JPG画像を選択ボタン押下
③ファイル選択
④PDFに変換ボタン押下
⑤PDFのダウンロードボタン押下
1.対象者
ブラウザで定期的に手作業で行っているデータ入力やデータ収集等の作業から開放されたい、楽したいと考えている方を対象に、Chromeの操作を自動化するツールの開発ができるようになることを目指しています。プログラミングの経験がない方でも、コードをコピーすれば開発できるようになっています。
2.開発環境
以下の環境を使用します。
・OS:Windows10
・ブラウザ:Chrome
・node
・npm
3.準備
ファイル構成は以下の通りになります。puppeteer-coreをインストールしたフォルダに今回実装するjavascriptのファイル「wait_test.js」を作成し、PDFに変換する画像ファイルを用意します。
puppeteer-coreのインストールについては以下を参考にしてください。
「wait_test.js」にあらかじめ以下のコードを記述しておきます。URLのページを表示するまでの処理になります。
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: 'C:\\Program Files\ (x86)\\Google\\Chrome\\Application\\Chrome.exe',
defaultViewport: null,
headless: false
});
//新しいタブを開く
const page = await browser.newPage();
//URLにアクセス
await page.goto('https://www.ilovepdf.com/ja/jpg_to_pdf');
})();
4.待機を考慮しない場合の実装
上記の操作手順①~④について待機を考慮しないで実装すると以下のようになります。
※「2.JPG画像を選択ボタン押下」のファイル選択ダイアログの待機について処理はどれだけ待機しても動作しなくなるため、例外として待機を考慮しています。
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: 'C:\\Program Files\ (x86)\\Google\\Chrome\\Application\\Chrome.exe',
defaultViewport: null,
headless: false,
});
//新しいタブを開く
const page = await browser.newPage();
//1.URLのページにアクセス
await page.goto('https://www.ilovepdf.com/ja/jpg_to_pdf');
//2.JPG画像を選択ボタン押下
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('a#pickfiles'),
]);
//3.ファイル選択
await fileChooser.accept(['image.png']);
//4.PDFに変換ボタンを押下
await page.click('button#processTask');
//5.PDFのダウンロードボタンを押下
await page.click('a.downloader__btn');
})();
以下のコマンドで実行すると、ファイル選択後にPDFに変換ボタンが押下されず処理が止まってしまいます。
>node wait_test.js
上記のコマンドを実行したターミナルに以下のエラーメッセージが表示されています。「button#processTask」のセレクタ要素が見つからないとのことです。これは、「4.PDFに変換ボタン押下」でクリックする要素として指定したセレクタになります。
(node:7804) UnhandledPromiseRejectionWarning: Error: No node found for selector: button#processTask
開発者ツールのコンソールで確認してみましょう。
document.querySelectorAll('button#processTask')[0]
上記のコードを実行すると要素が存在していることがわかります。以下のようにPDFに変換ボタンがフォーカスされているためセレクタも正しいことがわかります。
要素が存在しているのに見つからないとはどういうことでしょうか。
画像を選択する前には表示されていなく、画像選択直後に表示されていましたが、Webページの処理として画像を選択されたことを検知し「PDFに変換」ボタンを表示させています。人の目では一瞬で表示されているように見えますが、Webページの処理より今回実行したプログラムの処理の方が早く実行されたためボタンが見つからないという結果になったと考えられます。
したがって、処理の間に待機が必要であることがわかったと思います。以降から待機を考慮した処理の実装について解説します。
5.待機方法1 実行間隔(slowMo)
最初に「puppeteer.launch」でブラウザを起動する際のオプション「slowMo」で各処理間の共通の実行間隔をミリ秒単位で設定することができます。ちなみに、テキスト入力(page.type)では一文字入力する度に待機します。
今回はslowMoの値に「1000」(1秒)を指定すると最後まで操作することができました。
※画像サイズによって正常に操作できるようになる数値が変わります。
const browser = await puppeteer.launch({
・
・
・
slowMo: 1000,
});
設定箇所が1箇所なので実装は簡単ですが、値を調整しながら正常に動作できる実行間隔を見つけるのは操作が多くなるほど大変になり、無駄な待機時間が発生してしまいます。また、ネットワークの通信速度の変化により再現性が欠けてしまいます。
6.待機方法2 時間待機(page.waitForTimeout)
「page.waitForTimeout」で指定した時間だけ待機をします。単位は上記同様ミリ秒です。必要な箇所のみ実装すればよいため、上記の方法と比べ細かい調整がしやすくなります。※以前は「waitFor」でしたが、「waitForTimeout」を使うようにしましょう。
await page.waitForTimeout(100);
今回は以下の2箇所に実装したところ最後まで操作することができました。
※上記slowMoの設定は削除しています。
//3.ファイル選択
await fileChooser.accept(['image.png']);
await page.waitForTimeout(1000);
//4.PDFに変換ボタンを押下
await page.click('button#processTask');
await page.waitForTimeout(2000);
//5.PDFのダウンロードボタンを押下
await page.click('a.downloader__btn');
無駄な時間待機はなくなりましたが、やはり時間待機に動作の安定は期待し過ぎてはいけません。今回は画像をPDFに変換するオンラインツールを対象としているため、画像のサイズが大きくなった場合は変換に時間がかかり「4.PDFに変換ボタンを押下」~「5.PDFのダウンロードボタンを押下」の間に待機すべき時間が変わってしまいます。
その他Webページでも操作の条件により待機に必要な時間が変わることは大いに考えられるため、多用するのは避け、どうしても対応できない場合の最終手段として十分な時間を設定するのに留めるのがよいでしょう。
7.待機方法3 要素表示待機(page.waitForSelector)
「3.ファイル選択」と「4.PDFに変換ボタンを押下」の処理を修正し、操作後に表示されるべき要素を「page.waitForSelector」で待機しています。
「Promise.all」の中に記述される処理が全て完了するまで待機します。記述の注意として、待機処理、操作処理の順番で「await」を付けないことです。「await」は処理が完了するまで待機するために記述していましたが、中の処理を並行して実行するため「Promise.all」の中では「await」を付けないように注意してください。
//2.JPG画像を選択ボタン押下
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('a#pickfiles'),
]);
//3.ファイル選択
await Promise.all([
//PDFに変換ボタンの表示を待つ
page.waitForSelector('button#processTask'),
//ファイル添付
fileChooser.accept(['image.png'])
]);
//4.PDFに変換ボタンを押下
await Promise.all([
//PDFのダウンロードボタンの表示を待つ
page.waitForSelector('a.downloader__btn'),
//PDFに変換ボタン押下
page.click('button#processTask')
]);
//5.PDFのダウンロードボタンを押下
await page.click('a.downloader__btn');
時間待機ではなく要素の表示を待つことで無駄な待機がなくなり、画像サイズが変わっても安定して動作できるようになりました。
8.待機方法3 ページ読み込み待機(page.waitForNavigation)
最後にもう一つ待機の方法を紹介します。「page.waitForNavigation」でページの読み込みを待機することができます。画面が表示された後、ネットワークの接続を行い再度画面の更新を行うことがあります。上記で要素が表示されるのを待ちましたが、要素が再表示されるWebページの場合はネットワークの接続が完了するまで待機するとより安定する可能性があります。
要素表示待機と同様に「Promise.all」の中に記述します。「page.waitForNavigation」のオプション「waitUntill」で「'load'」と「'networkidle0'」を指定しています。「'load'」は画面の読み込みが完了するまで待機し、「'networkidle0'」はネットワーク通信が完了(コネクション数が0個である状態が500ミリ秒経過)するまで待機します。「page.waitForNavigation」のようにオプションを指定しない場合はデフォルトで「'load'」の設定が適用されます。
//4.PDFに変換ボタンを押下
await Promise.all([
//ボタン押下後に画面のloadイベントの完了、ネットワーク通信が500ms間ないことを待つ
page.waitForNavigation({waitUntil: ['load', 'networkidle0']}),
//PDFに変換ボタン押下
page.click('button#processTask')
]);
//5.PDFのダウンロードボタンを押下
await page.click('a.downloader__btn');
セレクタを指定しなくて済むため、一律「page.waitForNavigation」を使えばよいのではと思われるかもしれません。しかし、こちらは画面の読み込み(画面が白くなり画面全体の更新)が発生する場合のみ使用できるため、今回の画像選択後にPDFに変換ボタンの表示を待つためには使用できません。
したがって、画面の一部が変わる場合は「page.waitForSelector」を使用し、画面の読み込みが発生する場合は「page.waitForNavigation」を使用するという使い分けでよいと思います。
以上で待機の実装について解説が終わりになります。puppeteerにはデフォルトで優秀で使いやすい待機の処理が含まれているため、ぜひ使いこなせるようにしましょう。