🕷️

Axiosを使ったWebスクレイピング入門 - 基礎から実践まで

に公開

はじめに

Webスクレイピングは、Webサイトから情報を自動的に収集する技術です。本記事では、人気のHTTPクライアントライブラリであるAxiosを使用したWebスクレイピングの方法を解説します。

Axiosとは

AxiosはPromiseベースのHTTPクライアントで、ブラウザとNode.js環境の両方で動作します。シンプルなAPIと豊富な機能により、多くの開発者に支持されています。

Axiosの主な特徴

  • Promiseベースの非同期処理
  • リクエスト・レスポンスのインターセプト
  • 自動的なJSONデータ変換
  • タイムアウト設定
  • リクエストのキャンセル機能

環境構築

まず、必要なパッケージをインストールします。

npm init -y
npm install axios cheerio
  • axios: HTTPリクエストを送信するためのライブラリ
  • cheerio: HTMLを解析するためのjQueryライクなライブラリ

基本的なスクレイピングの実装

シンプルな例

以下は、Axiosを使った基本的なスクレイピングの例です。

const axios = require('axios');
const cheerio = require('cheerio');

async function scrapeWebsite(url) {
  try {
    // AxiosでHTMLを取得
    const response = await axios.get(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
      }
    });

    // CheerioでHTMLをパース
    const $ = cheerio.load(response.data);

    // タイトルを取得
    const title = $('title').text();
    console.log('ページタイトル:', title);

    // 特定の要素を取得
    const items = [];
    $('.article-item').each((index, element) => {
      const itemTitle = $(element).find('.title').text().trim();
      const itemLink = $(element).find('a').attr('href');
      items.push({ title: itemTitle, link: itemLink });
    });

    return items;
  } catch (error) {
    console.error('スクレイピングエラー:', error.message);
    throw error;
  }
}

// 使用例
scrapeWebsite('https://example.com')
  .then(data => console.log(data))
  .catch(err => console.error(err));

レート制限の実装

サーバーに負荷をかけないよう、リクエスト間に遅延を入れることが重要です。

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

async function scrapeMultiplePages(urls) {
  const results = [];
  
  for (const url of urls) {
    try {
      const data = await scrapeWebsite(url);
      results.push(data);
      
      // 1秒待機
      await sleep(1000);
    } catch (error) {
      console.error(`${url}のスクレイピングに失敗:`, error.message);
    }
  }
  
  return results;
}

エラーハンドリングとリトライ

実際の運用では、ネットワークエラーやタイムアウトに対応する必要があります。

async function scrapeWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await axios.get(url, {
        timeout: 10000, // 10秒のタイムアウト
        headers: {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
      });
      
      return response.data;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      console.log(`リトライ ${i + 1}/${maxRetries}...`);
      await sleep(2000 * (i + 1)); // 指数バックオフ
    }
  }
}

Axiosの高度な設定

インスタンスの作成

複数のリクエストで共通の設定を使用する場合、Axiosインスタンスを作成すると便利です。

const axiosInstance = axios.create({
  timeout: 10000,
  headers: {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html,application/xhtml+xml',
    'Accept-Language': 'ja,en;q=0.9',
  }
});

// インターセプターの追加
axiosInstance.interceptors.response.use(
  response => response,
  error => {
    console.error('リクエストエラー:', error.message);
    return Promise.reject(error);
  }
);

プロキシの使用

大規模なスクレイピングでは、プロキシの使用が必要になることがあります。

const response = await axios.get(url, {
  proxy: {
    host: 'proxy-server.com',
    port: 8080,
    auth: {
      username: 'user',
      password: 'password'
    }
  }
});

代替手段:Bright Data

大規模なWebスクレイピングプロジェクトでは、Bright Dataのような専門的なサービスの利用も検討できます。

Bright Dataの特徴

  • プロキシネットワーク: 世界中に分散したプロキシサーバー
  • 自動リトライ: 失敗したリクエストの自動再試行
  • CAPTCHA解決: 自動的なCAPTCHAの処理
  • データ収集API: スクレイピングロジックをAPI化
// Bright Data Web Scraper APIの使用例
const axios = require('axios');

async function scrapeWithBrightData(url) {
  const response = await axios.post(
    'https://api.brightdata.com/datasets/v3/trigger',
    {
      url: url,
      format: 'json'
    },
    {
      headers: {
        'Authorization': `Bearer YOUR_API_KEY`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  return response.data;
}

Bright Dataは有料サービスですが、大規模なスクレイピングやIP制限の回避が必要な場合に有用です。

パフォーマンスの最適化

並列処理

複数のURLを効率的にスクレイピングするには、並列処理を活用します。

async function scrapeParallel(urls, concurrency = 5) {
  const results = [];
  
  for (let i = 0; i < urls.length; i += concurrency) {
    const batch = urls.slice(i, i + concurrency);
    const batchResults = await Promise.all(
      batch.map(url => scrapeWebsite(url).catch(err => null))
    );
    results.push(...batchResults.filter(Boolean));
    
    // バッチ間に待機
    if (i + concurrency < urls.length) {
      await sleep(1000);
    }
  }
  
  return results;
}

キャッシュの実装

同じURLに対する重複リクエストを避けるため、キャッシュ機能を実装できます。

const cache = new Map();
const CACHE_DURATION = 3600000; // 1時間

async function scrapeWithCache(url) {
  const cached = cache.get(url);
  
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    console.log('キャッシュからデータを取得:', url);
    return cached.data;
  }
  
  const data = await scrapeWebsite(url);
  cache.set(url, {
    data: data,
    timestamp: Date.now()
  });
  
  return data;
}

メモリ管理

大量のデータをスクレイピングする場合、メモリ使用量に注意が必要です。

const stream = require('stream');
const { promisify } = require('util');
const pipeline = promisify(stream.pipeline);

async function scrapeAndStream(urls, outputStream) {
  for (const url of urls) {
    try {
      const data = await scrapeWebsite(url);
      outputStream.write(JSON.stringify(data) + '\n');
      
      await sleep(1000);
    } catch (error) {
      console.error(`${url}の処理に失敗:`, error.message);
    }
  }
  
  outputStream.end();
}

// 使用例
const fs = require('fs');
const output = fs.createWriteStream('output.jsonl');
scrapeAndStream(urls, output);

動的コンテンツへの対応

Axiosは静的なHTMLの取得に適していますが、JavaScriptで動的に生成されるコンテンツには対応できません。その場合、以下の方法を検討してください。

APIエンドポイントの直接利用

多くのWebサイトは、内部的にAPIを使用してデータを取得しています。ブラウザの開発者ツールでネットワークタブを確認し、APIエンドポイントを特定できます。

async function scrapeAPI() {
  try {
    // ブラウザの開発者ツールで見つけたAPIエンドポイント
    const response = await axios.get('https://example.com/api/v1/data', {
      headers: {
        'User-Agent': 'Mozilla/5.0',
        'Accept': 'application/json',
        'Referer': 'https://example.com'
      }
    });
    
    return response.data;
  } catch (error) {
    console.error('APIリクエストエラー:', error.message);
  }
}

Puppeteerとの組み合わせ

動的コンテンツが必要な場合は、Puppeteerなどのヘッドレスブラウザを使用します。

const puppeteer = require('puppeteer');

async function scrapeDynamic(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  await page.goto(url, { waitUntil: 'networkidle2' });
  
  const data = await page.evaluate(() => {
    const items = [];
    document.querySelectorAll('.item').forEach(el => {
      items.push({
        title: el.querySelector('.title')?.textContent,
        description: el.querySelector('.desc')?.textContent
      });
    });
    return items;
  });
  
  await browser.close();
  return data;
}

認証が必要なサイトへの対応

Basic認証

async function scrapeWithBasicAuth(url, username, password) {
  const response = await axios.get(url, {
    auth: {
      username: username,
      password: password
    }
  });
  
  return response.data;
}

Cookie認証

async function scrapeWithCookies(url, cookies) {
  const cookieString = cookies
    .map(cookie => `${cookie.name}=${cookie.value}`)
    .join('; ');
  
  const response = await axios.get(url, {
    headers: {
      'Cookie': cookieString
    }
  });
  
  return response.data;
}

トークン認証(Bearer Token)

async function scrapeWithToken(url, token) {
  const response = await axios.get(url, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  return response.data;
}

データの構造化と検証

Zodを使用したバリデーション

スクレイピングしたデータの妥当性を検証するため、Zodなどのバリデーションライブラリを使用できます。

const { z } = require('zod');

const ArticleSchema = z.object({
  title: z.string().min(1),
  url: z.string().url(),
  publishedAt: z.string().optional(),
  author: z.string().optional()
});

const ArticlesSchema = z.array(ArticleSchema);

async function scrapeAndValidate(url) {
  const rawData = await scrapeWebsite(url);
  
  try {
    const validatedData = ArticlesSchema.parse(rawData);
    return validatedData;
  } catch (error) {
    console.error('バリデーションエラー:', error.errors);
    throw error;
  }
}

データのクリーニング

function cleanText(text) {
  return text
    .trim()
    .replace(/\s+/g, ' ')
    .replace(/\n+/g, ' ')
    .replace(/[\r\t]/g, '');
}

function cleanArticleData(article) {
  return {
    title: cleanText(article.title || ''),
    description: cleanText(article.description || ''),
    url: article.url?.trim() || '',
    publishedAt: article.publishedAt ? new Date(article.publishedAt).toISOString() : null
  };
}

ログとモニタリング

構造化ログの実装

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'scraper-error.log', level: 'error' }),
    new winston.transports.File({ filename: 'scraper.log' })
  ]
});

async function scrapeWithLogging(url) {
  logger.info('スクレイピング開始', { url });
  
  try {
    const data = await scrapeWebsite(url);
    logger.info('スクレイピング成功', { 
      url, 
      itemCount: data.length 
    });
    return data;
  } catch (error) {
    logger.error('スクレイピング失敗', { 
      url, 
      error: error.message,
      stack: error.stack
    });
    throw error;
  }
}

進捗の表示

const cliProgress = require('cli-progress');

async function scrapeWithProgress(urls) {
  const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
  progressBar.start(urls.length, 0);
  
  const results = [];
  
  for (let i = 0; i < urls.length; i++) {
    try {
      const data = await scrapeWebsite(urls[i]);
      results.push(data);
    } catch (error) {
      console.error(`\n${urls[i]}のスクレイピングに失敗`);
    }
    
    progressBar.update(i + 1);
    await sleep(1000);
  }
  
  progressBar.stop();
  return results;
}

データベースへの保存

MongoDBへの保存

const { MongoClient } = require('mongodb');

async function saveToMongoDB(data, connectionString, dbName, collectionName) {
  const client = new MongoClient(connectionString);
  
  try {
    await client.connect();
    const db = client.db(dbName);
    const collection = db.collection(collectionName);
    
    const result = await collection.insertMany(data);
    console.log(`${result.insertedCount}件のドキュメントを挿入しました`);
  } finally {
    await client.close();
  }
}

PostgreSQLへの保存

const { Pool } = require('pg');

async function saveToPostgreSQL(data) {
  const pool = new Pool({
    host: 'localhost',
    database: 'scraping_db',
    user: 'user',
    password: 'password'
  });
  
  const client = await pool.connect();
  
  try {
    await client.query('BEGIN');
    
    for (const item of data) {
      await client.query(
        'INSERT INTO articles(title, url, published_at) VALUES($1, $2, $3)',
        [item.title, item.url, item.publishedAt]
      );
    }
    
    await client.query('COMMIT');
    console.log(`${data.length}件のレコードを保存しました`);
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

スクレイピングのスケジューリング

Node-cronを使用した定期実行

const cron = require('node-cron');

// 毎日午前3時に実行
cron.schedule('0 3 * * *', async () => {
  console.log('定期スクレイピングを開始:', new Date().toISOString());
  
  try {
    const urls = [
      'https://example.com/page1',
      'https://example.com/page2'
    ];
    
    const data = await scrapeMultiplePages(urls);
    await saveData(data, `scraped_${Date.now()}.json`);
    
    console.log('スクレイピング完了');
  } catch (error) {
    console.error('定期スクレイピングエラー:', error);
  }
});

Bull Queueを使用したジョブ管理

const Queue = require('bull');
const scrapeQueue = new Queue('scraping', 'redis://127.0.0.1:6379');

// ジョブの追加
scrapeQueue.add({ url: 'https://example.com' }, {
  attempts: 3,
  backoff: {
    type: 'exponential',
    delay: 2000
  }
});

// ワーカーの定義
scrapeQueue.process(async (job) => {
  const { url } = job.data;
  return await scrapeWebsite(url);
});

// イベントハンドラ
scrapeQueue.on('completed', (job, result) => {
  console.log(`ジョブ ${job.id} 完了`);
});

scrapeQueue.on('failed', (job, err) => {
  console.error(`ジョブ ${job.id} 失敗:`, err.message);
});

## データの保存

スクレイピングしたデータをJSONファイルに保存する例です。

```javascript
const fs = require('fs').promises;

async function saveData(data, filename) {
  try {
    await fs.writeFile(
      filename,
      JSON.stringify(data, null, 2),
      'utf-8'
    );
    console.log(`データを${filename}に保存しました`);
  } catch (error) {
    console.error('保存エラー:', error.message);
  }
}

// 使用例
scrapeWebsite('https://example.com')
  .then(data => saveData(data, 'scraped_data.json'))
  .catch(err => console.error(err));

ベストプラクティス

  1. robots.txtを確認する

    const robotsUrl = new URL('/robots.txt', targetUrl).href;
    const robotsResponse = await axios.get(robotsUrl);
    
  2. 適切なUser-Agentを設定する

    • ボットであることを明示し、連絡先を含める
  3. リクエスト間隔を設ける

    • 最低でも1秒以上の間隔を推奨
  4. エラーログを記録する

    • 失敗したURLとエラー内容を保存
  5. データの検証

    • 取得したデータの妥当性を確認

まとめ

Axiosを使用したWebスクレイピングは、シンプルなAPIと豊富な機能により、効率的なデータ収集を実現できます。本記事では以下の内容を解説しました:

基礎編

  • Axiosの基本的な使い方とCheerioとの組み合わせ
  • エラーハンドリングとリトライロジックの実装
  • レート制限とサーバー負荷への配慮

応用編

  • 並列処理とキャッシュによるパフォーマンス最適化
  • 動的コンテンツへの対応方法
  • 認証が必要なサイトへのアクセス
  • データの構造化とバリデーション

運用編

  • ログとモニタリングの実装
  • データベースへの永続化
  • 定期実行とジョブ管理

重要な注意点

スクレイピングを実装する際は、常に以下の点を意識してください:

  1. 法的・倫理的な配慮

    • 対象サイトの利用規約を確認
    • robots.txtを尊重
    • 個人情報や著作権に配慮
  2. 技術的なベストプラクティス

    • 適切なUser-Agentの設定
    • リクエスト間隔の確保
    • エラーハンドリングの実装
  3. スケーラビリティ

    • 大規模プロジェクトでは、Bright Dataなどの専門サービスも検討
    • キャッシュやキューを活用した効率化
    • モニタリングとログの整備

Webスクレイピングは強力なツールですが、責任を持って使用することが重要です。本記事の内容を参考に、適切で効率的なスクレイピングシステムを構築してください。

参考リンク


最後までお読みいただきありがとうございました!

Discussion