株式会社WR

株式会社WR

WEB TOTAL CONSULTING

Puppeteerでファイルダウンロードを自動化する
ブログ一覧へ
技術ブログ

Puppeteerでファイルダウンロードを自動化する

Puppeteerを使って、ログインが必要なページのファイルダウンロードを自動化する方法を解説します。セッション管理・ダウンロードディレクトリ指定・完了待ちの実装例を紹介します。

Puppeteerでのファイルダウンロード——どこで詰まるか

Puppeteerを使ったブラウザ自動化でよくある要件が「管理画面からCSVをダウンロードして処理する」というものです。ところがPuppeteerはデフォルトでファイルダウンロードを扱いにくい設計になっており、以下の点で詰まるケースが多いです。

  • ダウンロードボタンをクリックしてもどこに保存されたかわからない
  • ダウンロード完了を待つ方法がない
  • ヘッドレスモードでダウンロードが動かないことがある

本記事では、これらを解決する実装パターンを紹介します。


環境構築

mkdir download-automation && cd download-automation
npm init -y
npm install puppeteer

ダウンロードディレクトリの指定

Page._client.send でChrome DevTools Protocolを使い、ダウンロード先を指定します。

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

async function setupDownload(page, downloadPath) {
    // ダウンロードディレクトリを確保
    if (!fs.existsSync(downloadPath)) {
        fs.mkdirSync(downloadPath, { recursive: true });
    }

    // Chrome DevTools Protocolでダウンロード先を設定
    await page._client().send('Page.setDownloadBehavior', {
        behavior: 'allow',
        downloadPath: downloadPath,
    });
}

ダウンロード完了の待機

ファイルのダウンロード中は .crdownload という拡張子の一時ファイルが作られます。このファイルが消えた時点でダウンロード完了とみなせます。

const { watch } = require('fs');

/**
 * ダウンロードの完了を待機する
 * @param {string} downloadPath - ダウンロードディレクトリ
 * @param {number} timeout - タイムアウト(ミリ秒)
 */
function waitForDownload(downloadPath, timeout = 30000) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            watcher.close();
            reject(new Error('ダウンロードがタイムアウトしました'));
        }, timeout);

        const watcher = watch(downloadPath, (event, filename) => {
            if (filename && !filename.endsWith('.crdownload')) {
                // .crdownload以外のファイルが現れたら完了
                clearTimeout(timer);
                watcher.close();
                resolve(path.join(downloadPath, filename));
            }
        });
    });
}

ログインが必要なサイトの実装例

実際の業務では、管理画面にログインしてからCSVをダウンロードするケースが多いです。

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

const DOWNLOAD_PATH = path.resolve('./downloads');

async function downloadReportCSV() {
    const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox'],
    });

    const page = await browser.newPage();

    try {
        // ダウンロード設定
        await setupDownload(page, DOWNLOAD_PATH);

        // ログイン
        await page.goto('https://admin.example.com/login', {
            waitUntil: 'networkidle2',
        });
        await page.type('#email', process.env.ADMIN_EMAIL);
        await page.type('#password', process.env.ADMIN_PASSWORD);
        await page.click('button[type="submit"]');
        await page.waitForNavigation({ waitUntil: 'networkidle2' });

        // レポートページに移動
        await page.goto('https://admin.example.com/reports/monthly', {
            waitUntil: 'networkidle2',
        });

        // 期間を選択(先月)
        const lastMonth = new Date();
        lastMonth.setMonth(lastMonth.getMonth() - 1);
        const yearMonth = `${lastMonth.getFullYear()}-${String(lastMonth.getMonth() + 1).padStart(2, '0')}`;
        await page.select('#month-selector', yearMonth);

        // ダウンロード開始を並行して待機
        const [downloadedFile] = await Promise.all([
            waitForDownload(DOWNLOAD_PATH),
            page.click('#download-csv-btn'),
        ]);

        console.log(`ダウンロード完了: ${downloadedFile}`);
        return downloadedFile;

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

ダウンロードしたCSVをNode.jsで処理する

const csv = require('csv-parser');

async function processCSV(filePath) {
    const results = [];

    return new Promise((resolve, reject) => {
        fs.createReadStream(filePath)
            .pipe(csv())
            .on('data', (row) => results.push(row))
            .on('end', () => resolve(results))
            .on('error', reject);
    });
}

// 実行
(async () => {
    const file = await downloadReportCSV();
    const data = await processCSV(file);

    console.log(`${data.length}件のレコードを取得`);

    // 処理済みファイルを移動
    const archivePath = path.join('./archive', path.basename(file));
    fs.renameSync(file, archivePath);
})();

複数ファイルのバッチダウンロード

複数月のレポートを一括でダウンロードする場合は、ページを再利用して連続処理します。

async function downloadMultipleReports(months) {
    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();
    await setupDownload(page, DOWNLOAD_PATH);

    await login(page); // ログイン処理

    const downloadedFiles = [];
    for (const month of months) {
        await page.goto(`https://admin.example.com/reports/${month}`);
        await page.waitForSelector('#download-btn');

        const [file] = await Promise.all([
            waitForDownload(DOWNLOAD_PATH),
            page.click('#download-btn'),
        ]);
        downloadedFiles.push(file);

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

    await browser.close();
    return downloadedFiles;
}

まとめ

PuppeteerでのCSVダウンロード自動化は、setDownloadBehavior でディレクトリを指定し、ファイルシステム監視でダウンロード完了を検知するのが定石です。弊社では楽天・Amazon管理画面からの受注CSVの自動ダウンロードとDBへのインポートを実装・運用しています。

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

Category 技術ブログ

Related Posts

関連記事

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

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

お問い合わせ →