定期スクリーンショットによるサイト監視
競合サイトや自社サイトの変化を継続的に追跡する方法として、定期スクリーンショットは非常に効果的です。ページのデザイン変更・価格表示の変化・セールバナーの掲出など、テキストデータだけでは捉えにくい変化を視覚的に記録できます。
活用シーン:
- 競合他社のトップページ・価格ページの変化追跡
- 自社サイトのデプロイ後の表示確認(ビジュアルリグレッションテスト)
- 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スクレイピング・監視ツールの開発についてはお気軽にご相談ください。