前記事「大量商用ページの表示結果を比較するために試行錯誤した」にて、Node.js+Puppeteer+Looks-same を使った サイトキャプチャー比較について投稿しました。
ここでは、人間の目で見ることを重視した「大量商用ページの表示結果を比較する」別手法について説明したいと思います。
ステージングサーバー、本番サーバーの間でページ表示結果が一致しているか心配だから
確認してみたい!という「とあるお方の」ご要望を受けまして少し調べてみたり試行して
みたりしました。
ということで、Headless-chromeを使ったキャプチャー比較に取り組んでみたのですが、「とあるお方」より IE11 の利用者が一番多い+やはり人間の目で見て欲しいなぁ!というご要望を受けての再チャレンジです。
先に謝っておきますが
この記事も「わかる人ならわかる」という乱暴な纏め方になっています。すいません。
2021/10/29 の記事変更点
久しぶりに本記事を閲覧したところ、ESETが「トロイの木馬」だと怒ります。
どうやら特定のキーワードに反応して「トロイ」と誤判定する模様です。
面白いので何に反応しているのか探し「トロイ」と言われないようにしてみました。
private static extern IntPtr Set_WindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
上の Set_Win~のところは、本当は Set と Win の間に _ がないのですが、トロイのキーワードに該当させないように _ を入れてみました。コードを参照される方はご注意ください( _ は取ってくださいね)
新たな目標
・IE11 へサイトページ(A)とサイトページ(Aダッシュ)を表示します。
・表示するためにいちいち手打ちはしない。(何せ大量にあるからね。間違うよね)
・これを人間の目で差分有無を確認したい。(これがご要望だからね)
・しかも人間側の疲労は極限まで落としたい。(目が疲れるからね)
・しかーも、素早く確認していきたい。(大量にあって手と目が・・・略)
・しかーも、しかーも、縦に長いページが多いので、サイトページ(A)とサイトページ(Aダッシュ)を表示しているウインドウのスクロール位置の調整はうまく調整してあげてね
・2ウインドウのスクロール調整を手でやるのはいらいらして・・これなんとかしてあげてね。
目標をより具体化
以下のことをしてくれるツールがあったらいいね。
マウスは極力使わず、キーボードだけで操作を完結させると手の動きが少なくて楽だね。
・何か自動化してくれるツールを起動すると、ツールがブラウザ(IE11)のURL欄に確認したいサイトのURLを貼り付けてくれる。
・何か自動化してくれるツールがウインドウ(A)ウインドウ(Aダッシュ)のスクロール位置をぴったり同じにしてくれる。
・キーボードの特定キーを押すとツールでキーボード検知して(A)(Aダッシュ)のスクロール位置が同じ高さだけ移動させてくれる。
・ウインドウ(A)、ウインドウ(B)は最大表示にしておく。
・ウインドウ(A)、ウインドウ(B)の2つはタブで表示切替してあげると、交互に目に飛び込んでくるので、目で違うところ(ちかちかするよ)を見つける(ここは人間の目)!
・キーボードの特定キーを押すと、次の確認対象ページを自動で表示してくれる。
キーボード監視
キーが押されたことを検知する仕組みを探った。
すったもんだしたが、結局、PowerShellコマンドを使うことにした。
キーボード仕様
開始するとき: Ctl+G
処理終わるとき : ESCAPE
次のページを表示: Ctl+Right
前のページを表示: Ctl+Left
下へ一定量スクロール: Ctl+Down
上へ一定量スクロール: Ctl+Up
※ひたすらキーボードを触って動かしていく仕様です!
効果
大量にあるページURLを手でブラウザに貼り付ける必要がない(とても楽)。
ページ切替え、上下のスクロール、などなど、全ての操作はキーボードだけで完結して手を大きく動かく必要がない(とても楽)。
比較対象のウインドウ2個に同じページが表示されて、上下の位置が同じになるのでひたすらタブでウインドウ表示を切替えながら目で違いを追っていくだけ(ここは目でみないといけないけれどこれがご要望だから、この作業だけに集中したい!=結果集中できた!)
以下全コード。
powershell -Sta -ExecutionPolicy Bypass -file .\keyOperator.ps1
pause
Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace KeyLogger {
public static class Program {
private static bool keyCtrl = false;
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x101;
//private const string logFileName = "log.txt";
//private static StreamWriter logFile;
private static HookProc hookProc = HookCallback;
private static IntPtr hookId = IntPtr.Zero;
public static int Main() {
//logFile = File.AppendText(logFileName);
//logFile.AutoFlush = true;
hookId = SetHook(hookProc);
Application.Run();
//Console.WriteLine("Application.Run() EXIT("+Environment.ExitCode+")");
UnhookWindowsHookEx(hookId);
return Environment.ExitCode;
}
private static IntPtr SetHook(HookProc hookProc) {
IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, moduleHandle, 0);
}
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
/*
Press =[65],[A]
Press =[162],[LControlKey]
Press =[160],[LShiftKey]
Press =[81],[Q]
Press =[38],[Up]
Press =[39],[Right]
Press =[40],[Down]
Press =[37],[Left]
Press =[71],[G]
Press =[79],[O]
Press =[27],[Escape]
*/
private const int GO = 71;
private const int ESCAPE = 27;
private const int LControlKey = 162;
private const int LEFT = 37;
private const int UP = 38;
private const int RIGHT = 39;
private const int DOWN = 40;
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP){
int vkCode = Marshal.ReadInt32(lParam);
//Console.WriteLine("RELEASE =["+vkCode+"],["+(Keys)vkCode+"]");
if( vkCode == LControlKey ){
//Console.WriteLine("RELEASE CTRL");
keyCtrl = false;
}
}
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
int vkCode = Marshal.ReadInt32(lParam);
//string key = (string)((Keys)vkCode);
//Console.WriteLine("Press =["+vkCode+"],["+(Keys)vkCode+"]");
if( vkCode == ESCAPE ){
//Console.WriteLine("ESCAPE");
Environment.ExitCode = 99;
Application.Exit();
}else if( vkCode == LControlKey ){
//Console.WriteLine("PRESS CTRL");
keyCtrl = true;
}else if( keyCtrl && vkCode == UP ){
//Console.WriteLine("PRESS UP");
keyCtrl = false;
Environment.ExitCode = 110;
Application.Exit();
}else if( keyCtrl && vkCode == DOWN ){
//Console.WriteLine("PRESS DOWN");
keyCtrl = false;
Environment.ExitCode = 120;
Application.Exit();
}else if( keyCtrl && vkCode == RIGHT ){
//Console.WriteLine("PRESS RIGHT");
keyCtrl = false;
Environment.ExitCode = 20;
Application.Exit();
}else if( keyCtrl && vkCode == LEFT ){
//Console.WriteLine("PRESS LEFT");
keyCtrl = false;
Environment.ExitCode = 30;
Application.Exit();
}else if( keyCtrl && vkCode == GO ){
//Console.WriteLine("PRESS GO");
keyCtrl = false;
Environment.ExitCode = 10;
Application.Exit();
}
//logFile.WriteLine((Keys)vkCode);
}
return CallNextHookEx(hookId, nCode, wParam, lParam);
}
[DllImport("user32.dll")]
private static extern IntPtr Set_WindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
}
}
"@ -ReferencedAssemblies System.Windows.Forms
# VERSION
$VERSION = "1.0.0";
Write-Host "##########################";
Write-Host "# VERSION=$VERSION";
Write-Host "##########################";
$loop = $true;
$ieStaging = $null;
$ieProduct = $null;
$xmlElements = $null;
$kmjToolsPath = [System.IO.Directory]::GetCurrentDirectory();
echo "kmjToolsPath=[$kmjToolsPath]";
#Write-Host ("kmjToolsPath=" + $kmjToolsPath);
#$shell = New-Object -ComObject Shell.Application
$objNotifyIcon = New-Object System.Windows.Forms.NotifyIcon ;
function Baloon($title, $msg) {
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");
$objNotifyIcon.Icon = "$kmjToolsPath\cloudy.ico";
$objNotifyIcon.BalloonTipText = ":"+$msg;
$objNotifyIcon.BalloonTipTitle = "($VERSION) $title";
$objNotifyIcon.Visible = $True ;
$objNotifyIcon.ShowBalloonTip(500); # 500msec
}
# ログの出力先
$LogPath = "$kmjToolsPath\log";
# 現在時刻
$Now = Get-Date;
$currentTime1 = $Now.ToString("yyyyMMddHHmmss");
$currentTime2 = $Now.ToString("yyyy/MM/dd HH:mm:ss");
# ログファイル名
$LogName = "\executLog";
$LogName2 = "$currentTime1.txt"
$LogFilePath = "$LogPath$LogName";
$operatorName = "default";
# ログフォルダーがなかったら作成
if( -not (Test-Path $LogPath) ) {
New-Item $LogPath -Type Directory | Out-Null
}
$counter = -1;
function logger($msg){
$LogFilePath = "$LogPath$LogName"+"_"+"$operatorName"+"_"+$LogName2;
Write-Host $LogFilePath;
Write-output "###[$currentTime2]###[$operatorName]" | Out-File -FilePath $LogFilePath -Encoding Default -append
Write-Output "$msg" | Out-File -FilePath $LogFilePath -Encoding Default -append
Write-Host $msg;
}
function getURL($index,$type){
$size = $xmlElements.ChildNodes.Count;
$element = $xmlElements.ChildNodes[$index];
if($type -eq "staging"){
return $element.staging.url;
}else{
return $element.product.url;
}
}
function getStagingURL($index){
return getURL $index "staging"
}
function getProductURL($index){
return getURL $index "product"
}
function getBeforeCounter($counter){
$_counter = $counter;
$_counter -= 1;
if($_counter -lt 0){
$_counter = 0;
}
return $_counter;
}
function getNextCounter($counter){
$_counter = $counter;
$size = $xmlElements.ChildNodes.Count;
if($_counter -lt 0){
$_counter = 0;
}else{
$_counter += 1;
}
if($_counter -ge ($size -1)){
$_counter = $size -1;
}
#Write-Host "_counter=$_counter";
return $_counter;
}
function getNo($idex){
$element = $xmlElements.ChildNodes[$idex];
return $element.staging.no;
}
function kickIE(){
$ie = New-Object -ComObject InternetExplorer.Application
$dll_info = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
Add-Type -MemberDefinition $dll_info -Name NativeMethods -Namespace Win32
[Win32.NativeMethods]::ShowWindowAsync($ie.HWND, 3) | Out-Null
return $ie;
}
function getIE($hwnd){
$ie = $null;
.{
$ie = $(New-Object -ComObject "Shell.Application").Windows() | ? {$_.HWND -eq $hwnd};
return;
}| Out-Null
return $ie;
}
function ieNavigation($hwnd, $url){
$ie = getIE $hwnd;
try{
$ie.Navigate($url);
return $true;
}catch{
Baloon "--WARNING!--" "Check IE Basic Authentication!";
return $false;
}
# return $ie;
}
#function ieScroll( $scroll, $updown, $ie){
function ieScroll($hwnd, $scroll, $updown){
#Write-Host "scroll---$scroll";
$ie = getIE $hwnd;
# Scroll
if($updown -eq "DOWN"){
$scroll += 600;
}else{
$scroll -= 600;
if($scroll -lt 0){
$scroll = 0;
}
}
$ie.Document.parentWindow.scrollTo(0, $scroll) | Out-Null;
return $scroll;
}
function ieQuit($ie){
$ie.Quit() | Out-Null;;
}
$scrollP = 0;
#--- INPUT OPERATOR NAME ------#
$operatorName = "";
$operatorNameMsg = "STEP01: INPUT YOUR NAME ! (ex Igawa Seido )";
$yesNo = "n";
while( $yesNo -ne "y" ){
$operatorName =Read-Host $operatorNameMsg;
$yesNo = Read-Host "OperatorName = [$operatorName]--> Yes(y) / No (n) ";
}
$dataXmlFile = "\data\data";
$dataXmlPath = "";
#--- GET OPARATION XML NO -----#
$oprationUnitMsg = "STEP01: INPUT CHECK UNIT NO! = ";
$unitNo = "";
$yesNo = "n";
while( $yesNo -ne "y"){
$unitNo = Read-Host $oprationUnitMsg;
$dataXmlPath = "$kmjToolsPath$dataXmlFile"+"_$unitNo.xml";
$yesNo = Read-Host "STEP02: UnitNo = [$unitNo]--> Yes(y) / No (n) ";
if($yesNo -eq "y"){
if( Test-Path $dataXmlPath ){
break;
}
$yesNo = 'n';
Write-Host "XML is not exists![$dataXmlPath].";
Write-Host "Input correct UnitNo!";
}
}
#--- GET URL List ----#
$XML = [XML](Get-Content $dataXmlPath);
$xmlElements = $XML.elements;
# Read User's Order
$initCheck = "0";
$restartCheck ="1";
$exitCheck ="q";
$option = $null;
while($option -eq $null){
$msg = "OPTION [$initCheck :START, $exitCheck :CANCEL]";
$_option =Read-Host $msg;
if($_option -eq $initCheck){
$option = $_option;
## }elseif($_option -eq $restartCheck){
## $option = $_option;
}elseif($_option -eq ""){
$option = $initCheck;
}elseif($_option -eq $exitCheck){
Write-Host ("#### CANCELED BY USER ####");
exit;
}
}
#--- GET LAST CHECK POINT ---#
$LAST_XML = $null;
$lastCheckPointXML = "\result\lastCheckPoint.xml";
$LAST_XML = [XML](Get-Content $kmjToolsPath$lastCheckPointXML);
if($option -eq $restartCheck){
$lastNo = $LAST_XML.last.number;
if ([int]::TryParse($lastNo,[ref]$null)) {
$counter = ([int]$lastNo);
}else{
$counter = -1;
}
}
while($loop){
$rtn = [KeyLogger.Program]::Main();
if($rtn -eq 99 ){
Write-Host "#### OWARIMASU ####";
#--- Write LAST_XML
$LAST_XML.last.number = [string]$counter;
$LAST_XML.Save($kmjToolsPath+$lastCheckPointXML)
if($ieStaging -ne $null) {
ieQuit $ieStaging;
$ieStaging = $null;
}
if($ieProduct -ne $null) {
ieQuit $ieProduct;
$ieProduct = $null;
}
$loop = $false;
}elseif($rtn -eq 10){ # GO
Write-Host "#### GO ####";
$ieStaging=kickIE;
$ieProduct=kickIE;
$scroll = 0;
if($option -eq $restartCheck){
Write-Host "restart counter=$counter";
$url = getStagingURL $counter;
ieNavigation $ieStaging.HWND $url;
$url = getProductURL $counter;
ieNavigation $ieProduct.HWND $url;
$option = $null;
$currentNo = getNo($counter);
logger ("NO = [$currentNo], Name=[$operatorName]");
logger ("StagingUrl = ["+(getStagingURL $counter)+"]");
logger ("ProductUrl = ["+(getProductURL $counter)+"]");
$balMsg = "NO = [$currentNo]";
Baloon "NEXT PAGE" $balMsg;
}
}elseif($rtn -eq 20){ # RIGHT
Write-Host("NEXT PAGE");
if( $ieStaging -eq $null ) {
$ieStaging = kickIE;
}
if( $ieProduct -eq $null) {
$ieProduct = kickIE;
}
if($option -eq $restartCheck){
$url = getStagingURL $counter;
ieNavigation $ieStaging.HWND $url;
$url = getProductURL $counter;
ieNavigation $ieProduct.HWND $url;
$option = $null;
}else{
$counter = getNextCounter $counter;
$url = getStagingURL $counter;
ieNavigation $ieStaging.HWND $url;
$url = getProductURL $counter;
ieNavigation $ieProduct.HWND $url;
}
$currentNo = getNo($counter);
# Write-Host("NO = [$currentNo], Name=[$operatorName]");
# Write-Host("StagingUrl = ["+(getStagingURL $counter)+"]");
# Write-Host("ProductUrl = ["+(getProductURL $counter)+"]");
logger ("NO = [$currentNo], Name=[$operatorName]");
$stagingUrl = getStagingURL $counter;
logger ("StagingUrl = [$stagingUrl]");
logger ("ProductUrl = ["+(getProductURL $counter)+"]");
$balMsg = "NO = [$currentNo]";
Baloon "NEXT PAGE" $balMsg ;
$scroll = 0;
}elseif($rtn -eq 30){ # LEFT
Write-Host("PREVIOUS PAGE");
if( $ieStaging -eq $null) {
$ieStaging = kickIE;
}
if( $ieProduct -eq $null) {
$ieProduct = kickIE;
}
if($option -eq $restartCheck){
$url = getStagingURL $counter;
ieNavigation $ieStaging.HWND $url;
$url = getProductURL $counter;
ieNavigation $ieProduct.HWND $url;
$option = $null;
}else{
$counter = getBeforeCounter $counter;
if($counter -lt 0 ){ $counter = 0; }
$url = getStagingURL $counter;
ieNavigation $ieStaging.HWND $url;
$url = getProductURL $counter;
ieNavigation $ieProduct.HWND $url;
}
$currentNo = getNo($counter);
# Write-Host("NO = [$currentNo], Name=[$operatorName]");
# Write-Host("StagingUrl = ["+(getStagingURL $counter)+"]");
# Write-Host("ProductUrl = ["+(getProductURL $counter)+"]");
logger ("NO = [$currentNo], Name=[$operatorName]");
logger ("StagingUrl = ["+(getStagingURL $counter)+"]");
logger ("ProductUrl = ["+(getProductURL $counter)+"]");
$balMsg = "NO = [$currentNo]";
Baloon "PREVIOUS PAGE" $balMsg ;
$scroll = 0;
}elseif($rtn -eq 110){ # UP
Write-Host("SCROLL UP");
$scrollP = ieScroll $ieStaging.HWND $scroll "UP";
$scrollP = ieScroll $ieProduct.HWND $scroll "UP";
$scroll = $scrollP;
}elseif($rtn -eq 120){ # DOWN
Write-Host("SCROLL DOWN");
$hwnd = $ieStaging.HWND;
$scrollP = ieScroll $hwnd $scroll "DOWN";
$scrollP = ieScroll $ieProduct.HWND $scroll "DOWN";
$scroll = $scrollP;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<elements>
<element>
<product>
<url>https://www.xxxxxxxxxxxx.jp/pppppppp/about/stu.html</url>
<no>12</no>
</product>
<staging>
<url>https://stg-www.xxxxxxxxxxxx.jp/pppppppp/about/stu.html</url>
<no>12</no>
</staging>
</element>
<element>
<product>
<url>https://www.xxxxxxxxxxxx.jp/pppppppp/true_or_false/index.html</url>
<no>13</no>
</product>
<staging>
<url>https://stg-www.xxxxxxxxxxxx.jp/pppppppp/true_or_false/index.html</url>
<no>13</no>
</staging>
</element>
</elements>
↑ 掲載できないので抜粋しましたが、<element/> を大量に繰返ししています。
これはエクセル一覧からマクロで作っていたりするのですが、そこのところは本稿とあまり関係ないので割愛します。