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スクレイピング・業務自動化ツールの開発についてはお気軽にご相談ください。