Puppeteerで途中でユーザに入力してもらうようにする
日本の情シスはMicrosoft365然り、G Suite然り、20年前の知識を持ってあらゆる制限を掛けることがお仕事と思ってる人達が居たりします。その結果として、各種APIが用意されているにも関わらず制限し、ユーザの業務効率化を妨げる悪の枢軸になってたりします。
さて、そんな制限が掛けられている場合に仕方ないのでウェブ自動化を使うのにPupppeteerを使って操作をするのが、業務効率化で残された手段の1つになります。今回は、outlook.comにログイン部分はユーザに行わせて、そこから先を自動化するといった手法を実現してみます。
リンク
今回使用するライブラリ等
今回はログイン部分はユーザが行うので、promptsは利用しません(サンプルのEXEでは使用しています)。
今回のウェブページアクセス上の問題点
Microsoft365のログインページは少々特殊な作りになっていて、今回ユーザにあえてパスワードの部分だけは入力させるために待たせるようにしています。
- メールアドレス入力⇒ボタンクリック⇒パスワードを入力して⇒ボタンクリックでログイン出来る仕様です(どちらもボタンのIDは同じ)
- パスワード欄だけはユーザが入力し、サインインを実行するまで、Puppeteerは待機させます。
- 無事にログインが完了したら、自動実行の続きを行います。
- Outlookからサインイン出来ても、別のURLを実行した場合セッションが切れるので、必ずクリックさせて移動させる必要があります。
- 最終的には予定表画面へと遷移させます。
図:パスワード画面で一時停止。ユーザの入力を待つ
ソースコード
冒頭部分
'use strict';
const puppeteer = require('puppeteer-core');
var fs = require('fs');
const path = require("path");
//デスクトップのパスを取得
var dir_home = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
var deskpath = require("path").join(dir_home, "Desktop");
//オープンするURL
var url = "https://outlook.live.com/owa/";
//メールアドレス
var mail = "ここにoutlookのメールアドレスを入れる";
//Chromeのパスを取得(ユーザ権限インストール時)
const userHome = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
var kiteipath = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe";
var temppath = path.join(userHome, "AppData\Local\Google\Chrome\Application\chrome.exe");
//chrome場所判定
if(fs.existsSync(kiteipath)){
var chromepath = kiteipath
console.log("プログラムフォルダにChromeみつかったよ");
}else{
if(fs.existsSync(temppath)){
var chromepath = temppath;
console.log("ユーザディレクトリにChrome見つかったよ");
}else{
console.log("chromeのインストールが必要です。");
//IEを起動してChromeのインストールを促す
shell('start "" "iexplore" "https://www.google.co.jp/chrome/"')
return;
}
}
- mail変数の部分にメールアドレスを事前に入力しておく必要があります。
- グローバル変数でデスクトップのパスを取得しておきます。
- つづけて、getprompt()を実行してユーザの入力を受付待ちします。
- chromeはいつもの「C:\Program Files (x86)\Google\Chrome\Application\chrome.exe」ではなく、「C:\Users\ユーザー名\AppData\Local\Google\Chrome\Application\chrome.exe」となるため、ユーザ毎のパスを取得して、chromepathに格納する
Puppeteer部分
//puppeteer実行
(async() => {
const browser = await puppeteer.launch({
headless: false,
executablePath: chromepath,
ignoreDefaultArgs: ["--guest",'--disable-extensions','--start-fullscreen','--incognito',],
slowMo:150,
});
//pageを定義
const page = await browser.newPage();
const navigationPromise = page.waitForNavigation()
//トップページを開く
await page.goto(url)
await page.setViewport({ width: 1200, height: 900 })
await navigationPromise
//ログインページに移動
await page.waitForSelector('.masthead > .masthead-shell > .primary-content > .titling > .product-name')
await page.click('.masthead > .masthead-shell > .primary-content > .titling > .product-name')
await page.waitForSelector('.bound > .auxiliary-actions > ul > li > .internal')
await page.click('.bound > .auxiliary-actions > ul > li > .internal')
await navigationPromise
//IDを入力して次へボタンを押す
await page.waitForSelector('.cb > div > #i0281 > .outer > .middle')
await page.click('.cb > div > #i0281 > .outer > .middle')
await page.type('div #i0116', mail)
await page.waitForSelector('.cb > div > #i0281 > .outer > .middle')
await page.click('.cb > div > #i0281 > .outer > .middle')
await page.waitForSelector('.row #idSIButton9')
await page.click('.row #idSIButton9')
//パスワード画面だけはユーザに入力させる
var result_input = await waitEvent(page);
//waitEventからresolveされた値を表示
console.log(result_input)
await navigationPromise
//予定表へ移動する
await page.waitForSelector('._3KAPMPOz8KsW24ooeUflK2 > ._2jR8Yc0t2ByBbcz_HIGqZ4 > ._1TpU2KF6f_EeQiytBaYj8I > ._3_hHr3kfEhbNYRFM5YJxH9 > ._23fxOotSm5HPNB1U_ZVw4i')
await page.click('._3KAPMPOz8KsW24ooeUflK2 > ._2jR8Yc0t2ByBbcz_HIGqZ4 > ._1TpU2KF6f_EeQiytBaYj8I > ._3_hHr3kfEhbNYRFM5YJxH9 > ._23fxOotSm5HPNB1U_ZVw4i')
await page.waitForSelector('#O365_NavHeader > #O365_HeaderLeftRegion > .Ki82nsa2AcSf6aUx3eZlf > #O365_MainLink_NavMenu > .ms-Icon--WaffleMicrosoft365')
await page.click('#O365_NavHeader > #O365_HeaderLeftRegion > .Ki82nsa2AcSf6aUx3eZlf > #O365_MainLink_NavMenu > .ms-Icon--WaffleMicrosoft365')
await page.waitForSelector('#appLauncherMainView > #appsModule #O365_AppTile_Calendar')
await page.click('#appLauncherMainView > #appsModule #O365_AppTile_Calendar')
await navigationPromise
})();
//入力イベント待ちをする関数
async function waitEvent(page){
return new Promise(async resolve=>{
//chromeに一時的な関数を作って送り込む(funcmanという名前にしました)
//何度も使い回す場合は、event名を重複しないようにする必要があります。
await page.exposeFunction("funcman",()=>{
//result_inputへ値を返す
resolve("ログインできた");
});
//ボタンにクリックすると一時的な関数を実行するイベントを割り当てるコードを実行
await page.evaluate(()=>{
document.getElementById("idSIButton9").addEventListener("click",()=>{
//ダミーのイベント
eval('window.funcman();');
});
});
});
}
- 普通にメアドだけこれまで通り入力させる
- パスワード画面で一時停止。ユーザにパスワードを入力してもらう。
- サインインのボタンのIDはChrome Developer Toolで探索するとidSIButton9がそれだとわかります。
- この時、サインインボタンにwaitEvent関数にて、exposeFunctionメソッドにてfuncmanという関数を作ってChromeに送り込み、サインインボタンにイベントを追加で割り当てます。
- サインインボタンをクリックすると、funcmanが実行されて、resolveにてresult_input変数へと返されます。
- その後、予定表ボタンをクリックして、カレンダーを表示して終了
- Microsoft365全般の操作や、Dynamics、他の作業から連動させてカレンダーにイベント登録、Teamsへの投稿、Sharepointへの投稿などに繋げられるのではないかと思います。本来はGraph APIで素早く実現できるんですが・・・・
- このコードは単発のPuppeteerで使うよりは、Electronなどのアプリに組み込んで使うと効果があると思います。
- Graph APIよりもOAuth2.0認証の実装をせずに済む点でもメリットがあると言えます。
- また、ID/PWは自動入力としても、変動する二段階認証の数値などを入力する画面などで活躍すると思います。
単一実行ファイルを作成する
Node.js 18よりSingle executable applicationsという機能が装備され、標準で単独実行ファイルが作成できるようになりました。結果pkgはプロジェクト終了となっています。よって、以下のエントリーの単一実行ファイルを作成するを参考に、Node18以降はexeファイルを作成することが可能です。

