はじめに
chromeの開発者ツールの Recorder タブの機能を使うと、
画面操作を記録し、自動操作用のpuppeteerコードを出力できます。
puppeteerとは、ブラウザの自動操作ができるNode.jsのライブラリ
当初「この機能を使って、ブラウザを操作→記録して実行すれば、簡単にブラウザの操作を自動化できるじゃん!」と思ったんですが、そのまま使うと結構使いづらい部分がありました…
ので、この記事ではその改善をした経験を踏まえ
- Recorderタブやpuppeteerの解説
- 環境構築
- Recorderで出力→puppeteer実行する方法
- 自動生成したコードの改良方法
といったことを記します。
※後述しますが、いくつかutils作ってサンプルコードを作ってみたので、良かったら使ってください。
Recorderタブやpuppeteerの解説
Chrome Recorderタブ
Chrome DevTools(開発者ツール)のタブの一つ。
ブラウザのGUI操作を記録し、再生(同じ操作を繰り返す)をしたり、スクリプトファイルなどの形で出力ができる機能。
こんな感じで、高速に操作内容を再生してくれる。
ブラウザから再生する他、処理の内容をほかの自動操作ツール用にエクスポートすることもできる。
Puppeteer
パペティアは操り人形師の意。
Node.jsのライブラリで、Chrome/Firefoxブラウザを自動操作することができる。
テスト(DOM状態などの検証)や画面の自動操作ができる。
Googleが開発しており、他の自動操作ライブラリ(例: Selenium)と比べると、以下のような利点がある。
- 操作が正確かつ早い(直接DevToolsプロトコルにアクセスしているため)
- 環境構築が楽(Webドライバーは不要なため)
→個人的には、2のメリットは触っていて実感できた。
Seleniumの自動操作も過去やっていたが、ブラウザのバージョンに合わせてWebドライバーも結構頻繁に更新が必要になったりする。
(ドライバーのダウンロードや更新もコードからできたりするが、最初に触る人などは知らずに煩わしく思ったり、その更新頻度に閉口するんじゃないだろうか。)
Chrome Recorder × Puppeteerでの自動化をしてみよう
Puppeteer環境の構築手順
※主にWindowsを基準に記載する。
-
node のインストール
node が install されていなければ、 https://nodejs.org/ja からダウンロード&インストールする。 -
必要なライブラリのインストール
npm install puppeteer
最低限これだけで動く。
(なんなら自動化用のコードも、後述する方法で、GUI操作からのエクスポートをするだけで用意できる。)
なんて簡単なんだ!![]()
RecorderからPuppeteerコードをエクスポートする際の注意点
Recorderタブの表示や操作の基本は 公式のドキュメント 参照。
ここでは、特に「Recorderから出力してLocalのPuppeteerで動かす」といった使い方をしたいときの注意点などを記す。
注意点1:画面操作は最低限で済ませる
画面の操作やキーボードの操作は大概Recorderが記録してくれる。裏を返せば
- 半角/全角キーを押して入力切替をする
- 特にボタンとかでも何でもないエリアをクリックする
みたいな、自動化する上では別に必要ではない処理も記録されることがあるから、予め操作内容のシナリオのようなものを決めてから記録するのがおすすめ。
不要な処理が増えれば、それだけ実行時間がかかるし、後からコードを整理したい時に「何のための処理だっけ」と悩むことも増える。
一応、開発者ツールで操作を記録した後に、特定の処理だけ削除してからエクスポートする、といったことは可能。
注意点2:キー入力は、1キーずつ記録される
例えば、日本語ローマ字入力で「キー」と入力する場合、入力するキーはki-で、それを変換すればよい。
ただ、実際に記録すると以下のような結果になる。(省略しているが、全部で12ブロック)
{
const targetPage = page;
await targetPage.keyboard.up('k');
}
{
const targetPage = page;
await targetPage.keyboard.down('Process');
}
{
const targetPage = page;
await targetPage.keyboard.up('Process');
}
{
const targetPage = page;
await targetPage.keyboard.up('i');
}
...省略...
キー入力のイベントとして記録されるので1キーずつのブロックになっている。また、'Process'キーも記録されてしまう。
Processキーというのは、日本語IMEで変換をかけるときの指示に使う特殊なキー。
テスト目的で1字ずつ入力したい、とか言った場合を除けば、入力内容をあらかじめ用意して、コピペをすることをお勧めする。
コピペであれば、以下のように入力値単位で管理ができるため、自動化した後に値だけ変える、といったこともやりやすい。
{
const targetPage = page;
await puppeteer.Locator.race([
...省略...
])
.setTimeout(timeout)
.fill('コピペ');
}
エクスポートされたコードの例と構造
以降では、 Qiitaのトップページで検索窓に「Puppeteer」と入力してEnterを押す という処理の自動化コードを題材に、概ねどういった構造で生成されるかをコメントで付す。
※// ★コメント のように記載しているものは、解説用に付記したコメント。
const puppeteer = require('puppeteer'); // v23.0.0 or later
(async () => {
// ★ブラウザの初期化やタイムアウト秒数、ビューポートサイズなどの設定
const browser = await puppeteer.launch();
const page = await browser.newPage();
const timeout = 5000;
page.setDefaultTimeout(timeout);
{
const targetPage = page;
await targetPage.setViewport({
width: 1350,
height: 945
})
}
{ // ★操作対象のページを開く
const targetPage = page;
await targetPage.goto('https://qiita.com/');
}
{ // ★検索窓をクリック
const targetPage = page;
await puppeteer.Locator.race([ // ★以降に示したLocatorで順に画面内の要素を探す
targetPage.locator('::-p-aria(記事、質問を検索[role=\\"searchbox\\"])'),
targetPage.locator('header > div input'),
targetPage.locator('::-p-xpath(//*[@id=\\"GlobalHeader-react-component-f5286447-730a-4375-b837-314254992683\\"]/div/header/div/div[2]/form/input)'),
targetPage.locator(':scope >>> header > div input')
])
.setTimeout(timeout)
.click({ // ★見つかった要素をクリックする(offsetは対象要素の左上を基準にした調整用の値)
offset: {
x: 72,
y: 17.40625,
},
});
}
{ // ★Puppeteerと入力
const targetPage = page;
await puppeteer.Locator.race([
targetPage.locator('::-p-aria(記事、質問を検索[role=\\"searchbox\\"])'),
targetPage.locator('header > div input'),
targetPage.locator('::-p-xpath(//*[@id=\\"GlobalHeader-react-component-f5286447-730a-4375-b837-314254992683\\"]/div/header/div/div[2]/form/input)'),
targetPage.locator(':scope >>> header > div input')
])
.setTimeout(timeout)
.fill('Puppeteer');
}
{ // ★Enterキー押下と待ち処理
const targetPage = page;
const promises = [];
const startWaitingForEvents = () => {
promises.push(targetPage.waitForNavigation()); // ★画面遷移が終わるまで待つ
}
await targetPage.keyboard.down('Enter');
await Promise.all(promises);
}
{ // ★Enterキーを離す
const targetPage = page;
await targetPage.keyboard.up('Enter');
}
await browser.close(); // ★ブラウザを閉じる
})().catch(err => { // ★エラーハンドリング
console.error(err);
process.exit(1);
});
少しだけ追加で説明。
↓のようなコードで、操作対象の要素を探しているが、探す際に使うLocatorは複数指定されている。
await puppeteer.Locator.race([
targetPage.locator('::-p-aria(記事、質問を検索[role=\\"searchbox\\"])'),
targetPage.locator('header > div input'),
targetPage.locator('::-p-xpath(//*[@id=\\"GlobalHeader-react-component-f5286447-730a-4375-b837-314254992683\\"]/div/header/div/div[2]/form/input)'),
targetPage.locator(':scope >>> header > div input')
])
一般に、ブラウザ操作の自動化をする際、実装に用いているライブラリなどの影響で、idやclassNameなどにランダムな文字列が付与され、要素を探すときに安定して動かない、といった問題が起きやすい。
だが、Recorderタブで生成した自動コードは、複数候補を使って指定してくれるため、より安定し自動操作をすることができる。
自動化コードの改良
基本的な画面遷移、クリック、入力 といった処理はうまくいくことが多いが、いくつか注意が必要な処理もある。
以下のリポジトリにサンプルコードをアップしている。
デフォルトはブラウザ非表示/遅延時間なしで動作する
エクスポート時点では、以下のようにしてブラウザ起動をしている。
const browser = await puppeteer.launch();
デフォルトだと、以下の指定と同じになる。
const browser = await puppeteer.launch({
headless: true, // ブラウザ非表示(ヘッドレス)モードで動作する
slowMo: 0 // 遅延なしで動作する
});
これでうまくいけば問題ないが、
処理が速すぎるために失敗することがあり、かつ、ヘッドレスモードのためどこで落ちているのか気づきにくい ということがある。
慣れていない人は
headless: false,
slowMo: 5
で設定するのをお勧めする。
サンプルコードでは、createBrowserSessionという関数をutilsに定義し、引数で切り替えるようにしている。
viewportのサイズとウィンドウのサイズが違い、表示が崩れる(ようにみえる)
エクスポートしたコードだと、ウィンドウ生成後、setViewportでサイズの指定をしている。
{
const targetPage = page;
await targetPage.setViewport({
width: 1350,
height: 945
})
}
ただ、このままだと画像のように、viewportのサイズとウィンドウのサイズが違い、表示が崩れているように見えてしまう。
ブラウザ側の機能でキャプチャを取得する際などはviewport基準で取得されるため、見た目が悪い程度の問題で済むが、自動操作中の画面をスニッピングツールなどで撮る場合などは注意が必要。
サイズを合わせたい場合は、puppeteer.launchの引数にargsとして--window-size を指定するとよい。
const browser = await puppeteer.launch({
headless: headless,
slowMo: slowMo,
args: [`--window-size=945,1350`],
timeout: timeout,
});
ファイルのアップロード処理はそのままだと動作しない
例えば、「ファイルアップロード」というボタンを押してファイルを選択した場合。
この処理はRecorderからエクスポートした時点で以下のようなコードとして出力される。
{
const targetPage = page;
await puppeteer.Locator.race([
targetPage.locator('::-p-aria(ファイルアップロード:: sample.svg)'),
targetPage.locator('#fileInput'),
targetPage.locator('::-p-xpath(//*[@id=\\"fileInput\\"])'),
targetPage.locator(':scope >>> #fileInput')
])
.setTimeout(timeout)
.fill('C:\\fakepath\\sample.svg');
}
しかしこれはこのままだと動かない。ファイルダイアログを操作してファイルを選ぶ処理はブラウザ外のことで自動化ができないためだ。
この場合は、puppeteer側で用意されているuploadFileという関数を使って回避できる。
以下は使用例。
export const uploadFile = async (page: Page, selectors: string[], relativePath: string) => {
let inputElement: ElementHandle<HTMLInputElement> | null = null;
for (const selector of selectors) {
inputElement = (await page.$(selector)) as ElementHandle<HTMLInputElement>;
if (inputElement) break;
}
if (!inputElement) {
throw new Error('ファイルアップロード用のinputが見つかりませんでした');
}
const absolutePath = path.relative(process.cwd(), __dirname + relativePath);
await inputElement.uploadFile(absolutePath);
};
await uploadFile(
targetPage,
['#fileInput', 'input[name="image"]', 'input[type="file"]'], // input要素を特定するためのlocator
'/penguin.png' // アップロードするファイルのパス
);
ブラウザのダイアログ操作処理はそのままだと動作しない
alert() を使った場合に表示されるダイアログの操作も、エクスポートしたコードには含まれない。
回避策としては、ダイアログが出た場合の操作をするためのハンドラーをあらかじめ定義することになる。
以下、alertダイアログやconfirmダイアログが出た場合に自動的にOKを押したりするハンドラー定義。
page.on('dialog', async (dialog) => {
switch (dialog.type()) {
case 'alert':
case 'confirm':
await dialog.accept();
break;
case 'prompt':
await dialog.accept(promptInput);
break;
default:
await dialog.dismiss();
}
});
まとめ
- chrome x puppeteerを使うと、ブラウザの自動操作コードを書くときに面倒な部分を省力化できる
- chrome recorderは、小さな処理まで厳密に記録してしまうため、必要な操作のみを記録してから出力するとよい
- 出力したものをそのまま使おうとしても、特性上うまく動かない部分があるため注意は必要
- 出力コードのうち、基本的な処理(要素を探す、クリックなど)はそのまま使うとよい。特殊な処理(ブラウザの初期処理や、ダイアログ操作など)は書き換えが必要なことも多い
参考サイトなど
-
Puppeteer vs Selenium: Core Differences
PuppeteerとSeleniumの比較



