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


元記事はこちら

大人の事情で公開していなかった部分を含めて、全てみせます。
自分の記録の意味も含めて。

screenshotDiff.js
/*------------------------------------------------------------
   大量ページを逐次表示、スクリーンショットをとり
   ペアページ同士で比較差分を表示する。

   制限事項: 自動アクセス防止ヒューリスティックを採用して
   いるサイトは アクセス権限無し(403)を返すため比較差分を
   することができない。

    node v8.4.0
    puppeteer-core v1.8.0
    looks-same v4.0.0
    @I.Times  2018/9/25 
--------------------------------------------------------------*/
/*------------------------------------------------------------
   2018/9/24 I.Times Haranaga
   puppeteer によるスクリーンショットの工夫 
   (1) ページ表示後にスクリーンショットを取らず一度リロードを
       してからスクリーンショットをとる。
       リロード後のほうがページ描画が安定するため。

--------------------------------------------------------------*/
/*------------------------------------------------------------
   2018/9/24 I.Times Haranaga
   looks-sameには独自修正を加えている。
   (1) createDiff実行時に 差分ピクセル数を返すようCallBackを修正
  (2) 差分箇所のみに点を描画する画像ファイルを追加作成する。

--------------------------------------------------------------*/
const puppeteer = require('puppeteer-core');
const async = require('async');
const delay = require('delay');
const fs = require('fs');
const che = require('cheerio');
var looksSame = require('looks-same');

var os = require('os');

// 数値を前ゼロ埋めする。
function zeroPadding(num,length){
    var ZERO = '0000000000000000';
    if(length){
        if(length > ZERO.length){
            return (ZERO + num).slice(ZERO.length*(-1));
        }else if(length > 0){
            return (ZERO + num).slice(-length);
        }else{
            return num;
        }
    }else{
        return (ZERO + num).slice(ZERO.length*(-1));
    }

}

const headless = true;

const browserExecutablePath = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36';
//const userAgent = 'BOT/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36';
const WindowWidth = 1920;
const WindowHeight = 1080;

const viewPort = {
    width: WindowWidth, 
    height: WindowHeight, 
    deviceScaleFactor:0.85, 
    isMobile:false, 
    Mobile:false, 
    hasTouch:false, 
    isLandscape:false,
};

const NO_SANDBOX = '--no-sandbox';

const _browserOptions = {
    headless: headless,
    ignoreHTTPSErrors: true,
    executablePath: browserExecutablePath,
    defaultViewport:viewPort,
    args: ['--window-size='+WindowWidth+','+WindowHeight,'--window-position=0,0',NO_SANDBOX],
};

var browseOptions = _browserOptions;

// Async-Awaitの中の例外をキャッチする仕組み
process.on('unhandledRejection', console.dir);

const DIRNAME = __dirname;


(async () => {

    // URL一覧を読み込む。
    var elements = [];
    var elementList = async () => {
        var xml_data = await fs.readFileSync("data.xml", "utf-8");
        $ = che.load(xml_data);
        $("element").each(function(i0, el0) {
            var elem = {};
            var actualElem = {};
            var actual = $(this).children("actual");
            actualElem['url'] = actual.children("url").text();
            actualElem['networkidle'] = $(actual).children("networkidle").text();
            actualElem['delay'] = $(actual).children("delay").text();
            elem['actual'] = actualElem;
            var targetElem = {};
            var target = $(this).children("target");
            targetElem['url'] = target.children("url").text()
            targetElem['networkidle'] = target.children("networkidle").text()
            targetElem['delay'] = target.children("delay").text()
            elem['target'] = targetElem;
            elem['tolerance'] = $(this).children('tolerance').text();
            elements.push(elem);
        });
    };
    elementList();

    // スクリーンショットFunction定義:ページを表示,スクリーンショットを取る。
    var screenShot = async  (element, shotPath) =>{
        await page.setUserAgent(userAgent);
        await page.goto(element.url,  {waitUntil: element.networkidle})
        .then( async function(response){
            // ReLoadする理由
            // ReLoad再描画が時間が掛からず、ScreenShotタイミングを取りやすいため

            await page.reload({waitUntil: element.networkidle}).then( async function(response) {
                if(element.delay>0){
                    await delay(element.delay);
                }
                await page.screenshot( {path: DIRNAME+'\\'+shotPath, fullPage: true});
            });
        } );
    };

    // 配列要素を処理するAsync Function定義
    var eachProcess = async (element, callback) => {

        var index = elements.indexOf( element );
        var count = index + 1;
        var path = {
            imageA: 'imageA\\shotA_'+zeroPadding(count,5)+'.png',
            imageB: 'imageB\\shotB_'+zeroPadding(count,5)+'.png'
        };
        // Actual スクリーンショット
        await screenShot(element.actual, path.imageA);
        // Target スクリーンショット
        await screenShot(element.target, path.imageB);
        // Actual<=>Targetの比較
        var looksSameOption = {};
        looksSameOption.reference = path.imageA;
        looksSameOption.current = path.imageB;
        looksSameOption.diff = 'diff\\diff_'+zeroPadding(count,5)+'_1.png';
        looksSameOption.diff2 = 'diff\\diff_'+zeroPadding(count,5)+'_2.png'; // 独自追加オプション
        looksSameOption.highlightColor = '#ff00ff'; //color to highlight the differences
        looksSameOption.defaultColor = '#ffffff'; // 独自追加オプション
        looksSameOption.strict = true; //strict comparsion
        looksSameOption.writeOriginalDiff = true; // 独自追加オプション

        looksSame.createDiff(looksSameOption
        // このパラメータFunctionは独自追加です。
        ,function(unmatch){
            console.log("No.["+zeroPadding(count,5)+"] UnMatch["+zeroPadding(unmatch)+"]:"+element.actual.url);
            // 必ずここのfunctionを一度呼びだすので、ここで forEachSeries のcallbackを呼び出す。
            callback();
        // このパラメータFunctionは独自追加です。
        }, function(err){
            if(err){
                console.log(err);
                throw err;
            }
        }, function(err){
            if(err){
                console.log(err);
                throw err;
            }
        } ); // looksSame 終わり
    };

    // ブラウザを起動する。
    var browser = await puppeteer.launch( _browserOptions );
    const page = await browser.newPage();
    await page.setJavaScriptEnabled(true);
    //await page.evaluate('navigator.userAgent');
    // キャッシュ無効にする(効果は未確認)
    const client = await page.target().createCDPSession();
    await client.send( 'Network.setCacheDisabled', { 'cacheDisabled' : true } );
    await page.setCacheEnabled( false );

    // 配列(elements)の要素をAsync順次処理する。
    async.forEachSeries( 
        // 第一パラメータ:配列
        elements, 
        // 第二パラメータ:要素を処理するAsync Function
        (async function(element, callback){

            await eachProcess(element, callback);
        }),
        // 第三パラメータ: 最後に呼び出されるCallBack
        async function(err){
            if(err) throw err;

            await browser.close();
            console.log('##browser close');
        }
    ); // async.forEachSeries終わり
})();

data.xml(例)
<element>
    <actual>
        <url>https://www.naro.affrc.go.jp/nivfs/index.html</url>
        <networkidle>networkidle0</networkidle>
        <delay>0</delay>
    </actual>
    <target>
        <url>https://www.naro.affrc.go.jp/nivfs/index.html</url>
        <networkidle>networkidle0</networkidle>
        <delay>0</delay>
    </target>
    <tolerance>0</tolerance>
</element>
<element>
    <actual>
        <url>https://www.paxcompy.co.jp/</url>
        <networkidle>networkidle0</networkidle>
        <delay>0</delay>
    </actual>
    <target>
        <url>https://www.paxcompy.co.jp/</url>
        <networkidle>networkidle0</networkidle>
        <delay>0</delay>
    </target>
    <tolerance>0</tolerance>
</element>
<element>
    <actual>
        <url>http://www.machidukuri-nagano.jp/</url>
        <networkidle>networkidle0</networkidle>
        <delay>0</delay>
    </actual>
    <target>
        <url>http://www.machidukuri-nagano.jp/</url>
        <networkidle>networkidle0</networkidle>
        <delay>0</delay>
    </target>
    <tolerance>0</tolerance>
</element>
node_modules\looks-same\index.js
'use strict';

const _ = require('lodash');
const parseColor = require('parse-color');
const colorDiff = require('color-diff');
const png = require('./lib/png');
const areColorsSame = require('./lib/same-colors');
const AntialiasingComparator = require('./lib/antialiasing-comparator');
const IgnoreCaretComparator = require('./lib/ignore-caret-comparator');
const utils = require('./lib/utils');
const readPair = utils.readPair;
const getDiffPixelsCoords = utils.getDiffPixelsCoords;

const JND = 2.3; // Just noticeable difference if ciede2000 >= JND then colors difference is noticeable by human eye

const getDiffArea = (diffPixelsCoords) => {
    const xs = [];
    const ys = [];

    diffPixelsCoords.forEach((coords) => {
        xs.push(coords[0]);
        ys.push(coords[1]);
    });

    const top = Math.min.apply(Math, ys);
    const bottom = Math.max.apply(Math, ys);

    const left = Math.min.apply(Math, xs);
    const right = Math.max.apply(Math, xs);

    const width = (right - left) + 1;
    const height = (bottom - top) + 1;

    return {left, top, width, height};
};

const makeAntialiasingComparator = (comparator, png1, png2, opts) => {
    const antialiasingComparator = new AntialiasingComparator(comparator, png1, png2, opts);
    return (data) => antialiasingComparator.compare(data);
};

const makeNoCaretColorComparator = (comparator, pixelRatio) => {
    const caretComparator = new IgnoreCaretComparator(comparator, pixelRatio);
    return (data) => caretComparator.compare(data);
};

function makeCIEDE2000Comparator(tolerance) {
    return function doColorsLookSame(data) {
        if (areColorsSame(data)) {
            return true;
        }
        /*jshint camelcase:false*/
        const lab1 = colorDiff.rgb_to_lab(data.color1);
        const lab2 = colorDiff.rgb_to_lab(data.color2);

        return colorDiff.diff(lab1, lab2) < tolerance;
    };
}

const createComparator = (png1, png2, opts) => {
    let comparator = opts.strict ? areColorsSame : makeCIEDE2000Comparator(opts.tolerance);

    if (opts.ignoreAntialiasing) {
        comparator = makeAntialiasingComparator(comparator, png1, png2, opts);
    }

    if (opts.ignoreCaret) {
        comparator = makeNoCaretColorComparator(comparator, opts.pixelRatio);
    }

    return comparator;
};

const iterateRect = (width, height, callback, endCallback) => {
    const processRow = (y) => {
        setImmediate(() => {
            for (let x = 0; x < width; x++) {
                callback(x, y);
            }

            y++;

            if (y < height) {
                processRow(y);
            } else {
                endCallback();
            }
        });
    };

    processRow(0);
};

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);

    // ###### ここを変えた ########
    // -- add start ---
    const result2 = (options.writeOriginalDiff)? png.empty(width, height): null;
    var unmatch = 0;
    // -- add end ---

    iterateRect(width, height, (x, y) => {
        if (x >= minWidth || y >= minHeight) {
            result.setPixel(x, y, highlightColor);
        // ###### ここを変えた ########
        // -- add start ---
        unmatch += 1;
        // -- add end ---
            return;
        }

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

        if (!options.comparator({color1, color2})) {
            result.setPixel(x, y, highlightColor);
            // ###### ここを変えた ########
            // -- add start ---
            unmatch += 1;
        if(options.writeOriginalDiff){
        result2.setPixel(x, y, options.highlightColor);
        }
            // -- add end ---
        } else {
        result.setPixel(x, y, color1);
        // ###### ここを変えた ########
        // add start
        if(options.writeOriginalDiff){
        result2.setPixel(x, y, options.defaultColor);
        }
        // add end
        }
    // ###### ここを変えた ########
    //}, () => callback(result));
    }, () => callback(result, result2, unmatch));
};

const parseColorString = (str) => {
    const parsed = parseColor(str);

    return {
        R: parsed.rgb[0],
        G: parsed.rgb[1],
        B: parsed.rgb[2]
    };
};

const getToleranceFromOpts = (opts) => {
    if (!_.hasIn(opts, 'tolerance')) {
        return JND;
    }

    if (opts.strict) {
        throw new TypeError('Unable to use "strict" and "tolerance" options together');
    }

    return opts.tolerance;
};

const prepareOpts = (opts) => {
    opts.tolerance = getToleranceFromOpts(opts);

    _.defaults(opts, {
        ignoreAntialiasing: true,
        antialiasingTolerance: 0
    });
};

module.exports = exports = function looksSame(reference, image, opts, callback) {
    if (!callback) {
        callback = opts;
        opts = {};
    }

    prepareOpts(opts);

    readPair(reference, image, (error, pair) => {
        if (error) {
            return callback(error);
        }

        const first = pair.first;
        const second = pair.second;

        if (first.width !== second.width || first.height !== second.height) {
            return process.nextTick(() => callback(null, false));
        }

        const comparator = createComparator(first, second, opts);

        getDiffPixelsCoords(first, second, comparator, {stopOnFirstFail: true}, (result) => {
            callback(null, result.length === 0);
        });
    });
};

exports.getDiffArea = function(reference, image, opts, callback) {
    if (!callback) {
        callback = opts;
        opts = {};
    }

    prepareOpts(opts);

    readPair(reference, image, (error, pair) => {
        if (error) {
            return callback(error);
        }

        const first = pair.first;
        const second = pair.second;

        if (first.width !== second.width || first.height !== second.height) {
            return process.nextTick(() => callback(null, {
                width: Math.max(first.width, second.width),
                height: Math.max(first.height, second.height),
                top: 0,
                left: 0
            }));
        }

        const comparator = createComparator(first, second, opts);

        getDiffPixelsCoords(first, second, comparator, (result) => {
            if (!result.length) {
                return callback(null, null);
            }

            callback(null, getDiffArea(result));
        });
    });
};

//### ここ変えた!
//exports.createDiff = function saveDiff(opts, callback) {
exports.createDiff = function saveDiff(opts, callback, callback2, callback3) {
    const tolerance = getToleranceFromOpts(opts);

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

        const diffOptions = {
            highlightColor: parseColorString(opts.highlightColor),
        // ### 下記1行追加
        defaultColor : parseColorString(opts.defaultColor),
        writeOriginalDiff: opts.writeOriginalDiff,
            comparator: opts.strict ? areColorsSame : makeCIEDE2000Comparator(tolerance)
        };

        // ### ここ変えた!!    
    //buildDiffImage(result.first, result.second, diffOptions, (result) => {
        buildDiffImage(result.first, result.second, diffOptions, (result, result2, unmatch) => {
            // ### ここ変えた!!    
            //if (opts.diff === undefined) {
            //    result.createBuffer(callback);
            //} else {
            //    result.save(opts.diff, callback);
        //}
            if (opts.diff === undefined) {
                result.createBuffer(callback2);
            } else {
                result.save(opts.diff, callback2);
        if(opts.diff2 && result2){
                    result2.save(opts.diff2, callback3);
        }
        callback(unmatch);
            }
        });
    });
};

exports.colors = (color1, color2, opts) => {
    opts = opts || {};

    if (opts.tolerance === undefined) {
        opts.tolerance = JND;
    }

    const comparator = makeCIEDE2000Comparator(opts.tolerance);

    return comparator({color1, color2});
};