PuppeteerでWebサイトのスクリーンショットを自動化する方法
JavaScriptとPuppeteerでWebサイトのスクリーンショットを自動化します。1ページのキャプチャ、フルページ撮影、URLの一括処理、モバイル表示幅を、再利用可能なスクリプトで扱います。
13分で読めますWebサイトキャプチャ作成者:Temposhot Team
Puppeteerは、Chromeチームが管理するNode.jsライブラリです。JavaScript APIでChromeまたはFirefoxを操作し、スクリプトによるWebサイトのスクリーンショットによく使われます。以下の例は、5行のキャプチャから始めて、URLをファイルから読み込み、ページを並列に処理してレポートを書き出す再利用可能なランナーへと段階的に拡張します。
最後のセクションでは、同じ一括処理をMac上のTemposhot Website Captureの実行として示します。保守するスクリプトはありません。両方の方法で同等のファイルが得られます。どちらを選ぶかは、処理をどこで実行するかによります。
Puppeteerが適したケース
Puppeteerは、スクリーンショットを既存のコードの中で実行する場合に適しています。
- プレビューやソーシャル画像を生成するNode.jsサービス
- デプロイ後にページをキャプチャするcronジョブまたはCIのステップ
- 視覚的な記録も必要なスクレイピング処理
- Chrome DevTools Protocolの機能を既に使っているチーム
Node.jsプロジェクトを保守していない人が一度だけ実行する処理には向きません。
スクリーンショットスクリプト用にPuppeteerをインストールする
mkdir site-shots && cd site-shots
npm init -y
npm install puppeteer
puppeteerパッケージは、インストール時に対応するChromeビルドをダウンロードします。軽量なpuppeteer-coreパッケージはこのダウンロードを省略し、代わりにインストール済みブラウザーのパスを必要とします。以下のスクリプトは、トップレベルのawaitを使えるように.mjsファイルを使います。
puppeteerとpuppeteer-coreの選び方
ダウンロードが許可されている開発用マシンではpuppeteerを使います。Chromeが既にインストールされているCIイメージや、ブラウザーのダウンロードがブロックされているネットワークではpuppeteer-coreを使います。両パッケージのAPIは同じなので、スクリプトは変わりません。ブラウザーが独自の場所にある場合にのみ、起動処理にexecutablePathが必要です。
1ページのWebサイトスクリーンショットをキャプチャする
// one.mjs
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'example.png' });
await browser.close();
page.setViewportは幅と高さを設定します。同じ呼び出しにdeviceScaleFactor: 2を追加すると、Retinaディスプレイに近い2倍のピクセル密度の画像になります。
設定が生成するもの
上記のスクリプトにdeviceScaleFactor: 2を追加した場合、出力サイズは固定です。数値はビューポートと拡大率から決まります。
出力は、拡大率がピクセル数を掛ける一方でCSSサイズは変わらないため、各方向でビューポートの2倍になります。スケール1の1440 x 900ビューポートは1440 x 900の画像を生成します。これはWebプレビューに適したサイズです。拡大率は高密度ディスプレイで鮮明な画像を得るための設定で、その分ファイルサイズも増えます。
フルページスクリーンショットをキャプチャする
await page.screenshot({ path: 'example-full.png', fullPage: true });
fullPage: trueを指定すると、スクロール可能なドキュメント全体を撮影します。ファイルサイズを小さくするには、JPEGまたはWebPで保存します。
await page.screenshot({ path: 'example.jpg', type: 'jpeg', quality: 80, fullPage: true });
qualityは0から100の値を受け付け、JPEGとWebPにのみ適用されます。長いページのフルページスクリーンショットは高さが数万ピクセルになることがあるため、JPEGを使うとファイルサイズを抑えやすくなります。
長いページのサイズ
幅はビューポートの幅のまま、高さはドキュメントの長さに従います。長い記事では画像が非常に縦長になります。以下の数値は、その変化の規模を示します。
3840 x 2160フレームは約830万ピクセルなので、例の画像は4Kフレームのおよそ2倍の大きさです。縦に長い画像は一枚で見るのが難しいため、画面や文書で表示する場合は、clipを使って非常に長いページを区間に分割します。
1つのセクションをクリップする
clipオプションで1つの領域を保存します。座標はビューポートではなく、ページ上端からのCSSピクセルです。
await page.screenshot({
path: 'example-pricing.png',
clip: { x: 0, y: 2400, width: 1440, height: 900 },
});
1回の呼び出しでは、fullPageとclipのどちらか一方のみを使います。長いページを複数のファイルに分けるには、document.body.scrollHeightで高さを取得し、区間ごとに1回のclipを実行します。12000ピクセルのページを900ピクセルごとに分割すると14ファイルになり、最後のファイルは300ピクセルだけになります。
再利用可能なURLランナーを作る
// run.mjs
import { readFile, mkdir, writeFile } from 'node:fs/promises';
import puppeteer from 'puppeteer';
const urls = (await readFile('urls.txt', 'utf8'))
.split('\n').map((l) => l.trim()).filter(Boolean);
const outDir = `shots/${new Date().toISOString().slice(0, 10)}`;
await mkdir(outDir, { recursive: true });
const browser = await puppeteer.launch();
async function shoot(url, index) {
const page = await browser.newPage();
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
const path = `${outDir}/${String(index + 1).padStart(3, '0')}.jpg`;
await page.screenshot({ path, type: 'jpeg', quality: 80, fullPage: true });
return { url, path, ok: true };
} finally {
await page.close();
}
}
URLごとに新しいページを開くため、あるサイトでクラッシュが起きても次のサイトには影響しません。日付付きの出力フォルダーにより、以前の実行結果は残ります。
このランナーは5つの段階で構成されます。以下の図は各段階を順に示し、それぞれが扱うファイルまたはプロセスを示します。
URLリストを整理する
空行、コメント、重複したURLはブラウザーの時間を無駄にし、重複すると同じページのファイルが二重に生成されます。以下のバージョンは、#で始まる行をスキップし、完全に一致する重複を削除し、HTTPまたはHTTPSではないアドレスを除外します。rawは前の手順で読み込んだファイルの内容を保持します。
const lines = raw.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
const urls = [...new Set(lines)].filter((l) => {
try {
const { protocol } = new URL(l);
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
});
Setは文字列を完全一致で比較するため、https://example.comとhttps://example.com/は別々のURLとして残ります。却下された行もログに記録します。htps://example.comのような誤字は、45秒後のタイムアウトとしてではなく、実行開始時にログへ表示されます。
大量バッチの番号付け
スクリプトはインデックスを3桁に揃えるため、999ファイルまでは正しく並びます。インデックス1000は1000.jpgになり、文字列順のソートでは100.jpgと101.jpgの間に並びます。代わりにバッチの桁数に合わせて揃えます。
const width = String(urls.length).length;
const path = `${outDir}/${String(index + 1).padStart(width, '0')}.jpg`;
1,250件のURLのバッチでは桁数は4になり、最初のファイルは0001.jpgになります。桁数はワーカーを起動する前に一度だけ計算します。
大きなバッチ向けに並列数を増やす
const LIMIT = 4;
const queue = [...urls.entries()];
const results = [];
async function worker() {
while (queue.length) {
const [i, url] = queue.shift();
results.push(await shootWithRetry(url, i));
}
}
await Promise.all(Array.from({ length: LIMIT }, worker));
await browser.close();
各ワーカーは共有キューから次のURLを取り出します。並列数は4から始め、メモリを確認してから上限を上げます。開いている各ページは、それぞれ独自のレンダラープロセスを使うためです。
スクリプトは1つのブラウザーからすべてのページを開きます。ページ同士はブラウザーのキャッシュを共有し、既定ではCookieも共有します。無関係な公開サイトのバッチでは問題ありません。ある実行で1つのサイトの状態を次のサイトに持ち込んではならない場合は、browser.createBrowserContext()でURLごとにコンテキストを作成し、キャプチャ後に閉じます。各コンテキストはメモリを消費するため、この選択は並列数の上限と合わせて決めます。
上限の選び方
上限は同時にレンダリングするページ数を決めます。上限を低くするとメモリ使用量は一定に保たれますが、実行時間は長くなります。上限を高くすると逆になります。以下の表は、1ページあたり平均約6秒かかる200件のバッチでのトレードオフを示します。
理想値は、ネットワーク待機と失敗がないことを前提にしています。実際のサイトでは差があり、タイムアウトと再試行の分だけ時間が増えます。対象のマシンで20件のバッチを2つの上限で計測し、Macの操作性を保てる最大の上限を選びます。
モバイル向けスクリーンショットをキャプチャする
Puppeteerは、KnownDevicesを通じてデバイス記述子を提供します。
import puppeteer, { KnownDevices } from 'puppeteer';
const page = await browser.newPage();
await page.emulate(KnownDevices['iPhone 13']);
await page.goto('https://example.com');
page.emulateは、1回の呼び出しでビューポート、ピクセル比、タッチ対応、User Agentを設定します。User Agentを変更せずにモバイル幅だけを指定する場合は、setViewportをisMobile: trueとhasTouch: true付きで呼び出します。
サイトがUser Agentに応じて異なるマークアップを返す場合はemulateを使います。レイアウトの幅だけが重要な場合はsetViewportを使います。どちらの呼び出しも実行したページにのみ適用されるため、後から開く2つ目のページには、それぞれ個別の呼び出しが必要です。
デスクトップとモバイルが混在するバッチでは、スクリプトごとではなく出力セットごとに1つのビューポートが必要です。モバイル画像はshots/2026-04-10-mobileのような独自の日付付きフォルダーに保存し、2つのセットのファイル番号が衝突しないようにします。Temposhot Website Captureも同じ目的で、390 x 844のMobileサイズを使い、同じ実行内で行ごとに設定します。
エミュレートされたビューポートを確認する
キャプチャの前に、ページ内のレイアウト幅を確認します。記述子が誤っていると、モバイルサイズの画像の中にデスクトップのレイアウトが表示されます。以下のコードは、ページが認識している幅とピクセル比を取得します。
const seen = await page.evaluate(() => ({
width: window.innerWidth,
pixelRatio: window.devicePixelRatio,
}));
console.log(seen);
最近のPuppeteerリリースでのiPhone 13記述子では、幅は390になるはずです。ピクセル比も記述子から取得されます。記述子のデータはリリースごとに変わるため、インストールするバージョンで値を確認します。
遅延読み込みされたコンテンツを扱う
スクロールに応じて読み込まれる画像は、ナビゲーション直後に撮影したキャプチャでは空白のままになります。先にページをスクロールします。
await page.evaluate(async () => {
for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight) {
window.scrollTo(0, y);
await new Promise((r) => setTimeout(r, 200));
}
window.scrollTo(0, 0);
});
このパターンは、遅延読み込みの項目で説明しています。
スクロールにかかる時間
ループは1ステップごとに200ミリ秒待機し、各ステップでビューポートの高さ分だけ移動します。以下の例は、ビューポートの高さ900ピクセルで8000ピクセルのページです。
ページには9つのステップがあるため、スクロールの処理によってスクリーンショットの前に1.8秒が加わります。画像の読み込みが遅いページでは、1ステップごとの待機時間を長くする必要があります。無限スクロールのページには固定の終点がないため、ドキュメントの高さではなく、固定のステップ数で停止します。
内部コンテナをスクロールする
一部のレイアウトは、ウィンドウではなく内部の要素をスクロールします。上記のループでは動きが検出されず、ページ下部が空白のままになります。代わりに、要素のscrollTopでスクロールし、その要素のscrollHeightとclientHeightを上限にします。実行後は、最初のフレームと最後のフレームを比較します。2つのフレームが同一の場合はページがスクロールされていないことを示し、スクロールコンテナが使われている可能性があります。
適切な待機戦略を選ぶ
| オプション | 待機が完了する条件 |
|---|---|
load | loadイベントが発火したとき。既定値です。 |
domcontentloaded | HTMLの解析が完了したとき。 |
networkidle0 | 500ミリ秒以上ネットワーク接続がない状態が続いたとき。 |
networkidle2 | 500ミリ秒以上接続数が2件以下の状態が続いたとき。 |
networkidle0は、ポーリングやストリーミングを行うページでタイムアウトすることがあります。networkidle2は、そのようなバックグラウンド通信を許容します。ページに明確な準備完了の信号がある場合は、その要素に対するpage.waitForSelectorが最も確実です。既定のナビゲーションタイムアウトは30秒です。
コンテンツの準備が整ったときにのみページが設定する信号を1つ選びます。ヘッダーのように、どのページの読み込みでも存在するセレクターは、その下のデータが準備できたことを示しません。
待機を選ぶ前に計測する
待機戦略を決める前に、URLごとのナビゲーション時間を計測します。以下の値は、loadイベントが発火するまでの時間です。
const started = Date.now();
await page.goto(url, { waitUntil: 'load', timeout: 45000 });
const loadMs = Date.now() - started;
各URLのloadMsをレポートに記録します。ほとんどのページが数秒以内に読み込まれるなら、既定の待機で問題ありません。ページ群がタイムアウトの近くにある場合は、それらのURLをdomcontentloadedに切り替えてセレクターを待機させ、変更の効果を確認するためにレポートを残します。
再試行とレポートを追加する
async function shootWithRetry(url, i, tries = 3) {
for (let attempt = 1; attempt <= tries; attempt++) {
try {
return await shoot(url, i);
} catch (error) {
if (attempt === tries) return { url, ok: false, error: error.message };
}
}
}
// after Promise.all in run.mjs
await writeFile(`${outDir}/report.json`, JSON.stringify(results, null, 2));
レポートには、すべてのURLについてファイルパスまたはエラーが記録されます。失敗したエントリはurls.txtに戻し、2回目の処理に使います。
レポートから再試行リストを作る
レポートがあると、2回目の処理は手作業のコピーではなくフィルターになります。次のjqコマンドは、失敗したURLを新しいファイルに書き出します。
jq -r '.[] | select(.ok == false) | .url' shots/2026-04-10/report.json > urls-retry.txt
2回目の処理では、スクリプトがurls-retry.txtを読むように指定します。スクリプトは出力フォルダーを日付で命名するため、同じ日に2回目の実行を行うと同じフォルダーに書き込まれます。同じ番号のファイルは、1回目の実行の画像を上書きします。2回目の処理の前に、outDirに-retryなどの接尾辞を追加し、両方の実行結果がフォルダーに残るようにします。
最悪の場合の実行時間
遅いサイトでは、すべての再試行を使い切ることがあります。タイムアウトと再試行回数により、1つのURLの最悪の場合が決まり、並列数がその負荷をワーカー間に分散します。以下の図は、スクリプトの値を使っています。
健全なバッチでは最悪の場合はまず起きませんが、上限を示します。タイムアウトが多い実行でも、次の実行が始まる前に終わるようにジョブをスケジュールします。
変化する可能性のあるエラーだけを再試行する
ナビゲーションのタイムアウトや接続の切断は、2回目の試行で成功することがあります。ページが完全に読み込まれても期待するセレクターがない場合や、404ステータスを返すページは、通常は同じように失敗します。前者は再試行し、後者は一度だけ記録します。上記のshootWithRetry関数はすべてのエラーを再試行するため、恒久的な失敗では、レポートに表示されるまでに3回のフルタイムアウトが発生します。
実用的なフォルダー構成
site-shots/
run.mjs
urls.txt
shots/
2026-04-10/
001.jpg
002.jpg
report.json
実行日ごとに1つのフォルダー、フォルダーごとに1つのレポートです。データベースがなくても、後から実行結果を比較するには、この構成で十分です。
よくある間違い
- すべてのサイトで
networkidle0を使う。 分析ツールやチャットウィジェットを含むページは、接続が落ち着きません。 - ページを開いたままにする。 メモリが増え続け、プロセスが遅くなるかクラッシュします。
- 同時に多くのページを処理する。 ノートPCで上限を20にすると、実行が速くなるどころか、タイムアウトが増えることがよくあります。
- 同意オーバーレイをキャプチャする。 セレクターで拒否ボタンをクリックするか、
page.addStyleTagでバナーを非表示にします。 - サイトの利用規約を確認しない。 自動キャプチャは実際のリクエストを送信します。各サイトの利用規約を確認し、リクエストの頻度を低く保ちます。
症状別のトラブルシューティング
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| フルページ画像の空白領域 | 遅延読み込みされたコンテンツが表示範囲にスクロールされていない | キャプチャの前にスクロール処理を実行する |
| gotoでタイムアウト | ページが待機イベントに到達しない | セレクターを待機するか、domcontentloadedに切り替える |
| すべてのURLで同じ画像 | ナビゲーションが完了する前にキャプチャが実行された | 各ページでgotoをawaitし、URLごとに新しいページを開く |
| 画像が想定の2倍のサイズ | deviceScaleFactorが2 | 係数を削除するか、1に設定する |
| 長時間の実行でメモリが増える | キャプチャ後もページが開いたまま | finallyブロックで各ページを閉じ、並列数を制限する |
| 画像に同意バナーが写る | バナーが処理されていない | セレクターで拒否ボタンをクリックするか、バナーを非表示にする |
Puppeteerスクリプトの代替手段
- Playwrightは類似のAPIを持ち、WebKitも操作できます。関連記事はPlaywrightでWebサイトのスクリーンショットを自動化する方法です。
- ホスト型のスクリーンショットAPIは、ブラウザーの実行を代行し、HTTPで画像を返します。
- ブラウザー拡張機能、たとえばTemposhot拡張機能は、アクティブなタブのフルページPNGを1枚保存します。
- Temposhot Website Captureは、貼り付けたURLリストをMac上でコードなしで実行します。サイズはDesktop 1920×1080、Tablet 768×1024、Mobile 390×844から選択でき、行ごとに0秒から60秒の遅延を設定できます。
スクリプトが割に合わない場合
スクリプトのコストは、最初のバージョンで見えるより大きくなります。サイトの変更、Node.jsのアップグレード、Chromeの更新のたびに修正が必要になることがあります。URLリストが頻繁に変わる月次ジョブは、URLリストと行の設定が1つのウィンドウにまとまっているWebsite Captureの方がコストが低くなります。スクリプトは、画像をビルドのステップで使う場合、CIパイプラインで実行する必要がある場合、または訪問とキャプチャを超える操作が必要な場合に残します。
同じバッチをTemposhot Website Captureで実行する
Temposhot Website Captureは、この記事のバッチ処理をスクリプトなしで実行できます。以下の設定は、1440 x 900のフルページキャプチャのランナーと一致します。待機条件とブロック設定は、Temposhotが行ごとに提供する項目を使います。
デバイスのサイズは、スクリプトと意図的に異なります。TemposhotのDesktopは1920 x 1080で、スクリプトは1440 x 900を使います。出力を一致させる場合は、両方で同じビューポートを設定します。Temposhotの実行後は、結果バーのZIP、Images、PDFのいずれかを選択すると、選択したフレームを書き出せます。
バッチの実行中、各行はCapturingやCapture failedなどの状態を表示します。失敗した行は、残りのバッチを再開せずに、その行だけ再試行できます。
FAQ
PuppeteerはFirefoxで動作しますか?
はい。最近のバージョンは、ChromeだけでなくFirefoxも起動できます。利用者が最も多いのはChromeです。
PuppeteerでWebP形式のスクリーンショットを保存できますか?
はい。スクリーンショットのオプションでtype: 'webp'を設定します。qualityの値は、JPEGと同様にWebPにも適用されます。
一部のサイトでpage.gotoがタイムアウトするのはなぜですか?
ページが30秒以内に、選択した待機イベントに到達しなかったためです。networkidle2またはloadに切り替えるか、timeoutを延長するか、代わりにセレクターを待機します。
フルページ画像の一部が空白になるのはなぜですか?
コンテンツは表示範囲にスクロールされたときに読み込まれます。キャプチャの前に、遅延読み込みのセクションにあるスクロール処理を実行し、最後の行が表示されるまで待機します。
1回の実行で何件のURLを処理できますか?
上限はコードではなく、メモリと時間によって決まります。使用する並列数で20件のバッチを計測し、メモリを監視しながら規模を広げます。Temposhot Website Captureでは、Concurrent browsersを1から10の範囲で設定でき、既定値は4です。
スクリーンショットの出力をGitに含めるべきですか?
含めません。画像とレポートは、実行ごとに増えるビルドの出力です。urls.txtとスクリプトはバージョン管理に保持し、出力フォルダーは、そのサイズに合う場所に保存します。
参考資料
コードなしでWebサイトのスクリーンショットを自動化しますか?
Temposhot Website Captureは、同じ種類のURLバッチをMac上でローカルに実行します。行ごとの設定があり、保守するスクリプトはありません。Webサイトのスクリーンショット自動化またはキャプチャオプションのガイドを参照します。
puppeteerwebsite-captureautomationnodejs
このページで使用する用語: full page screenshot, headless browser, playwright, puppeteer, screenshot api, screenshot automation, viewport screenshot
繰り返し撮影するスクリーンショットを、完成したデータに仕上げます
Temposhotは、タイマーまたはクリックごとに撮影し、結果をPDF、画像、ZIP、MP4で書き出します。
関連記事
- Macでウェブサイトのスクリーンショットを一括で撮影する方法MacでTemposhotを使い、ウェブサイトのスクリーンショットを一括で撮影します。URLリストを貼り付け、デスクトップ・タブレット・モバイルのプリセットを選び、並列撮影を実行して監査やレポート用に書き出せます。
- Playwright で Webサイトのスクリーンショットを自動化する方法Playwright と JavaScript で Webサイトのスクリーンショットを自動化します。1ページの取得、ページ全体、モバイル表示、リトライ付きの URL バッチを扱います。
- ノーコードでWebサイトのスクリーンショットを自動化する方法ノーコードでWebサイトのスクリーンショットを自動化する方法を選びます。URL一覧からのキャプチャ、スクリーンショットAPIを使うオートメーションプラットフォーム、監視ツール、ブラウザーの補助ツールを扱います。
- 2026 年版: Mac 向け Xnapper の代替アプリ比較Mac 向け Xnapper の代替アプリを、繰り返し撮影、スクロールキャプチャと録画、無料の背景、クロスプラットフォームのガイド、標準搭載アプリの観点で比較します。