2024年9月14日現在,起動させると「msxml3.dll: 指定されたリソースが見つかりません。」というエラーメッセージが表示されて正常に動作しません。
2024年9月26日,不具合の原因は msedgedriver ver.128 以降,これまで9515固定だったポート番号がランダムになったためと判明しました。この仕様変更に対応して正常動作するようになりました。
2024年9月27日,サイドバーおよび「自動テストソフトウェアによって制御されています」の表示を消すようにしました。
0. はじめに
会社の e-learning が面倒だ。ブラウザで数十枚のスライドを見た後でテストがあるので,スライドを飛ばして見ると後で後悔する。以前は,自作のプログラムでブラウザ画面をキャプチャしていたのだが,Windows10 以降キャプチャできなくなった。FireFox の画面はキャプチャできるけど Edge の画面はできないので,Edge は GPU を使ってるとか何とかのせいだろう・・・
ところが世の中には WebDriver というものがあることを知った。おそらく一般的な用途としてはブラウザ操作の自動化だと思うが,その中にブラウザ画面のスクリーンショットを取る機能があることを知り,これは使えないかと思った次第である。
1. 基本方針
対象ブラウザは Edge(Chromium版)である。Edge 用の WebDriver(msedgedriver)はマイクロソフト社謹製なので会社でも使えるが,Selenium は使用不可。なので WebDriver を直接操作しなくてはならない。
WebDriver とのデータのやり取りは JSON 形式で行われるので,JavaScript で組むのが良いだろう。
2. 課題
2.1 WebDriver とのやり取り
WebDriver とのやり取りには MSXML2.XMLHTTP
オブジェクトを用いる。データ送信はよく使うので共通部品を用意した。
//------------------------------------------------------------------------------
// HTTP クライアントの生成
//------------------------------------------------------------------------------
var client = WScript.CreateObject("MSXML2.XMLHTTP");
//------------------------------------------------------------------------------
// データ送信
//------------------------------------------------------------------------------
var send = function(method, addr, str) {
client.open(method, addr, false);
client.setRequestHeader("Content-Type", "application/json");
client.setRequestHeader("Pragma", "no-cache");
client.setRequestHeader("Cache-Control", "no-cache");
client.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");
client.send(str);
};
//------------------------------------------------------------------------------
// セッション ID の取得
//------------------------------------------------------------------------------
send("POST", "http://localhost:" + port_num + "/session", JSON_stringify({"capabilities":{}}));
var e = JSON_parse(client.responseText);
var id = e.value.sessionId;
2.2 IE6 では JSON が使えない
IE6 では JSON.stringify も JSON.parse も使えないのでサブセット(代替品)を自作した。
var JSON_parse = function(str) {
var e = null; e = eval("e = " + str + ";");
return e;
};
var JSON_stringify = function(val) {
if(val == null) {
return "null";
} else if(typeof(val) == "number" || typeof(val) == "boolean") {
return val.toString();
} else if(typeof(val) == "string") {
return "\"" + val + "\"";
} else if(typeof(val) == "object") {
if(val.length == undefined) {
var a = [];
for(var i in val)
a.push("\"" + i + "\":" + JSON_stringify(val[i]));
return "{" + a.join(",") + "}";
} else {
var a = [];
for(var i = 0; i < val.length; i++)
a.push(JSON_stringify(val[i]));
return "[" + a.join(",") + "]";
}
}
};
2.3 BASE64 デコードとバイナリ形式での保存
スクリーンショットは PNG 形式で格納され,それを BASE64 エンコードしたテキストとして受け取るようだ。BASE64 デコードは MSXML2.DOMDocument
を使う。またバイナリ形式でのファイル保存には ADODB.Stream
を使う。
//------------------------------------------------------------------------------
// ADODB.Stream オブジェクトの生成
//------------------------------------------------------------------------------
var stream = WScript.CreateObject("ADODB.Stream")
//------------------------------------------------------------------------------
// MSXML2.DOMDocument オブジェクトの生成
//------------------------------------------------------------------------------
var doc = WScript.CreateObject("MSXML2.DOMDocument");
var element = doc.createElement("base64");
send("GET", "http://localhost:" + port_num + "/session/" + id + "/screenshot", "");
var obj = JSON_parse(client.responseText);
element.dataType = "bin.base64";
element.text = obj.value;
stream.Type = 1; // adTypeBinary = 1
stream.Open();
stream.Write(element.nodeTypedValue)
stream.SaveToFile(filename, 2); // adSaveCreateOverWrite = 2
stream.Close();
3. 実装コード
ささっと作ったつもりだったが,エラー処理を入れたら結構長くなってしまった・・・
//------------------------------------------------------------------------------
// メイン関数
//------------------------------------------------------------------------------
function main() {
//--------------------------------------------------------------------------
// JSON 文字列の解析
//--------------------------------------------------------------------------
var JSON_parse = function(str) {
var e = null; e = eval("e = " + str + ";");
return e;
};
//--------------------------------------------------------------------------
// JSON オブジェクトの文字列化
//--------------------------------------------------------------------------
var JSON_stringify = function(val) {
if(val == null) {
return "null";
} else if(typeof(val) == "number" || typeof(val) == "boolean") {
return val.toString();
} else if(typeof(val) == "string") {
return "\"" + val + "\"";
} else if(typeof(val) == "object") {
if(val.length == undefined) {
var a = [];
for(var i in val)
a.push("\"" + i + "\":" + JSON_stringify(val[i]));
return "{" + a.join(",") + "}";
} else {
var a = [];
for(var i = 0; i < val.length; i++)
a.push( JSON_stringify(val[i]));
return "[" + a.join(",") + "]";
}
}
};
//--------------------------------------------------------------------------
// ポート番号
//--------------------------------------------------------------------------
var port_num = 9515;
//--------------------------------------------------------------------------
// ウェブドライバ名
//--------------------------------------------------------------------------
var driver_name = "msedgedriver.exe";
//--------------------------------------------------------------------------
// WScript.Shell オブジェクトの生成
//--------------------------------------------------------------------------
var shell = WScript.CreateObject("WScript.Shell");
//--------------------------------------------------------------------------
// 既にウェブ・ドライバが起動されている場合はそのまま利用する
// そうでない場合、新しいウィンドウを開いてウェブ・ドライバを起動する
//--------------------------------------------------------------------------
(function(){
var locator = WScript.CreateObject("WbemScripting.SWbemLocator");
var service = locator.ConnectServer();
var list = service.ExecQuery("Select * From Win32_Process Where Name = '" + driver_name + "'");
if(list.Count == 0) shell.Run(driver_name + " --port=" + port_num);
})();
//--------------------------------------------------------------------------
// ADODB.Stream オブジェクトの生成
//--------------------------------------------------------------------------
var stream = WScript.CreateObject("ADODB.Stream")
//--------------------------------------------------------------------------
// MSXML2.DOMDocument オブジェクトの生成
//--------------------------------------------------------------------------
var element = (function(){
var doc = WScript.CreateObject("MSXML2.DOMDocument");
return doc.createElement("base64");
})();
//--------------------------------------------------------------------------
// HTTP クライアントの生成
//--------------------------------------------------------------------------
var client = WScript.CreateObject("MSXML2.XMLHTTP");
var send = function(method, addr, str) {
client.open(method, addr, false);
client.setRequestHeader("Content-Type", "application/json");
client.setRequestHeader("Pragma", "no-cache");
client.setRequestHeader("Cache-Control", "no-cache");
client.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");
client.send(str);
};
//--------------------------------------------------------------------------
// セッションIDの取得
//--------------------------------------------------------------------------
send("POST", "http://localhost:" + port_num + "/session", JSON_stringify({
"capabilities":{
"alwaysMatch":{
"ms:edgeOptions":{
"excludeSwitches":["enable-automation"],
"args":["--enable-features=msEdgeTowerAutoHide"]
}
}
}
}));
if(client.Status != 200) {
WScript.StdErr.WriteLine("セッションIDの取得に失敗しました!!");
return -1;
}
var e = JSON_parse(client.responseText);
if(!e.value || !e.value.sessionId) {
WScript.StdErr.WriteLine("セッションIDの取得に失敗しました!!");
return -1;
}
var id = e.value.sessionId;
if(e.value && e.value.capabilities) {
var f = e.value.capabilities;
if(f.browserName && f.browserVersion)
WScript.StdErr.WriteLine(f.browserName + ":" + f.browserVersion);
if(f.msedge && f.msedge.msedgedriverVersion)
WScript.StdErr.WriteLine(driver_name + ":" + f.msedge.msedgedriverVersion);
}
//--------------------------------------------------------------------------
// スクリーンショットをとる
//--------------------------------------------------------------------------
var count = 0;
for(;;) {
//----------------------------------------------------------------------
// キー入力
//----------------------------------------------------------------------
WScript.StdErr.Write("SCREEN SHOT(ENTER)/WINDOW LIST(W)/WINDOW SELECT(0-9)/QUIT(Q):");
var key = WScript.StdIn.ReadLine();
if(key == "Q" || key == "q") {
//------------------------------------------------------------------
// 終了
//------------------------------------------------------------------
WScript.StdErr.WriteLine("終了しました。");
break;
} else if(key == "W" || key == "w") {
//------------------------------------------------------------------
// ウィンドウリストを表示する
//------------------------------------------------------------------
if(!(function(){
send("GET", "http://localhost:" + port_num + "/session/" + id + "/window/handles", "");
if(client.Status != 200) {
WScript.StdErr.WriteLine("ウィンドウハンドルの取得に失敗しました!!");
return false;
}
var obj = JSON_parse(client.responseText);
if(!obj.value || !obj.value.length) {
WScript.StdErr.WriteLine("ウィンドウハンドルの取得に失敗しました!!");
return false;
}
for(var i = 0; i < obj.value.length; i++)
WScript.StdErr.WriteLine("[" + i + "] " + obj.value[i]);
return true;
})() ) break;
} else if(/^[0-9]+$/.test(key)) {
//------------------------------------------------------------------
// ウィンドウを選択する
//------------------------------------------------------------------
if(!(function(){
send("GET", "http://localhost:" + port_num + "/session/" + id + "/window/handles", "");
if(client.Status != 200) {
WScript.StdErr.WriteLine("ウィンドウハンドルの取得に失敗しました!!");
return false;
}
var obj = JSON_parse(client.responseText);
if(!obj.value || !obj.value.length) {
WScript.StdErr.WriteLine("ウィンドウハンドルの取得に失敗しました!!");
return false;
}
var n = parseInt(key);
if(n < 0 || n >= obj.value.length) {
WScript.StdErr.WriteLine("不正なウィンドウを指定しました!!");
return true;
}
send("POST", "http://localhost:" + port_num + "/session/" + id + "/window",
JSON_stringify({ "handle": obj.value[n] }));
if(client.Status != 200) {
WScript.StdErr.WriteLine("ウィンドウの選択に失敗しました!!");
return false;
}
WScript.StdErr.WriteLine("ウィンドウ [" + n + "] を選択しました。");
return true;
})()) break;
} else if(key == "") {
//------------------------------------------------------------------
// スクリーンショットを取得する
//------------------------------------------------------------------
if(!(function(){
send("GET", "http://localhost:" + port_num + "/session/" + id + "/screenshot", "");
if(client.Status != 200) {
WScript.StdErr.WriteLine("スクリーンショットの取得に失敗しました!!");
return false;
}
var obj = JSON_parse(client.responseText);
if(!obj.value || obj.value.length == 0 || obj.value.length % 4 != 0) {
WScript.StdErr.WriteLine("スクリーンショットの取得に失敗しました!!");
return false;
}
//--------------------------------------------------------------
// BASE64 デコードして保存する
//--------------------------------------------------------------
var filename = ("0000" + count).slice(-4) + ".PNG";
element.dataType = "bin.base64";
element.text = obj.value;
stream.Type = 1; // adTypeBinary = 1
stream.Open();
stream.Write(element.nodeTypedValue)
stream.SaveToFile(filename, 2); // adSaveCreateOverWrite = 2
stream.Close();
WScript.StdErr.WriteLine(filename + " ... 保存しました。")
count++;
return true;
})()) break;
} else {
WScript.StdErr.WriteLine(key + " は不正なコマンドです!!");
}
}
//--------------------------------------------------------------------------
// ウェブ・ブラウザを閉じる
//--------------------------------------------------------------------------
send("DELETE", "http://localhost:" + port_num + "/session/" + id, "");
return 0;
}
//------------------------------------------------------------------------------
// メイン関数の呼び出し
//------------------------------------------------------------------------------
var ret = main(WScript.Arguments.Unnamed);
WScript.Quit(ret);
4. 実行画面
コマンドラインから起動すると Edge と WebDriver のバージョンを表示する。ENTER キーを押すとスクリーンショットを取る。ファイル名は 0000.PNG
, 0001.PNG
, 0002.PNG
... と四桁の連番とする。ウィンドウ(タブ)が複数ある場合のため,ウィンドウ(タブ)の一覧機能と選択機能を追加した。
MicrosoftEdge:129.0.2792.52
msedgedriver.exe:129.0.2792.52 (6462e8fc16ec8b62bef06801adff3cbeccd6a7d4)
SCREEN SHOT(ENTER)/WINDOW LIST(W)/WINDOW SELECT(0-9)/QUIT(Q):
5. 余談
ちなみに職場の同僚に尋ねたら,ブラウザを二つ開いてスライド見ながらテストを解いているとのこと。