株式会社WR

株式会社WR

WEB TOTAL CONSULTING

Puppeteerで定期スクリーンショット——競合サイト監視に活用
ブログ一覧へ
技術ブログ

Puppeteerで定期スクリーンショット——競合サイト監視に活用

競合他社のサイトを定期的にスクリーンショット撮影して、デザイン変更・キャンペーン更新を自動監視する仕組みをPuppeteerで構築します。

定期スクリーンショットによるサイト監視

競合サイトや自社サイトの変化を継続的に追跡する方法として、定期スクリーンショットは非常に効果的です。ページのデザイン変更・価格表示の変化・セールバナーの掲出など、テキストデータだけでは捉えにくい変化を視覚的に記録できます。

活用シーン:

  • 競合他社のトップページ・価格ページの変化追跡
  • 自社サイトのデプロイ後の表示確認(ビジュアルリグレッションテスト)
  • LP(ランディングページ)のA/Bテスト記録
  • クライアントサイトの定期納品物の品質確認

環境構築

mkdir screenshot-bot && cd screenshot-bot
npm init -y
npm install puppeteer node-cron dotenv sharp

基本的なスクリーンショット取得

const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');

/**
 * URLのスクリーンショットを取得する
 * @param {string} url
 * @param {string} outputPath
 * @param {object} options
 */
async function takeScreenshot(url, outputPath, options = {}) {
    const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox'],
    });

    const page = await browser.newPage();

    // ビューポート設定
    await page.setViewport({
        width:  options.width ?? 1280,
        height: options.height ?? 900,
        deviceScaleFactor: options.dpr ?? 1,
    });

    // ユーザーエージェント設定
    await page.setUserAgent(
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
        'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
    );

    try {
        await page.goto(url, {
            waitUntil: 'networkidle2',
            timeout: 30000,
        });

        // 動的コンテンツの読み込みを待つ
        await page.waitForTimeout(2000);

        // スクリーンショット取得
        await page.screenshot({
            path: outputPath,
            fullPage: options.fullPage ?? true,
            type: 'png',
        });

        console.log(`取得完了: ${outputPath}`);
        return { success: true, path: outputPath };

    } catch (error) {
        console.error(`取得失敗: ${url}`, error.message);
        return { success: false, error: error.message };

    } finally {
        await browser.close();
    }
}

監視対象リストを管理する

// config/sites.js
module.exports = [
    {
        id:   'competitor_a_top',
        name: '競合A トップページ',
        url:  'https://competitor-a.com/',
        viewport: { width: 1280, height: 900 },
        fullPage: false,
    },
    {
        id:   'competitor_a_pricing',
        name: '競合A 料金ページ',
        url:  'https://competitor-a.com/pricing',
        viewport: { width: 1280, height: 900 },
        fullPage: true,
    },
    {
        id:   'own_site_top',
        name: '自社サイト トップページ',
        url:  'https://example.com/',
        viewport: { width: 375, height: 812 }, // モバイル確認
        fullPage: true,
    },
];

日時付きファイル名で保存する

const { format } = require('date-fns');
const { ja } = require('date-fns/locale');

function buildOutputPath(siteId, timestamp) {
    const dateStr = format(timestamp, 'yyyy-MM-dd_HH-mm');
    const dir = path.join('./screenshots', siteId);
    fs.mkdirSync(dir, { recursive: true });
    return path.join(dir, `${dateStr}.png`);
}

// 全サイトのスクリーンショットを取得
async function captureAll(sites) {
    const timestamp = new Date();
    const results = [];

    for (const site of sites) {
        const outputPath = buildOutputPath(site.id, timestamp);
        const result = await takeScreenshot(site.url, outputPath, {
            width: site.viewport?.width,
            height: site.viewport?.height,
            fullPage: site.fullPage,
        });
        results.push({ site: site.name, ...result });

        // サーバー負荷を考慮して待機
        await new Promise(r => setTimeout(r, 3000));
    }

    return results;
}

差分検知——前回と比較する

const sharp = require('sharp');

/**
 * 2枚の画像のピクセル差分を計算する(簡易版)
 */
async function compareImages(img1Path, img2Path) {
    if (!fs.existsSync(img2Path)) return null;

    const [img1, img2] = await Promise.all([
        sharp(img1Path).raw().toBuffer({ resolveWithObject: true }),
        sharp(img2Path).raw().toBuffer({ resolveWithObject: true }),
    ]);

    // サイズが異なる場合は変化ありとみなす
    if (img1.info.width !== img2.info.width || img1.info.height !== img2.info.height) {
        return { changed: true, diffPercent: 100 };
    }

    let diffPixels = 0;
    const totalPixels = img1.info.width * img1.info.height;

    for (let i = 0; i < img1.data.length; i += 3) {
        const dr = Math.abs(img1.data[i]   - img2.data[i]);
        const dg = Math.abs(img1.data[i+1] - img2.data[i+1]);
        const db = Math.abs(img1.data[i+2] - img2.data[i+2]);
        if (dr + dg + db > 30) diffPixels++; // 閾値30
    }

    const diffPercent = (diffPixels / totalPixels * 100).toFixed(2);
    return { changed: diffPercent > 1.0, diffPercent: parseFloat(diffPercent) };
}

cron で定期実行

const cron = require('node-cron');
const sites = require('./config/sites');

// 毎日朝8時と夜20時に実行
cron.schedule('0 8,20 * * *', async () => {
    console.log(`[${new Date().toLocaleString('ja-JP')}] スクリーンショット撮影開始`);

    const results = await captureAll(sites);
    const successCount = results.filter(r => r.success).length;

    console.log(`完了: ${successCount}/${results.length} 件成功`);

    // 失敗があればSlack通知
    const failed = results.filter(r => !r.success);
    if (failed.length > 0) {
        await notifySlack(`スクリーンショット取得失敗: ${failed.map(f => f.site).join(', ')}`);
    }
}, { timezone: 'Asia/Tokyo' });

まとめ

Puppeteerによる定期スクリーンショットは、競合サイト監視・自社サイトの品質確認・ビジュアルリグレッションテストに活用できる強力なツールです。弊社では顧客のEC運用支援として競合モニタリングシステムを構築・提供しています。

Webスクレイピング・監視ツールの開発についてはお気軽にご相談ください。

Category 技術ブログ

Related Posts

関連記事

開発・技術のご相談はお気軽に

お見積りは無料です。まずはお気軽にご相談ください。

お問い合わせ →