VBScriptのIE操作をPuppeteerのChrome操作へ移行する例
かれこれもう3年半以上前になりますが、「WSH(VBScript)でブラウザを自動操作する」という記事を投稿しました。
しかしこの度、2022年6月15日でIEのサポートが終了し、今後はEdgeが自動的に起動するようになるそうで、VBScriptでIEの自動操作をしているプログラムは動作しなくなってしまいます。
(なぜか6月17日現在ではまだIEの自動操作が出来ていますが、まぁ近い将来動かなくなるでしょう…たぶん…)
ということで、今回はむかーし作ったVBScriptをNodejsのJavaScriptに書き換えていこうと思います。尚、NodejsとPuppeteerのインストール方法については、「puppeteerでAmazonのレポートページに自動ログインしてSSを撮る」で詳しく解説しているため、そちらをどうぞ。
ざっくり言うと、Nodejsはダウンロードしてインストールボタンをポチポチするだけですし、Puppeteerのインストールも任意のディレクトリを作って npm install puppeteer というコマンドを実行するだけなので、たぶん想像するよりもずっと楽ちんですよ。
過去の遺産:VBScriptによるIE自動操作
実行内容としてはValue Serverの管理ページへログインし、SSH登録というボタンをクリックする簡単なスクリプトになります。
何でこんなものを作ったかというと、Value Serverの場合、管理ページで[SSH登録]というボタンを押して、自分のIPアドレスを登録しないとSSH接続できないんですよ。しかもIPアドレスって(普通のプロバイダーなら)数日に1回は変わっちゃうじゃないですか。
毎日SFTP(SSH File Transfer Protocol)で接続して、Webサイトのバックアップを取る処理も自動化してあるので、定期的にSSH登録ボタンをぽちぽちしないといけないんですね。じゃあこれも自動化しちゃおう、という結論に至って作ったわけです。
'******************************************************************************
'* VALUE SERVER SSH登録の自動処理
'*
'******************************************************************************
user = "ユーザー名"
pass = "パスワード"
url = "https://e1.valueserver.jp"
Set IE = CreateObject("InternetExplorer.Application")
IE.Visible = True
Call ValueServerSSH(url, user, pass)
IE.Quit
'************************************************
'* VALUE SERVERのコンパネへログインしSSH用のIPを登録する
'************************************************
Sub ValueServerSSH(url, user, pass)
IE.Navigate url & "/cp/admin.cgi"
Call WaitIE(IE)
'//ログイン情報入力
IE.document.getElementsByName("id")(5).value = user
IE.document.getElementsByName("pass")(5).value = pass
'//ログインボタンクリック
IE.document.getElementsByName("explain")(2).Click
Call WaitIE(IE)
'//左メニュー[お役立ちツール]-[SSH接続]をクリックした時のURLへ移動
IE.Navigate url & "/cp/admin.cgi?telnet=1"
Call WaitIE(IE)
'//SSH登録ボタンをクリック
IE.document.getElementsByName("ssh2")(0).Click
Call WaitIE(IE)
'//ログアウトボタンをクリック
IE.document.getElementsByName("clearcookie")(0).Click
Call WaitIE(IE)
End Sub
'************************************************
'* IEのページがロードされるまで待つ関数
'************************************************
Sub WaitIE(IE)
Do While IE.Busy Or IE.ReadyState <> 4
WScript.Sleep(1000)
Loop
End Sub
Puppeteerに置き換えるとこうなる
あれこれ解説する前にとっととサンプル出します。
const puppeteer = require('puppeteer');
const url = 'https://e1.valueserver.jp/'; //URL;
const user_id = 'ユーザー名';
const user_pw = 'パスワード';
(async function() {
//ブラウザ起動
const browser = await puppeteer.launch({
headless:false,
defaultViewport:null,
args:['--window-size:1200,800']
});
const page = await browser.newPage();
//ログイン画面
await page.goto(url + 'cp/admin.cgi', { waitUntil:'networkidle2' });
//IDとパスワードの入力
await page.type("#topLogin input[name=id]", user_id);
await page.type("#topLogin input[name=pass]", user_pw);
//ログインボタンクリック
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle2'}),
page.click('.home_on')
]);
//[お役立ちツール]の[SSH接続]ページを開く
await page.goto(url + 'cp/admin.cgi?telnet=1', { waitUnti:'networkidle2' });
//[SSH登録]ボタンをクリック
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle2'}),
page.click('input[name=ssh2]')
]);
//[ログアウト]ボタンをクリック
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle2'}),
page.click('input[name=clearcookie]')
]);
await browser.close();
})();
解説
Puppeteerは非同期で動作するため、async関数内から呼ぶ必要があり、async function () { ~ } という形で書きます。よくわからなければおまじないと思っておいても良いですが、同期処理/非同期処理の違いがわからないとこの先必ず詰むので、理解しておいたほうが良いです。
ブラウザの起動
const browser = await puppeteer.launch({
headless:false,
defaultViewport:null,
args:['--window-size:1200,800']
});
puppeteer.launchでブラウザの起動をしますが、その際、起動オプションを指定できます。
- headless: ヘッドレスモード※で動くか否か。動作を見たいのでfalse(=ヘッドレスモードではない)
- defaultViewport: ビューポートの指定。たぶんデフォルトでは800x600固定なので、あえてnullを指定することでウィンドウサイズと同じビューポートに設定する
- args: Chromeに渡す起動オプション。上2つはPuppeteerに対するオプションであり、ここに書くのはChromeブラウザのオプション。'--window-size' は名前のままウィンドウサイズ。他にも '--incognito' を付けてシークレットモードで起動することも可能。
ページ遷移
await page.goto(url + 'cp/admin.cgi', { waitUntil:'networkidle2' });
VBScriptで言うところのNavigateメソッドが、Puppeteerではgotoメソッドになります。
VBScriptではIEのReadyStateを見て4(データの受信完了)になるまで、Sleepを挟んでぐるぐるループするという石器時代のような書き方でしたが、Puppeteerではgotoメソッドに「どこまで待つか」の指定が可能です。waitUntilの部分ですね。
load | ページの読み込み完了 |
domcontentloaded | HTMLの解析完了 |
networkidle0 | ネットワーク接続が0件になってから500ミリ秒経過 |
networkidle2 | ネットワーク接続が2件以下になってから500ミリ秒経過 |
よほど急いで処理しなければならない事情でもない限りはnetworkidle2で良いんじゃないかなぁ。
なぜなら、昨今は少しでもページを軽く見せるために、見えている範囲だけ描画して、残りはJavaScriptでゆっくりとロードするというWebサイトが増えているからです。
そのため、ネットワーク接続がなくなるまで待ったほうが良い…………ようにも思えますが、例えば広告を表示しているサイトなど、数十秒ごとに新しい広告を読み込んで、永遠にネットワーク接続が切れないサイトなどもあります。
そんなサイトを waitUntil:networkidle0 で読み込むと一生終わらないので、networkidle2あたりが無難かなぁ、というところ。相手のJavaScriptの実行なんて待たなくていいわ、というならdomcontentloadedでも良いです。デフォルトはたぶんload。
入力フォームへの文字入力
//IDとパスワードの入力
await page.type("#topLogin input[name=id]", user_id);
await page.type("#topLogin input[name=pass]", user_pw);
VBScriptでは↓こんな書き方をしていた部分ですね。
IE.document.getElementsByName("id")(5).value = user
IE.document.getElementsByName("pass")(5).value = pass
Puppeteerではセレクタ指定できるので、CSSに詳しい方ならパッと見でもわかるんじゃないかなぁ。
#topLogin input[name=id] は「ID属性が"topLogin"の要素の中にあるINPUTタグのname属性が"id"の要素」となります。
ややこしいわ!って言いたいんですけど、こればかりはValue Serverの管理ページをデザインした人に文句言ってもらうしかないです。
VBScriptでIEを操作していた際はセレクタ指定なんてオシャンティなやり方がなかったので、getElementsByName("id")(5)という、「name属性に"id"が付いている要素の中で5番目」などという泥臭い指定方法をしていました。
…いやだって、HTMLソースを見ると<input name="id">という要素が5つもあるんですよ。ホンモノは最後のだけ。もしかして、何等かのアタック対策なのだろうか…?まさかなぁ…。
同じような書き方をしても良かったのですが、せっかくセレクタ指定できるので、今回は #topLogin input[name=id] という書き方にしました。
でも、たぶん、どうしてもVBScriptと同じような書き方がしたい!というケースもあると思うので、一例だけ出しておきます。
//IDとパスワードの入力
await page.evaluate((user_id,user_pw) => {
document.getElementsByName('id')[5].value = user_id;
document.getElementsByName('pass')[5].value = user_pw;
}, user_id, user_pw);
この書き方でも同じことが行えます。
Puppeteerでは基本的にChromeブラウザに直接命令を送りながら自動操縦するわけですが、evaluateメソッドを使うことで、ブラウザにJavaScriptを送り込んで実行させることができます。
つまり、NodejsのJavaScriptではなく、ブラウザ内にJavaScriptを書いたのと同じ動作をさせるんですね。
だから、上の例のように、見慣れた getElementsByName なんて書き方が出来ます。
ただ、変数のスコープが異なるので、user_id/user_pwというグローバル変数は直接参照はできず、evaluateメソッドに引数を渡すことでIDとパスワードの受け渡しをしています。
わかってしまえば何てことないんですけど、パッと見だと括弧が多くて見づらいでしょ。それにPuppeteer側で直接操作したほうがwaitUntilの例のように融通がきくので、今回は採用しませんでした。
ボタンクリックとページ遷移の待ち方
//ログインボタンクリック
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle2'}),
page.click('.home_on')
]);
ログインボタンに限らず、何かボタンを押してからページ遷移されるまで待つ、という処理が必要な場面って多いですよね。
頭の中で考えるのとちょっと逆っぽい書き方に見えるかもですが、Puppeteerでは上の例のようにwaitForNavigationを指定してからclickを実行するのがセオリーです。
というのも、waitForNavigationは、この関数が呼び出されてから次のページ遷移が起こるまで待つ、という処理なので、仮に
await page.click('.home_on');
await page.waitForNavigation({waitUntil: 'networkidle2'});
こんな書き方をしたとしたら、waitForNavigationに来た時点でページ遷移が終わってしまっているので、一生(といってもデフォルトでは30秒)戻ってきません。
それなら、ってことで click をawaitで待たない例を出しているサンプルもあります。
(ぼくも以前のサンプルではこの書き方をしていました)
page.click('.home_on');
await page.waitForNavigation({waitUntil: 'networkidle2'});
これは…大抵の場面でちゃんと動きます。クリック処理をしなさいよーという命令を送ったその直後にすぐwaitForNavigationが動くからですが… 完璧ではありません。waitForNavigationが呼び出される前に超速でページ遷移が終わってしまう可能性が、なくもなくもないからです。
なので、正しくは、最初に書いたとおり、Promise.all() で同時並列実行させてあげるのが良い……………らしいです。
ぶっちゃけ、Puppeteerのドキュメントにこう書けよってあったのをそのまま書いてるだけですw
https://github.com/puppeteer/puppeteer/blob/v1.20.0/docs/api.md#pagewaitfornavigationoptionsブラウザの終了
await browser.close();
最後はブラウザを閉じて終了。closeしなければブラウザは残ったままになるので、デバッグ中などはコメントアウトしておくとわかりやすくて良いでしょう。
まとめ
「puppeteerでAmazonのレポートページに自動ログインしてSSを撮る」で、Puppeteerの使い方についてはけっこう詳しく解説したつもりですが、IE終了に伴い、VBScriptで書いていたこの部分はどう書くんだ?みたいな疑問が出てくる人もいるかな~ということで、昔VBScriptで書いたスクリプトをNodejsに移行してみました。
いや逆ですね。実際にこのスクリプトはぼくのWeb運営で使っているので、VBScriptからNodejsへ移行したついでに、記事にしてみた感じです。
以上、何かの参考にでもなれば幸いです。