0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Puppeteer + ImageMagick で「過去のWebサイトTOPデザイン」をサムネ生成→1枚PNGに時系列整列する

Last updated at Posted at 2025-09-02

2010ss〜2025ss の hotsuits.jp トップページを自動で撮影し、1枚のPNG に並べる手順とスクリプトを公開します。
失効URLは Wayback Machine のスナップショットへ自動フォールバックします。

  • スクショ取得: Puppeteer (Headless Chrome)
  • 画像合成: ImageMagickmontage
  • 動作確認: macOS (Apple/Intel), Node v18/20
    Hotsuits_Top_Timeline.jpg

TL;DR

# 1) 準備
mkdir -p hotsuits_timeline && cd hotsuits_timeline
npm init -y >/dev/null 2>&1 && npm i puppeteer
brew list imagemagick >/dev/null 2>&1 || brew install imagemagick

# 2) 取得スクリプトを作る
$EDITOR capture_thumbs.mjs  # 下のコードをコピペ保存

# 3) スクショ実行
node capture_thumbs.mjs

# 4) 合成(1枚PNGに)
magick montage hotsuits_shots/*.png \
  -tile 6x -geometry 480x300+20+50 -background white \
  -set label '%t' -font Helvetica -pointsize 20 \
  -title "hotsuits.jp TOP (2010ss–2025ss)" \
  Hotsuits_Top_Timeline.png

1. 前提

  • Node.js: v18 以上fetch を使うため)
  • Homebrew(macOS)
  • ImageMagick(montage コマンドを使います)

Windows: Chocolatey や winget で ImageMagick を入れてください。open の代わりに start
Linux: sudo apt-get install imagemagick など。


2. 対象サイトと時系列リスト

今回は下記の hotsuits.jp を例にします。
時系列スラグ(2010ss〜2025ss)は配列で定義します。

https://www.hotsuits.jp/
2010ss / 2010aw / 2011ss / ... / 2025ss

3. スクリーンショット取得スクリプト(Puppeteer)

capture_thumbs.mjs を作成して、以下を丸ごとコピペ保存してください。

// capture_thumbs.mjs
import fs from 'fs/promises';
import path from 'path';
import puppeteer from 'puppeteer';

const base = 'https://www.hotsuits.jp/';
const slugs = [
  '2010ss','2010aw','2011ss','2011aw','2012ss','2012aw','2013ss','2013aw',
  '2014ss','2014aw','2015ss','2015aw','2016ss','2016aw','2017ss','2017aw',
  '2018ss','2018aw','2019ss','2019aw','2020ss','2020aw','2021ss','2021aw',
  '2022ss','2022aw','2023ss','2023aw','2024ss','2024aw','2025ss'
];

const outDir = 'hotsuits_shots';
const viewport = { width: 1280, height: 900, deviceScaleFactor: 2 };
const delayMs = 2500;

const sleep = (ms) => new Promise(r => setTimeout(r, ms));

/** Wayback Machine の最新スナップショットURLを返す */
async function findWayback(targetUrl) {
  try {
    const api = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&filter=statuscode:200&collapse=digest`;
    const res = await fetch(api);
    const data = await res.json();
    // data[0] はヘッダ行。2行目以降がレコード。timestamp は 2番目の要素。
    if (Array.isArray(data) && data.length > 1) {
      const last = data[data.length - 1];
      const ts = last[1]; // timestamp
      return `https://web.archive.org/web/${ts}/${targetUrl}`;
    }
  } catch {
    // noop
  }
  return null;
}

async function capture() {
  await fs.mkdir(outDir, { recursive: true });

  const browser = await puppeteer.launch({
    headless: true,
    defaultViewport: viewport,
    args: ['--no-sandbox','--lang=ja-JP']
  });
  const page = await browser.newPage();
  await page.setUserAgent(
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'
  );
  page.setDefaultNavigationTimeout(60000);

  for (const slug of slugs) {
    const dest = path.join(outDir, `${slug}.png`);
    let url = `${base}${slug}/`;
    let usedWayback = false;

    try {
      const resp = await page.goto(url, { waitUntil: 'networkidle0' });
      if (!resp || resp.status() >= 400) throw new Error(`HTTP ${resp?.status()}`);
    } catch {
      const wb = await findWayback(url);
      if (wb) {
        usedWayback = true;
        await page.goto(wb, { waitUntil: 'networkidle0' });
      } else {
        console.error(`[SKIP] ${slug}: live & wayback not available`);
        continue;
      }
    }

    // フォント読み込み(対応ブラウザのみ)+描画安定待ち
    await page.evaluate(() => (document.fonts && document.fonts.ready) ? document.fonts.ready : null);
    await sleep(delayMs);

    // 先頭に戻して撮影(全体が良ければ fullPage: true)
    await page.evaluate(() => window.scrollTo(0, 0));
    await page.screenshot({ path: dest }); // { path: dest, fullPage: true } にすると縦長キャプチャ
    console.log(`[OK] ${slug} ${usedWayback ? '(Wayback)' : ''}`);
  }

  await browser.close();
}

capture().catch(e => { console.error(e); process.exit(1); });

実行

node capture_thumbs.mjs
  • 正常なら hotsuits_shots/2010ss.png … が並びます
  • 404/タイムアウトは Wayback Machine に自動フォールバック
  • それもなければ [SKIP] 表示

4. 画像を1枚に合成(ImageMagick)

montage でグリッド整列+ラベル付与します。
%t は拡張子を除いたファイル名(=2010ss など)がラベルに入ります。

magick montage hotsuits_shots/*.png \
  -tile 6x -geometry 480x300+20+50 -background white \
  -set label '%t' -font Helvetica -pointsize 20 \
  -title "hotsuits.jp TOP (2010ss–2025ss)" \
  Hotsuits_Top_Timeline.png

macOS なら open Hotsuits_Top_Timeline.png、Windows は start, Linux は xdg-open


5. そのまま使えるワンコマンド版(全部自動)

記事を丸ごと1発で動かしたい人向け。
コピペ→Enter で最後に Hotsuits_Top_Timeline.png まで出ます。

bash -c '
set -e
mkdir -p hotsuits_timeline && cd hotsuits_timeline
command -v node >/dev/null || { echo "Node.js v18+ が必要です"; exit 1; }
npm init -y >/dev/null 2>&1
npm i puppeteer >/dev/null 2>&1
if ! command -v magick >/dev/null; then
  if command -v brew >/dev/null; then brew install imagemagick; else
    echo "ImageMagick を入れてください(magick コマンドが必要)"; exit 1;
  fi
fi
cat > capture_thumbs.mjs << "EOF"
import fs from "fs/promises";
import path from "path";
import puppeteer from "puppeteer";
const base = "https://www.hotsuits.jp/";
const slugs = ["2010ss","2010aw","2011ss","2011aw","2012ss","2012aw","2013ss","2013aw","2014ss","2014aw","2015ss","2015aw","2016ss","2016aw","2017ss","2017aw","2018ss","2018aw","2019ss","2019aw","2020ss","2020aw","2021ss","2021aw","2022ss","2022aw","2023ss","2023aw","2024ss","2024aw","2025ss"];
const outDir = "hotsuits_shots";
const viewport = { width: 1280, height: 900, deviceScaleFactor: 2 };
const delayMs = 2500;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function findWayback(targetUrl) {
  try {
    const api = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&filter=statuscode:200&collapse=digest`;
    const res = await fetch(api);
    const data = await res.json();
    if (Array.isArray(data) && data.length > 1) {
      const last = data[data.length - 1];
      const ts = last[1];
      return `https://web.archive.org/web/${ts}/${targetUrl}`;
    }
  } catch {}
  return null;
}
async function capture() {
  await fs.mkdir(outDir, { recursive: true });
  const browser = await puppeteer.launch({ headless: true, defaultViewport: viewport, args: ["--no-sandbox","--lang=ja-JP"] });
  const page = await browser.newPage();
  await page.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36");
  page.setDefaultNavigationTimeout(60000);
  for (const slug of slugs) {
    const dest = path.join(outDir, `${slug}.png`);
    let url = `${base}${slug}/`;
    let usedWayback = false;
    try {
      const resp = await page.goto(url, { waitUntil: "networkidle0" });
      if (!resp || resp.status() >= 400) throw new Error(`HTTP ${resp?.status()}`);
    } catch {
      const wb = await findWayback(url);
      if (wb) {
        usedWayback = true;
        await page.goto(wb, { waitUntil: "networkidle0" });
      } else {
        console.error(`[SKIP] ${slug}: live & wayback not available`);
        continue;
      }
    }
    await page.evaluate(() => (document.fonts && document.fonts.ready) ? document.fonts.ready : null);
    await sleep(delayMs);
    await page.evaluate(() => window.scrollTo(0, 0));
    await page.screenshot({ path: dest });
    console.log(`[OK] ${slug} ${usedWayback ? "(Wayback)" : ""}`);
  }
  await browser.close();
}
capture().catch(e => { console.error(e); process.exit(1); });
EOF
node capture_thumbs.mjs
magick montage hotsuits_shots/*.png -tile 6x -geometry 480x300+20+50 -background white -set label "%t" -font Helvetica -pointsize 20 -title "hotsuits.jp TOP (2010ss–2025ss)" Hotsuits_Top_Timeline.png
'

6. カスタマイズ

  • 列数-tile 6x-tile 5x など
  • サムネサイズ-geometry 480x300+20+50480x300 を調整
  • ラベル-set label '%t' はファイル名。2010 春夏 などにしたければ撮影時のファイル名を変えるか、montage 前に mogrify -set label "..." を使う
  • 全ページ縦長page.screenshot({ fullPage: true }) に変更
  • 高解像度deviceScaleFactor: 2 を 1/3/4 に変更
  • タイトル除去-title "..." を外す

7. トラブルシュート

  • TypeError: page.waitForTimeout is not a function
    → 廃止API。上記コードは sleep()setTimeout)で対応済み。
  • 画面が崩れる / 画像が真っ白
    delayMs を 4000–6000 に。waitUntil: "networkidle0" 維持。
  • 日本語ラベルが文字化け
    -font Helvetica を macOS の日本語フォント(例:-font "Hiragino Sans W3")に変更。
  • ImageMagick の警告
    magick -version でインストール確認。ポリシー制限が強い場合は policy.xml を見直し。

8. マナーと注意

  • 対象サイトの 利用規約robots.txt・トラフィック負荷に配慮してください(今回は数十ページ・1回実行想定)。
  • 公開物に使う場合、権利表記出典明記 を推奨します。

9. まとめ

  • Puppeteer でサクッとスクショ、ImageMagick で一発整列
  • 失効URLも Wayback に自動切替
  • ブランドやプロダクトの歴史を可視化するのに超便利

気に入ったらスターやLGTMお願いします。
改良版(PDF出力/縦長版/キャプションCSV対応)も需要があれば追記します!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?