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));
ベストプラクティス
-
robots.txtを確認する
const robotsUrl = new URL('/robots.txt', targetUrl).href; const robotsResponse = await axios.get(robotsUrl); -
適切なUser-Agentを設定する
- ボットであることを明示し、連絡先を含める
-
リクエスト間隔を設ける
- 最低でも1秒以上の間隔を推奨
-
エラーログを記録する
- 失敗したURLとエラー内容を保存
-
データの検証
- 取得したデータの妥当性を確認
まとめ
Axiosを使用したWebスクレイピングは、シンプルなAPIと豊富な機能により、効率的なデータ収集を実現できます。本記事では以下の内容を解説しました:
基礎編
- Axiosの基本的な使い方とCheerioとの組み合わせ
- エラーハンドリングとリトライロジックの実装
- レート制限とサーバー負荷への配慮
応用編
- 並列処理とキャッシュによるパフォーマンス最適化
- 動的コンテンツへの対応方法
- 認証が必要なサイトへのアクセス
- データの構造化とバリデーション
運用編
- ログとモニタリングの実装
- データベースへの永続化
- 定期実行とジョブ管理
重要な注意点
スクレイピングを実装する際は、常に以下の点を意識してください:
-
法的・倫理的な配慮
- 対象サイトの利用規約を確認
- robots.txtを尊重
- 個人情報や著作権に配慮
-
技術的なベストプラクティス
- 適切なUser-Agentの設定
- リクエスト間隔の確保
- エラーハンドリングの実装
-
スケーラビリティ
- 大規模プロジェクトでは、Bright Dataなどの専門サービスも検討
- キャッシュやキューを活用した効率化
- モニタリングとログの整備
Webスクレイピングは強力なツールですが、責任を持って使用することが重要です。本記事の内容を参考に、適切で効率的なスクレイピングシステムを構築してください。
参考リンク
最後までお読みいただきありがとうございました!
Discussion