株式会社WR

株式会社WR

WEB TOTAL CONSULTING

Node.jsでcronジョブを実装——定期的なデータ収集の自動化
ブログ一覧へ
技術ブログ

Node.jsでcronジョブを実装——定期的なデータ収集の自動化

node-cronライブラリを使ったcronジョブの実装方法と、スクレイピング結果のデータベース保存・エラー通知まで含めた実践的な定期実行の仕組みを解説します。

Node.jsでのcronジョブ——なぜ必要か

データ収集・レポート生成・期限チェックなど、「定期的に実行したい処理」はWebアプリに必ずと言っていいほど存在します。Node.jsでこれを実現する主な方法は以下の3つです。

  1. node-cron:npm packageで軽量に実装
  2. Agenda:MongoDBベースのジョブスケジューラー
  3. BullMQ:Redisベースの本格的なジョブキュー

本記事では最も手軽な node-cron を中心に解説し、実務で使えるパターンを紹介します。


node-cronのインストールと基本

npm install node-cron axios dotenv
const cron = require('node-cron');

// cron式:秒 分 時 日 月 曜日
// *      *  *  *  *  *

// 毎分実行
cron.schedule('* * * * *', () => {
    console.log('毎分実行:', new Date().toISOString());
});

// 毎朝9時に実行
cron.schedule('0 9 * * *', () => {
    console.log('毎朝9時に実行');
}, {
    timezone: 'Asia/Tokyo',
});

// 毎週月曜0時に実行
cron.schedule('0 0 * * 1', () => {
    console.log('毎週月曜0時に実行');
}, { timezone: 'Asia/Tokyo' });

cron式の読み方

┌───────── 秒(0〜59)※省略可
│ ┌─────── 分(0〜59)
│ │ ┌───── 時(0〜23)
│ │ │ ┌─── 日(1〜31)
│ │ │ │ ┌─ 月(1〜12)
│ │ │ │ │ ┌ 曜日(0〜7、0と7が日曜)
│ │ │ │ │ │
* * * * * *

よく使うパターン:
"0 * * * *"       → 毎時0分
"0 9 * * *"       → 毎日9時
"0 9 * * 1-5"     → 平日9時
"0 0 1 * *"       → 毎月1日0時
"*/15 * * * *"    → 15分ごと
"0 9,18 * * *"    → 9時と18時

実践:ECサイトの価格監視ジョブ

require('dotenv').config();
const cron = require('node-cron');
const axios = require('axios');
const { JSDOM } = require('jsdom');

// 監視対象商品リスト
const watchList = [
    {
        name: 'ワイヤレスイヤホン A',
        url: 'https://example-ec.com/products/1234',
        selector: '.product-price',
        targetPrice: 8000,
    },
    // ...
];

async function scrapePrices() {
    const results = [];

    for (const item of watchList) {
        try {
            const response = await axios.get(item.url, {
                headers: { 'User-Agent': 'Mozilla/5.0 (compatible; PriceBot/1.0)' },
                timeout: 10000,
            });

            const dom = new JSDOM(response.data);
            const priceText = dom.window.document.querySelector(item.selector)?.textContent ?? '';
            const price = parseInt(priceText.replace(/[^\d]/g, ''), 10);

            results.push({ ...item, currentPrice: price });

            // 目標価格以下になったらSlack通知
            if (price > 0 && price <= item.targetPrice) {
                await notifySlack(item, price);
            }

            // 礼儀正しい待機
            await sleep(1500 + Math.random() * 1000);

        } catch (error) {
            console.error(`Error fetching ${item.url}:`, error.message);
        }
    }

    return results;
}

async function notifySlack(item, price) {
    const payload = {
        text: [
            `:tada: *価格アラート*`,
            `商品: ${item.name}`,
            `現在価格: ¥${price.toLocaleString()}`,
            `目標価格: ¥${item.targetPrice.toLocaleString()}`,
            `URL: ${item.url}`,
        ].join('\n'),
    };

    await axios.post(process.env.SLACK_WEBHOOK_URL, payload);
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// 1時間ごとに価格チェック
cron.schedule('0 * * * *', async () => {
    console.log(`[${new Date().toLocaleString('ja-JP')}] 価格チェック開始`);
    const results = await scrapePrices();
    console.log(`チェック完了: ${results.length}件`);
}, { timezone: 'Asia/Tokyo' });

console.log('価格監視ボット起動');

ジョブの排他制御——重複実行を防ぐ

ジョブの実行時間が長い場合、次の実行時刻までに終わらず重複実行が起きることがあります。

let isRunning = false;

cron.schedule('*/30 * * * *', async () => {
    if (isRunning) {
        console.log('前回のジョブがまだ実行中です。スキップします。');
        return;
    }

    isRunning = true;
    try {
        await runHeavyJob();
    } finally {
        isRunning = false;
    }
});

ジョブの結果をDBに記録する

const mysql = require('mysql2/promise');

async function saveJobResult(jobName, status, details) {
    const conn = await mysql.createConnection(process.env.DATABASE_URL);
    await conn.execute(
        'INSERT INTO job_logs (job_name, status, details, executed_at) VALUES (?, ?, ?, NOW())',
        [jobName, status, JSON.stringify(details)]
    );
    await conn.end();
}

// ジョブ内で使用
cron.schedule('0 3 * * *', async () => {
    const startedAt = Date.now();
    try {
        const result = await runDataCollection();
        await saveJobResult('data_collection', 'success', {
            collected: result.length,
            duration: Date.now() - startedAt,
        });
    } catch (error) {
        await saveJobResult('data_collection', 'failure', {
            error: error.message,
            duration: Date.now() - startedAt,
        });
    }
});

PM2で本番運用

開発環境では node app.js でよいですが、本番環境ではPM2を使って常時起動します。

npm install -g pm2

# 起動
pm2 start app.js --name "price-bot"

# 自動起動設定
pm2 startup
pm2 save

# ログ確認
pm2 logs price-bot

# 再起動
pm2 restart price-bot

まとめ

node-cronを使えば、Node.jsアプリに定期実行処理を簡単に組み込めます。弊社ではEC価格監視・在庫チェック・データ収集など複数のcronジョブを本番稼働させています。PM2による常時起動と、Slack通知によるエラー監視を組み合わせることで、信頼性の高い自動化システムを実現しています。

業務自動化・データ収集システムの開発についてはお気軽にご相談ください。

Category 技術ブログ

Related Posts

関連記事

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

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

お問い合わせ →