大量商用ページの表示結果を比較するために試行錯誤した

Qiitaは初投稿のHarHidです。つらつらと書いていきます。

**(2019/09/12)**
**画像横並びのつもりがいつのまにか縦表示になってしまっている。**
**表の中に画像を埋め込むように変更した。**

# お題：ブラウザ表示結果の差分を確認したいがどうしたらよいのでしょうか？しかも大量に！

ステージングサーバー、本番サーバーの間でページ表示結果が一致しているか心配だから確認してみたい！という「とあるお方の」ご要望を受けまして少し調べてみたり試行してみたりしました。

人間の目視（目と手を動かす）を前提にしますと、例えばモニターを２個用意して、両方でAサイト/Bサイトの同じページを表示、目を左右に動かしながら（残像を頼りに）違い有無を確認していくことになります。ページによっては縦に長い場合もありスクロールさせながら両者の差異有無を確認していくことになります。

数十ページ、多くて百ページ前後ならば人間の手で泥臭くやったほうが確実なのですが、数千～数万のページ数になりますと人間の手(及び目)だけではさすがにつらい、眼球疲労蓄積・集中力が途切れてしまい、差分を見逃すかもしれない恐れあり（いや絶対に数個程度は見逃すでしょう）。

なんとかして機械的に確実な比較をさせたい、しかも自動でできないかな、今はやりの__RPA__(RoboticProcessAutomation)を使ったらいいのかな、といろいろ模索したことをつらつらと纏めてみます。

# 目標

数千ページ～数万単位のページがあるとき、__人間の目（体力・精神力の維持）__を前提にせずに、__全ページの差異比較を可能とする__こと！ 商用ページを対象とするので、__世に広くつかわれている環境__で確認したい

````
MS-Windows 系環境で！ 一番利用者数が多そうです
Linux(CentOS)環境のブラウザは利用者少、検証に使うことに抵抗がありますので確認には使わない。
Mac環境は手元になく(残念!)、試行できないので対象外とする。
````

# 最初に思いついたこと
当初はブラウザウインドウを２個作っておき、それぞれにURLを入力（ペースト）してページを表示、表示後に両者を重ね合わせて一方を透過ツールで半透明にしておき、差分を探そうと思いました。
そのやり方を自動化（RPAっぽく)してみようと思いました。
透過ツールは「[__TranspWnds__](http://pclife365.com/software/utility/1560.html)」を想定してみました。

⇒ 長所
-
一方を半透明にして重ね合わせることで、２つのモニターで左右に目を振りながら（残像を頼りに）差異確認するという不確実な手法から脱することができます。直観的に差異を発見できそうです。

⇒ 短所
-
やってみて気づきましたが、縦に長いページを比較するために２つのページを同じようにスクロールさせる必要がありました。__スクロール量が少しでも違うと重ね合わせがうまくいきません__。ぴったり同じになるようにスクロール量を調整するのが難点です。さらにいうと、２つのページを重ねたとしても、ほんの少しだけ違うところを目で探すのが__意外につらい（精神的につらい）__！
⇒ とりあえずの結論
-
RPAを念頭においたとしてもスクロール調整が壁になりますし、重ね合わせたページの差異を探すために、長時間モニター画面を見続ける__「目の体力・精神力の維持」欠如__が懸念されます。→ いろいろと__失敗しそうな予感__がします

# その次に思いついたこと
２モニタに表示させた段階で、両方ともにキャプチャ画像を保存、両者を画像マッチングして差異確認するのはどうだ？と考えました。ページを表示するステップ+キャプチャをとるステップをRPAツールで自動化したらうまくいくんじゃないか？と思いさっそく使えそうなRPAツールを調べてみました。企業向けの本格的なやつは（ライセンス費用的に）手がでませんので、無料版が大前提ですが。RPAツールでキャプチャも取れるのか、ブラウザでスクロールさせるのはどうしたらよいのか？など調べている過程で、__headless-chrome__ が使えそうと気づきます。

ブラウザを表示させておく必要はないよね？、じゃ__RPA__もいらないんじゃないの？スクリプトでできるよね？
今更ながらですが、__RPAは却下__、__スクリプトの方向に路線変更__です！RPAは今回は使わないで済みそうです。

ちなみに、無料で使えて使い勝手がよさそうなRPAツールは下記の２つでした。後日再トライしようと思います。
・[RPAExpress](https://www.workfusion.com/rpa-express/) ( 正真正銘の無料ツール )
-
完全無欠の無料ツールというのが気にいりましたが、スクリプト路線でいけそうなので試用中止。
・[UI-Path](https://www.uipath.com/ja/) 
-
[Ui-Pathライセンス条項](https://www.uipath.com/community-license-agreement)の理解が足りていないかもしれませんが、エンタープライズ企業(大企業のことと解釈しました)でなければ業務でも使えそうです。エンタープライズ企業の定義も正確にはよくわからんのですが・・。[UI-Path Studio](https://www.uipath.com/ja/products/studio) を導入してみようかなと一度は思いましたが前述の理由で試用中止。

# __headless-chrome__ スクリプト路線でいきますかね

__headless-Chrome__ は特別なブラウザを導入する必要なし！ 普通の__最新版Chrome__さえあればよいことが一番のメリットです。__Chrome__には__headless機能__が標準装備されています。

[headless-Chrome](https://www.webprofessional.jp/headless-chrome-node-js/) + [Puppeteer](https://www.sbbit.jp/article/cont1/34562) 一択で！（正確には __Puppeteer-core__)
あっさり Node + Puppeteer-core に乗り換えることにしました。
__Puppeteer__はheadlessブラウザの操作ライブラリであり、Headless(GUIなし)でも制御できるので高速です。
利用者も多いため良い感じで使えることが期待できます。
__Puppeteer__はページ全体のキャプチャを簡単にとれるし、技術情報が多数公開されていることも安心材料のひとつです。

Puppeteerについてくるブラウザ__Chromium__は不要なので、__puppeteer-core__をinstallします。
(Chromiumが「とあるお方」の推奨ブラウザでないこともありますし）

Windows版Chromeのウインドウをモニター全体サイズいっぱい[^1]で表示させたいので下記の指定にします。


````
const _browserOptions = {
　　headless: true, // Headlessで実行
　　executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
　　defaultViewport:
      {
	width: WindowWidth, 
	height: WindowHeight, 
	deviceScaleFactor:0.85,  // これは適当 
	isMobile:false, 
	Mobile:false, 
	hasTouch:false, 
	isLandscape:false,
      },
　　args: ['--window-size=1920,1080 --window-position=0,0','--no-sandbox'],
};
var browser = await puppeteer.launch( _browserOptions );
````
↑ __await__ を使っていますが、__async__ の中で利用していると脳内変換してください。

[^1]:  俗に言う__フルスクリーン__のことですね。私の使っているモニタのサイズが 1920,1080です。

# 画像比較はどうする？

結果的に [__LooksSame__](https://www.npmjs.com/package/looks-same) を採用しました。
技術情報が多数公開されてること、画像比較条件を指定（甘くしたり厳しくしたり）が出来て試行に都合がよいことから__[LooksSame](https://www.npmjs.com/package/looks-same)__ を採用しました。他の同様なライブラリ__image-diff__などもありますが、メンテがない様子であり対象から除外しました。

# やってみたらどうだったか？
当方、Java使いであるためNode(というよりは Javascriptの非同期の世界）に四苦八苦しましたが、その枝葉末節は除きます。

まず同じページを Actual/Targetにしてキャプチャ＋画像比較をしてみました。
同じページ(同じURL)同士を比較するのだから、キャプチャ画像は完全一致するはずだよね、と軽い気持ちで試行を開始しています。

```
page.goto(element.url,  {waitUntil: "networkidle0"})
.then( async function(response){
    await page.screenshot( {path: 画像パス, fullPage: true});
});

```

⇒ あれ？同じページのはずが差異差分がでるんですけど！
-
ページによっては微妙に差分がでてしまいます[^2]。画像が1pixel分ずれて表示されていたり、色が微妙に違う（濃い・薄い）、メニューの文字の幅が違っていたり・・・、差異がでるときもあり、出ないときもあり、何故なのかさっぱりわからない。

[^2]: ロード直後に動きを見せるサイトにて差分が発生するような気がしています。

左から **actual / target / diff**  <== diff：ピンクの箇所が差異あるところです [^3]

<table style="width:80%;table-layout: fixed;"><thead><tr>
<th style="text-align: center">actual</th>
<th style="text-align: center">target</th>
<th style="text-align: center">diff</th>
</tr></thead>
<tbody><tr>
<td style="text-align: center"><img src="https://qiita-image-store.s3.amazonaws.com/0/294946/98197519-e926-789a-76c3-e734fa5e2b40.png" width="50%"/></td>
<td style="text-align: center"><img src="https://qiita-image-store.s3.amazonaws.com/0/294946/451c2a34-defd-8f49-19b2-a162089c09f8.png" width="50%"/>
</td>
<td style="text-align: center"><img src="https://qiita-image-store.s3.amazonaws.com/0/294946/cfbaf69b-8a9f-250b-a2cb-3ad2660d2ac5.png" width="50%"/>
</td>
</tr></tbody></table>


[^3]: [三井物産株式会社](https://www.mitsui.com/jp/ja/index.html)様サイトを使わせて頂きました。ありがとうございます。

#　気を取り直して原因を探します

⇒ おやおや、気づいたことがあるんですけど
-

元々大量ページの差異比較のためにスクリプト化しているので、同じページを何回も（数十回以上）連続して表示⇒キャプチャ⇒画像比較をしているのですが、２回目以降の比較では差異がでないんですよ。差異がでるときはスクリプト起動後の最初のページ分だけ。私のスクリプトは、Browse(Chrome)を起動して連続表示を繰返し、全ての比較が終わったら Browser.close() という流れでした。

⇒ 差異がでるところはどこだ？
-
ブラウザを可視化して表示確認をするとページ表示直後に、section部、画像、メニューなどが「ちょこっと」動くことを確認できました。理由はわかりません。ブラウザ上でのレンダリングが関係していそうな気配がありますが、はっきりと断言まではできず。

⇒ そのままブラウザ上で「えい」とリロードしてみると、１回目の表示時の動きと２回目（それ以降）での動きが「ほんのちょっと（気のせいかも）」違う感じがしました。もしかしたら２回目以降の表示どうしを比較したらいいかもね！(繰り返すが理由はわかりません）

⇒ 表示後にリロードしてからキャプチャすると・・
-
これはうまくいった。同じページだから当然だが、**一発目の差異量発生**は解消しました！
多分だけど、キャッシュがあるときと無いときの違いかもしれないですね。


< こんな感じ> 

```
page.goto(element.url,  {waitUntil: "networkidle0"})
.then( async function(response){
    await page.reload({waitUntil: "networkidle0"}).then( async function(response) {
          await page.screenshot( {path: 画像パス, fullPage: true});
    });
});
```
表示時、リロード時ともに、同じオプションにしています。

```
{waitUntil: "networkidle0"}
```

# looks-safe のオプション

````
{
   reference : imageAのパス,
   current : imageBのパス,
   diff : 差分画像のパス,
   highlightColor : '#ff00ff', //color to highlight the differences	 
   strict = true;   // strict comparsion
}
````
# looks-sameの改造もしてみた

大量ページの連続差分確認をさせるわけなので、ログに差分量を出力しておきたいのですが、looks-sameの機能は、差分有無(true/false)を返す、もしくは 差分画像ファイルを出力するの２通りしかなく、当方の希望には物足りないです。

- 差分画像ファイルは出力したい！( 結果を画像で再確認したいから)
- 差分量は把握したい。( ピクセル単位で差分数を知りたい )
- 加えて looks-same標準の差分ファイルは actualページ画像に HIGH-LIGHT色で差分箇所にドットをつけたもの。これはこれでいいのだが、無色（背景白など）に対して HIGH-LIGHT色をつけた画像ファイルも欲しい。

というわけで、looks-same のソースにも手を加えています。
この手を加えた内容は今回は割愛します[^4]が、割と簡単にできました。

[^4]: （手を加えた内容）大人の事情に触れない範囲で、後日整理をして公開したいと思います[→ここで公開](https://qiita.com/harhid/items/5780a6c878553917fd84)

ヒントは下記の数か所をいじる！です。

```node_modules\looks-same\index.js
const buildDiffImage = (png1, png2, options, callback) => {
    const width = Math.max(png1.width, png2.width);
    const height = Math.max(png1.height, png2.height);
    const minWidth = Math.min(png1.width, png2.width);
    const minHeight = Math.min(png1.height, png2.height);
    const highlightColor = options.highlightColor;
    const result = png.empty(width, height);

    iterateRect(width, height, (x, y) => {
        if (x >= minWidth || y >= minHeight) {
            result.setPixel(x, y, highlightColor);
            return;
        }

        const color1 = png1.getPixel(x, y);
        const color2 = png2.getPixel(x, y);

        if (!options.comparator({color1, color2})) {
            result.setPixel(x, y, highlightColor);
        } else {
            result.setPixel(x, y, color1);
        }
    }, () => callback(result));
};

````
```node_modules\looks-same\index.js

exports.createDiff = function saveDiff(opts, callback) {
    const tolerance = getToleranceFromOpts(opts);

    readPair(opts.reference, opts.current, (error, result) => {
        if (error) {
            return callback(error);
        }

        const diffOptions = {
            highlightColor: parseColorString(opts.highlightColor),
            comparator: opts.strict ? areColorsSame : makeCIEDE2000Comparator(tolerance)
        };

        buildDiffImage(result.first, result.second, diffOptions, (result) => {
            if (opts.diff === undefined) {
                result.createBuffer(callback);
            } else {
                result.save(opts.diff, callback);
            }
        });
    });
};

````

# LooksSameを改造して得られる画像の例

- 完全一致した例1 [^5]
左からActual画像 / Target画像 / Diff画像(背景Actual) / ___差異がないのでDIFF(白地省略)___

|actual|target|diff|
|:-----------------:|:------------------:|:------------------:|
|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/b814d266-31c4-1035-9a6f-bf87f802590f.png" width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/dab74d03-c5e6-023e-4653-8a05b5f371b2.png"  width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/460e7b21-b338-cd02-cdf1-bde6053235f2.png"  width="50%"/>|


- 完全一致した例2 [^6]
左からActual画像 / Target画像 / Diff画像(背景Actual) / ___差異がないのでDIFF(白地省略)___

|actual|target|diff|
|:-----------------:|:------------------:|:------------------:|
|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/661673d9-c714-b903-def9-b9a3832e299e.png" width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/95d0d198-b378-d424-a541-42e5ccd9d1f9.png"  width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/dc3cf04f-2f78-74bb-cc32-958bd5eae618.png"  width="50%"/>|


- 差異がある例 [^7]
左からActual画像 / Target画像 / Diff画像(背景Actual) / Diff画像(背景白)

|actual|target|diff<br/>(背景Actual)|diff<br/>(背景白)|
|:-----------------:|:------------------:|:------------------:|:------------------:|
|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/3d35c70b-dfee-a60c-68e4-4c2ba28f4004.png" width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/965955f3-5a84-0d5f-b50b-6b65581b32d2.png" width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/2fdedc24-425e-401f-c8a0-f29d540b4403.png" width="50%"/>|<img src="https://qiita-image-store.s3.amazonaws.com/0/294946/67c560b9-56b3-309f-f315-fc7faabd530e.png" width="50%"/>|


[^5]: [三井物産株式会社](https://www.mitsui.com/jp/ja/index.html)様サイトを使わせて頂きました。ありがとうございます。
[^6]: [農研機構](https://www.naro.affrc.go.jp/nivfs/index.html)様サイトを使わせて頂きました。ありがとうございます。
[^7]: [パックス工業株式会社様](https://www.paxcompy.co.jp/)サイトを使わせて頂きました。ありがとうございます。

# ツールが出力するログの例

````
No.[00001] UnMatch[0000000000000000]:https://www.xxxxx.com/jp/ja/index.html
No.[00002] UnMatch[0000000000000000]:https://www.xxxxx.com/jp-ja/index.html
No.[00003] UnMatch[0000000000000000]:https://www.xxxxx.jp/business/index.html
No.[00004] UnMatch[0000000000000000]:https://www.xxxxx.jp/business/support/important/180906_01_02.html
～
No.[00024] UnMatch[0000000000419314]:https://www.xxxxx.com.au/home

````
UnMatch[xxxx] の数字は、差分がでたピクセルの数を示します。0 の場合は差分なし。

# 最終結論

- Puppeteer と LooksSame をつかえば大量ページの差分比較自動化できる、しかも安定しているっぽい。
- キャプチャとるときは、ReLoadすると良いことがありそう(少なくとも差異大量発生は避けられる)
- 動く画像( FadeIn / 画像切替え )があるときは高頻度で差分がでる。これは目視点検で補うしかなさそう。
- Looks-same は 標準機能物足りないから手を加えて使えばよい。

# Puppeteer+LooksSame で差分が確認されたら？

Puppeteer+LooksSameの差分結果画像を見て不審な差分があると思えた場合は、[DIFFEE](http://diffee.me/)のような２つのページの動きをリアルタイムで比較差分をとるツールを使って検証しどのように異なるのかを見るとよいでしょう。


# リダイレクト先ＵＲＬを確認したいときも・・

これも__Puppeteer__にて簡単に確認できる。

````
var originalUrl = "http://www.itimes.co.jp/aipo/";
var res = await page.goto(originalUrl, {waitUntil: "networkidle0"});
var redirectUrl = res._request._url;
console.log("original=: "+ originalUrl);
console.log("redirect=: "+ redirectUrl);
````
実行結果のログは下記の感じにでます。

````
original=: http://www.itimes.co.jp/aipo/
redirect=: https://aipo.itimes.co.jp/aipo/
````

# 注意喚起事項

ここで説明する手法は ブラウザをスクリプト処理で操作し求めるサイト（ページ）にアクセスするものです。世のサイト（ページ）のなかには、不正アクセスを制限するアンチボット対策がなされたサイト[^8]があります。このようなサイトに対しては、本稿で説明したライブラリ(__Puppeteer__)の類は適用できない場合があります。不正アクセスと見なされてアクセス権限無し(403)が返ることがあります。

[^8]: __ヒューリスティック機能__を使ってボットを検知するサイトのことです。

以上です。


# 追伸：(2019/9/9)
別方法で実現したものがあるので、別記事でご紹介します。
[大量商用ページの表示結果を比較するために試行錯誤した（３）](https://qiita.com/harhid/items/c4c1bf5587d651e5b7ef)