# スクレイピング防止方法
Webサイトをスクレイピングから保護することは、サーバーリソースの保護、データの不正利用防止、利用規約の遵守など、多くの理由から重要です。この記事では、効果的なスクレイピング防止の方法と実装例を解説します。
# スクレイピング防止の重要性
スクレイピング防止を行う主な理由:
- サーバーリソースの保護: 過度なリクエストによるサーバー負荷の軽減
- データの保護: コンテンツの不正利用や著作権侵害の防止
- ビジネス価値の保護: 有料データやAPIサービスの価値を守る
- 利用規約の遵守: 適切な利用を促進し、規約違反を防止
- ユーザー体験の向上: 通常のユーザーへの影響を最小限に抑える
# 基本的な防止方法
# 1. User-Agent の検証
スクレイピングツールは、しばしば不自然なUser-Agentや、User-Agentを設定しないリクエストを送信します。
# Node.js/Express での実装例
const express = require('express');
const app = express();
// User-Agent検証ミドルウェア
function validateUserAgent(req, res, next) {
const userAgent = req.get('User-Agent');
// User-Agentが存在しない、または不自然なものをブロック
if (!userAgent) {
return res.status(403).json({ error: 'User-Agent is required' });
}
// 一般的なスクレイピングツールのUser-Agentをブロック
const blockedAgents = [
'curl',
'wget',
'python-requests',
'scrapy',
'Go-http-client',
'Java/',
'Apache-HttpClient'
];
const isBlocked = blockedAgents.some(agent =>
userAgent.toLowerCase().includes(agent.toLowerCase())
);
if (isBlocked) {
return res.status(403).json({ error: 'Access denied' });
}
next();
}
app.use(validateUserAgent);
// 通常のルート
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# Python/Flask での実装例
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
# ブロックするUser-Agentのリスト
BLOCKED_USER_AGENTS = [
'curl',
'wget',
'python-requests',
'scrapy',
'Go-http-client',
'Java/',
'Apache-HttpClient'
]
def validate_user_agent(f):
"""
User-Agent検証デコレータ
"""
@wraps(f)
def decorated_function(*args, **kwargs):
user_agent = request.headers.get('User-Agent', '')
# User-Agentが存在しない場合
if not user_agent:
return jsonify({'error': 'User-Agent is required'}), 403
# ブロックリストに該当する場合
user_agent_lower = user_agent.lower()
if any(blocked in user_agent_lower for blocked in BLOCKED_USER_AGENTS):
return jsonify({'error': 'Access denied'}), 403
return f(*args, **kwargs)
return decorated_function
@app.route('/')
@validate_user_agent
def index():
return jsonify({'message': 'Hello World'})
if __name__ == '__main__':
app.run(port=3000)
# Laravel での実装例
ミドルウェアの作成
php artisan make:middleware ValidateUserAgent
app/Http/Middleware/ValidateUserAgent.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ValidateUserAgent
{
/**
* ブロックするUser-Agentのリスト
*/
private $blockedAgents = [
'curl',
'wget',
'python-requests',
'scrapy',
'Go-http-client',
'Java/',
'Apache-HttpClient'
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$userAgent = $request->header('User-Agent');
// User-Agentが存在しない場合
if (empty($userAgent)) {
return response()->json(['error' => 'User-Agent is required'], 403);
}
// ブロックリストに該当する場合
$userAgentLower = strtolower($userAgent);
foreach ($this->blockedAgents as $blocked) {
if (strpos($userAgentLower, strtolower($blocked)) !== false) {
return response()->json(['error' => 'Access denied'], 403);
}
}
return $next($request);
}
}
app/Http/Kernel.php に登録
protected $middlewareGroups = [
'web' => [
// ... 他のミドルウェア
\App\Http\Middleware\ValidateUserAgent::class,
],
'api' => [
// ... 他のミドルウェア
\App\Http\Middleware\ValidateUserAgent::class,
],
];
ルートでの使用例
// routes/web.php
Route::get('/', function () {
return response()->json(['message' => 'Hello World']);
});
# 2. レート制限(Rate Limiting)
同じIPアドレスやユーザーからの過度なリクエストを制限します。
# Node.js/Express + express-rate-limit での実装例
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// レート制限の設定
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 15分間に最大100リクエスト
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true, // `RateLimit-*` ヘッダーを返す
legacyHeaders: false, // `X-RateLimit-*` ヘッダーを無効化
});
// より厳しいレート制限(APIエンドポイント用)
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 15分間に最大10リクエスト
message: 'Too many API requests, please try again later.',
});
// 全ルートに適用
app.use(limiter);
// APIエンドポイントに厳しい制限を適用
app.use('/api/', apiLimiter);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# Python/Flask + Flask-Limiter での実装例
from flask import Flask, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
# レート制限の設定
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/')
@limiter.limit("100 per 15 minutes")
def index():
return jsonify({'message': 'Hello World'})
@app.route('/api/data')
@limiter.limit("10 per 15 minutes")
def api_data():
return jsonify({'data': 'some data'})
if __name__ == '__main__':
app.run(port=3000)
# Laravel でのレート制限実装例
Laravelには標準でレート制限機能が組み込まれています。
ルートでの使用例
// routes/web.php
use Illuminate\Support\Facades\Route;
// 15分間に100リクエストまで
Route::middleware(['throttle:100,15'])->group(function () {
Route::get('/', function () {
return response()->json(['message' => 'Hello World']);
});
});
// APIエンドポイントに厳しい制限(15分間に10リクエスト)
Route::middleware(['throttle:10,15'])->group(function () {
Route::get('/api/data', function () {
return response()->json(['data' => 'some data']);
});
});
カスタムレート制限の設定
// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('strict', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
}
Redis を使った高度なレート制限(Laravel)
// app/Http/Middleware/CustomRateLimit.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class CustomRateLimit
{
public function handle(Request $request, Closure $next)
{
$ip = $request->ip();
$key = "rate_limit:{$ip}";
try {
$current = Redis::incr($key);
if ($current === 1) {
Redis::expire($key, 60); // 60秒
}
$ttl = Redis::ttl($key);
// リクエスト数をチェック
if ($current > 100) {
return response()->json([
'error' => 'Too many requests',
'retryAfter' => $ttl
], 429)->header('Retry-After', $ttl);
}
// レート制限情報をヘッダーに追加
return $next($request)
->header('X-RateLimit-Limit', '100')
->header('X-RateLimit-Remaining', max(0, 100 - $current))
->header('X-RateLimit-Reset', now()->addSeconds($ttl)->timestamp);
} catch (\Exception $e) {
\Log::error('Rate limit error: ' . $e->getMessage());
return $next($request);
}
}
}
# Redis を使った高度なレート制限(Node.js)
const express = require('express');
const redis = require('redis');
const client = redis.createClient();
const app = express();
// Redisを使ったレート制限ミドルウェア
async function rateLimitWithRedis(req, res, next) {
const ip = req.ip || req.connection.remoteAddress;
const key = `rate_limit:${ip}`;
try {
const current = await client.incr(key);
if (current === 1) {
// 初回リクエストの場合、TTLを設定
await client.expire(key, 60); // 60秒
}
const ttl = await client.ttl(key);
// リクエスト数をチェック
if (current > 100) { // 60秒間に100リクエストを超えた場合
res.setHeader('Retry-After', ttl);
return res.status(429).json({
error: 'Too many requests',
retryAfter: ttl
});
}
// レート制限情報をヘッダーに追加
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', Math.max(0, 100 - current));
res.setHeader('X-RateLimit-Reset', Date.now() + (ttl * 1000));
next();
} catch (error) {
console.error('Rate limit error:', error);
next(); // エラー時は通過させる
}
}
app.use(rateLimitWithRedis);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 3. IPアドレスのブロック
特定のIPアドレスやIP範囲をブロックします。
# Node.js/Express での実装例
const express = require('express');
const app = express();
// ブロックするIPアドレスのリスト
const blockedIPs = new Set([
'192.168.1.100',
'10.0.0.50',
// CIDR表記もサポートする場合は、ipaddr.jsなどのライブラリを使用
]);
// IPアドレス取得関数
function getClientIP(req) {
return req.headers['x-forwarded-for']?.split(',')[0] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null);
}
// IPブロックミドルウェア
function blockIPs(req, res, next) {
const clientIP = getClientIP(req);
if (blockedIPs.has(clientIP)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
}
app.use(blockIPs);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# Laravel でのIPブロック実装例
ミドルウェアの作成
php artisan make:middleware BlockIPs
app/Http/Middleware/BlockIPs.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class BlockIPs
{
/**
* ブロックするIPアドレスのリスト
*/
private $blockedIPs = [
'192.168.1.100',
'10.0.0.50',
];
public function handle(Request $request, Closure $next)
{
$clientIP = $request->ip();
if (in_array($clientIP, $this->blockedIPs)) {
return response()->json(['error' => 'Access denied'], 403);
}
return $next($request);
}
}
# 動的なIPブロック(レート制限違反時)
Laravel での実装例
// app/Http/Middleware/DynamicIPBlock.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class DynamicIPBlock
{
public function handle(Request $request, Closure $next)
{
$ip = $request->ip();
$violationKey = "violations:{$ip}";
$blockKey = "blocked:{$ip}";
try {
// ブロックリストをチェック
if (Redis::exists($blockKey)) {
return response()->json(['error' => 'IP address is blocked'], 403);
}
// レート制限違反をチェック
$violations = Redis::incr($violationKey);
if ($violations === 1) {
Redis::expire($violationKey, 3600); // 1時間
}
// 違反が5回を超えた場合、IPをブロック
if ($violations > 5) {
Redis::setex($blockKey, 86400, '1'); // 24時間ブロック
return response()->json(['error' => 'IP address is blocked due to violations'], 403);
}
return $next($request);
} catch (\Exception $e) {
\Log::error('IP block error: ' . $e->getMessage());
return $next($request);
}
}
}
Node.js/Express での実装例
const express = require('express');
const redis = require('redis');
const client = redis.createClient();
const app = express();
// 動的IPブロック機能
async function dynamicIPBlock(req, res, next) {
const ip = req.ip || req.connection.remoteAddress;
const violationKey = `violations:${ip}`;
const blockKey = `blocked:${ip}`;
try {
// ブロックリストをチェック
const isBlocked = await client.exists(blockKey);
if (isBlocked) {
return res.status(403).json({ error: 'IP address is blocked' });
}
// レート制限違反をチェック
const violations = await client.incr(violationKey);
if (violations === 1) {
await client.expire(violationKey, 3600); // 1時間
}
// 違反が5回を超えた場合、IPをブロック
if (violations > 5) {
await client.setex(blockKey, 86400, '1'); // 24時間ブロック
return res.status(403).json({ error: 'IP address is blocked due to violations' });
}
next();
} catch (error) {
console.error('IP block error:', error);
next();
}
}
app.use(dynamicIPBlock);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 4. CAPTCHA の実装
人間とボットを区別するためにCAPTCHAを実装します。
# Google reCAPTCHA v3 の実装例
フロントエンド(HTML + JavaScript)
<!DOCTYPE html>
<html>
<head>
<title>CAPTCHA Example</title>
<script src="https://www.google.com/recaptcha/api.js"></script>
</head>
<body>
<form id="myForm">
<input type="text" name="data" placeholder="Enter data">
<button type="submit">Submit</button>
</form>
<script>
grecaptcha.ready(function() {
document.getElementById('myForm').addEventListener('submit', function(e) {
e.preventDefault();
grecaptcha.execute('YOUR_SITE_KEY', {action: 'submit'}).then(function(token) {
// トークンをサーバーに送信
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: document.querySelector('input[name="data"]').value,
recaptchaToken: token
})
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch(error => {
console.error('Error:', error);
});
});
});
});
</script>
</body>
</html>
バックエンド(Node.js/Express)
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// reCAPTCHA検証ミドルウェア
async function verifyRecaptcha(req, res, next) {
const recaptchaToken = req.body.recaptchaToken;
if (!recaptchaToken) {
return res.status(400).json({ error: 'reCAPTCHA token is required' });
}
try {
const response = await axios.post('https://www.google.com/recaptcha/api/siteverify', null, {
params: {
secret: process.env.RECAPTCHA_SECRET_KEY,
response: recaptchaToken
}
});
const { success, score } = response.data;
// reCAPTCHA v3はスコアを返す(0.0〜1.0、1.0が最も人間らしい)
if (!success || score < 0.5) {
return res.status(403).json({ error: 'reCAPTCHA verification failed' });
}
next();
} catch (error) {
console.error('reCAPTCHA verification error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
app.post('/api/submit', verifyRecaptcha, (req, res) => {
res.json({ message: 'Data submitted successfully', data: req.body.data });
});
app.listen(3000);
# Laravel でのreCAPTCHA実装例
ミドルウェアの作成
php artisan make:middleware VerifyRecaptcha
app/Http/Middleware/VerifyRecaptcha.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class VerifyRecaptcha
{
public function handle(Request $request, Closure $next)
{
$recaptchaToken = $request->input('recaptchaToken');
if (!$recaptchaToken) {
return response()->json(['error' => 'reCAPTCHA token is required'], 400);
}
try {
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret'),
'response' => $recaptchaToken
]);
$data = $response->json();
// reCAPTCHA v3はスコアを返す(0.0〜1.0、1.0が最も人間らしい)
if (!$data['success'] || ($data['score'] ?? 0) < 0.5) {
return response()->json(['error' => 'reCAPTCHA verification failed'], 403);
}
return $next($request);
} catch (\Exception $e) {
\Log::error('reCAPTCHA verification error: ' . $e->getMessage());
return response()->json(['error' => 'Internal server error'], 500);
}
}
}
config/services.php に設定を追加
'recaptcha' => [
'site_key' => env('RECAPTCHA_SITE_KEY'),
'secret' => env('RECAPTCHA_SECRET_KEY'),
],
# hCaptcha の実装例
Laravel での実装例
// app/Http/Middleware/VerifyHcaptcha.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class VerifyHcaptcha
{
public function handle(Request $request, Closure $next)
{
$hcaptchaToken = $request->input('hcaptchaToken');
if (!$hcaptchaToken) {
return response()->json(['error' => 'hCaptcha token is required'], 400);
}
try {
$response = Http::asForm()->post('https://hcaptcha.com/siteverify', [
'secret' => config('services.hcaptcha.secret'),
'response' => $hcaptchaToken
]);
if (!$response->json()['success']) {
return response()->json(['error' => 'hCaptcha verification failed'], 403);
}
return $next($request);
} catch (\Exception $e) {
\Log::error('hCaptcha verification error: ' . $e->getMessage());
return response()->json(['error' => 'Internal server error'], 500);
}
}
}
Node.js/Express での実装例
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// hCaptcha検証ミドルウェア
async function verifyHcaptcha(req, res, next) {
const hcaptchaToken = req.body.hcaptchaToken;
if (!hcaptchaToken) {
return res.status(400).json({ error: 'hCaptcha token is required' });
}
try {
const response = await axios.post('https://hcaptcha.com/siteverify', null, {
params: {
secret: process.env.HCAPTCHA_SECRET_KEY,
response: hcaptchaToken
}
});
if (!response.data.success) {
return res.status(403).json({ error: 'hCaptcha verification failed' });
}
next();
} catch (error) {
console.error('hCaptcha verification error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
app.post('/api/submit', verifyHcaptcha, (req, res) => {
res.json({ message: 'Data submitted successfully' });
});
app.listen(3000);
# 5. JavaScript チャレンジ
JavaScriptの実行を必要とするページ構造にすることで、シンプルなスクレイピングツールをブロックできます。
# クライアントサイドでの実装例
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Challenge</title>
</head>
<body>
<div id="content" style="display: none;">
<h1>Protected Content</h1>
<p>This content is only visible after JavaScript execution.</p>
</div>
<script>
// トークンを生成してサーバーに送信
function generateToken() {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(7);
return btoa(`${timestamp}:${random}`);
}
// コンテンツを表示
function showContent() {
const token = generateToken();
// トークンをサーバーに送信して検証
fetch('/api/verify-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token })
})
.then(response => response.json())
.then(data => {
if (data.verified) {
document.getElementById('content').style.display = 'block';
}
})
.catch(error => {
console.error('Error:', error);
});
}
// ページ読み込み時に実行
window.addEventListener('DOMContentLoaded', showContent);
</script>
</body>
</html>
# 6. robots.txt の設定
robots.txtファイルを適切に設定することで、倫理的なスクレイパーに対してアクセス制限を指示できます。
# robots.txt の例
# すべてのボットを許可
User-agent: *
Allow: /
# 特定のボットをブロック
User-agent: BadBot
Disallow: /
# 特定のパスをブロック
User-agent: *
Disallow: /api/
Disallow: /admin/
Disallow: /private/
# クロール頻度の制限
User-agent: *
Crawl-delay: 10
# サイトマップの指定
Sitemap: https://example.com/sitemap.xml
# 動的なrobots.txtの生成
Laravel での実装例
// routes/web.php
Route::get('/robots.txt', function (Request $request) {
$ip = $request->ip();
$blockedIPs = ['192.168.1.100'];
if (in_array($ip, $blockedIPs)) {
return response('User-agent: *\nDisallow: /', 200)
->header('Content-Type', 'text/plain');
}
$robots = "User-agent: *\n";
$robots .= "Allow: /\n";
$robots .= "Disallow: /api/\n";
$robots .= "Disallow: /admin/\n";
$robots .= "Crawl-delay: 10\n";
$robots .= "Sitemap: https://example.com/sitemap.xml";
return response($robots, 200)
->header('Content-Type', 'text/plain');
});
Node.js/Express での実装例
const express = require('express');
const app = express();
app.get('/robots.txt', (req, res) => {
const ip = req.ip || req.connection.remoteAddress;
// 特定のIPからのアクセスをブロック
const blockedIPs = ['192.168.1.100'];
if (blockedIPs.includes(ip)) {
res.type('text/plain');
res.send('User-agent: *\nDisallow: /');
} else {
res.type('text/plain');
res.send(`User-agent: *
Allow: /
Disallow: /api/
Disallow: /admin/
Crawl-delay: 10
Sitemap: https://example.com/sitemap.xml`);
}
});
app.listen(3000);
# 7. リクエストパターンの分析
不自然なリクエストパターンを検出してブロックします。
# Node.js/Express での実装例
const express = require('express');
const app = express();
// リクエストパターンを記録
const requestPatterns = new Map();
// リクエストパターン分析ミドルウェア
function analyzeRequestPattern(req, res, next) {
const ip = req.ip || req.connection.remoteAddress;
const path = req.path;
const now = Date.now();
// IPごとのリクエスト履歴を取得
if (!requestPatterns.has(ip)) {
requestPatterns.set(ip, []);
}
const history = requestPatterns.get(ip);
// 最近のリクエスト(過去1分間)をフィルタ
const recentRequests = history.filter(timestamp => now - timestamp < 60000);
// リクエスト頻度が異常に高い場合(1分間に100リクエスト以上)
if (recentRequests.length > 100) {
return res.status(429).json({ error: 'Too many requests' });
}
// 同じパスへの連続アクセスを検出(10回以上)
const samePathRequests = recentRequests.filter((_, index) => {
const requestInfo = history[index];
return requestInfo && requestInfo.path === path;
});
if (samePathRequests.length > 10) {
return res.status(403).json({ error: 'Suspicious request pattern detected' });
}
// リクエストを記録
history.push({ timestamp: now, path: path });
// 古い記録を削除(メモリリーク防止)
if (history.length > 1000) {
history.splice(0, history.length - 1000);
}
next();
}
app.use(analyzeRequestPattern);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 8. セッション管理とCookie検証
正常なブラウザセッションを必要とする実装にします。
# Laravel での実装例
ミドルウェアの作成
php artisan make:middleware ValidateSession
app/Http/Middleware/ValidateSession.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ValidateSession
{
public function handle(Request $request, Closure $next)
{
// セッションが存在しない、または新規セッションの場合
if (!$request->session()->has('initialized')) {
$request->session()->put('initialized', true);
$request->session()->put('firstAccess', now()->timestamp);
}
// セッション開始から一定時間経過していない場合(ボットの可能性)
$firstAccess = $request->session()->get('firstAccess');
$sessionAge = now()->timestamp - $firstAccess;
if ($sessionAge < 5) { // 5秒未満
// 追加の検証を要求(CAPTCHAなど)
if (!$request->session()->has('verified')) {
return response()->json([
'error' => 'Session verification required',
'requiresCaptcha' => true
], 403);
}
}
return $next($request);
}
}
config/session.php でセッション設定
'cookie' => env('SESSION_COOKIE', 'laravel_session'),
'lifetime' => 1440, // 24時間(分)
'secure' => env('SESSION_SECURE_COOKIE', false),
'http_only' => true,
# Node.js/Express での実装例
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24時間
}
}));
// セッション検証ミドルウェア
function validateSession(req, res, next) {
// セッションが存在しない、または新規セッションの場合
if (!req.session.initialized) {
// 初期化フラグを設定
req.session.initialized = true;
req.session.firstAccess = Date.now();
}
// セッション開始から一定時間経過していない場合(ボットの可能性)
const sessionAge = Date.now() - req.session.firstAccess;
if (sessionAge < 5000) { // 5秒未満
// 追加の検証を要求(CAPTCHAなど)
if (!req.session.verified) {
return res.status(403).json({
error: 'Session verification required',
requiresCaptcha: true
});
}
}
next();
}
app.use(validateSession);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 9. リファラー(Referer)の検証
特定のページからのみアクセスを許可します。
# Laravel での実装例
ミドルウェアの作成
php artisan make:middleware ValidateReferer
app/Http/Middleware/ValidateReferer.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ValidateReferer
{
/**
* 許可するリファラーのリスト
*/
private $allowedReferers = [
'https://example.com',
'https://www.example.com'
];
public function handle(Request $request, Closure $next)
{
// APIエンドポイントなど、リファラーが不要な場合はスキップ
if ($request->is('api/*')) {
return $next($request);
}
$referer = $request->header('Referer') ?: $request->header('Referrer');
// リファラーが存在しない、または許可されていない場合
if (!$referer || !$this->isAllowedReferer($referer)) {
return response()->json(['error' => 'Invalid referer'], 403);
}
return $next($request);
}
private function isAllowedReferer($referer)
{
foreach ($this->allowedReferers as $allowed) {
if (str_starts_with($referer, $allowed)) {
return true;
}
}
return false;
}
}
# Node.js/Express での実装例
const express = require('express');
const app = express();
// リファラー検証ミドルウェア
function validateReferer(req, res, next) {
const referer = req.get('Referer') || req.get('Referrer');
const allowedReferers = [
'https://example.com',
'https://www.example.com'
];
// APIエンドポイントなど、リファラーが不要な場合はスキップ
if (req.path.startsWith('/api/')) {
return next();
}
// リファラーが存在しない、または許可されていない場合
if (!referer || !allowedReferers.some(allowed => referer.startsWith(allowed))) {
return res.status(403).json({ error: 'Invalid referer' });
}
next();
}
app.use(validateReferer);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 10. ハニーポット(Honeypot)
人間には見えないが、ボットがアクセスしやすい要素を設置します。
# HTML + JavaScript での実装例
<!DOCTYPE html>
<html>
<head>
<title>Honeypot Example</title>
<style>
/* ハニーポットフィールドを非表示(CSSで) */
.honeypot {
position: absolute;
left: -9999px;
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body>
<form id="contactForm" method="POST" action="/api/contact">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<!-- ハニーポットフィールド(人間には見えない) -->
<input type="text" name="website" class="honeypot" tabindex="-1" autocomplete="off">
<button type="submit">Submit</button>
</form>
<script>
document.getElementById('contactForm').addEventListener('submit', function(e) {
const honeypot = document.querySelector('input[name="website"]').value;
// ハニーポットフィールドに値が入力されている場合(ボットの可能性)
if (honeypot) {
e.preventDefault();
console.log('Bot detected');
return false;
}
});
</script>
</body>
</html>
# サーバーサイドでの検証
Laravel での実装例
// app/Http/Controllers/ContactController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function store(Request $request)
{
// ハニーポットフィールドをチェック
if ($request->filled('website') && trim($request->input('website')) !== '') {
// ボットと判定
return response()->json(['error' => 'Access denied'], 403);
}
// 正常な処理
// ... お問い合わせの保存処理など
return response()->json(['message' => 'Contact form submitted successfully']);
}
}
Node.js/Express での実装例
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/api/contact', (req, res) => {
// ハニーポットフィールドをチェック
if (req.body.website && req.body.website.trim() !== '') {
// ボットと判定
return res.status(403).json({ error: 'Access denied' });
}
// 正常な処理
res.json({ message: 'Contact form submitted successfully' });
});
app.listen(3000);
# 高度な防止方法
# 1. 機械学習ベースの検出
リクエストパターンを機械学習で分析し、ボットを検出します。
# Python + scikit-learn での実装例(概念)
from sklearn.ensemble import IsolationForest
import numpy as np
class BotDetector:
def __init__(self):
self.model = IsolationForest(contamination=0.1, random_state=42)
self.is_trained = False
def extract_features(self, request_data):
"""
リクエストから特徴量を抽出
"""
features = [
request_data.get('request_rate', 0), # リクエスト頻度
request_data.get('user_agent_length', 0), # User-Agentの長さ
request_data.get('header_count', 0), # ヘッダーの数
request_data.get('cookie_count', 0), # Cookieの数
request_data.get('referer_exists', 0), # リファラーの有無
request_data.get('accept_language_exists', 0), # Accept-Languageの有無
]
return np.array(features).reshape(1, -1)
def train(self, normal_requests, anomalous_requests):
"""
モデルを訓練
"""
# 正常なリクエストと異常なリクエストの特徴量を結合
all_features = []
for req in normal_requests + anomalous_requests:
features = self.extract_features(req)
all_features.append(features[0])
X = np.array(all_features)
self.model.fit(X)
self.is_trained = True
def predict(self, request_data):
"""
リクエストがボットかどうかを予測
"""
if not self.is_trained:
return False
features = self.extract_features(request_data)
prediction = self.model.predict(features)
# -1が異常(ボット)、1が正常
return prediction[0] == -1
# 使用例
detector = BotDetector()
# 訓練データ(実際の実装では、より多くのデータが必要)
normal_requests = [
{'request_rate': 1, 'user_agent_length': 100, 'header_count': 10,
'cookie_count': 3, 'referer_exists': 1, 'accept_language_exists': 1},
# ... より多くの正常なリクエスト
]
anomalous_requests = [
{'request_rate': 100, 'user_agent_length': 20, 'header_count': 3,
'cookie_count': 0, 'referer_exists': 0, 'accept_language_exists': 0},
# ... より多くの異常なリクエスト
]
detector.train(normal_requests, anomalous_requests)
# 新しいリクエストを検証
new_request = {
'request_rate': 50,
'user_agent_length': 25,
'header_count': 5,
'cookie_count': 0,
'referer_exists': 0,
'accept_language_exists': 0
}
is_bot = detector.predict(new_request)
print(f'Is bot: {is_bot}')
# 2. WAF(Web Application Firewall)の利用
クラウドベースのWAFサービスを利用します。
# Cloudflare の設定例
// Cloudflare Workers での実装例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const ip = request.headers.get('CF-Connecting-IP');
const userAgent = request.headers.get('User-Agent');
// Cloudflareのボット検出機能を利用
const isBot = request.headers.get('CF-Ray') ? false : true;
if (isBot) {
return new Response('Access denied', { status: 403 });
}
// 通常のリクエスト処理
return fetch(request);
}
# 3. コンテンツの動的生成
JavaScriptで動的にコンテンツを生成することで、シンプルなスクレイパーをブロックします。
# クライアントサイドでの実装例
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Content</title>
</head>
<body>
<div id="content"></div>
<script>
// サーバーから暗号化されたデータを取得
async function loadContent() {
const response = await fetch('/api/encrypted-content');
const encryptedData = await response.json();
// クライアントサイドで復号化(簡単な例)
const decrypted = decrypt(encryptedData.data);
// DOMに動的に挿入
document.getElementById('content').innerHTML = decrypted;
}
function decrypt(data) {
// 実際の実装では、より強力な暗号化を使用
return atob(data);
}
// ページ読み込み時に実行
window.addEventListener('DOMContentLoaded', loadContent);
</script>
</body>
</html>
# 実装のベストプラクティス
# 1. 多層防御
単一の対策に依存せず、複数の対策を組み合わせます。
Laravel での実装例
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ... 他のミドルウェア
\App\Http\Middleware\ValidateUserAgent::class, // User-Agent検証
\App\Http\Middleware\BlockIPs::class, // IPブロック
\App\Http\Middleware\ValidateSession::class, // セッション検証
\App\Http\Middleware\ValidateReferer::class, // リファラー検証
],
'api' => [
'throttle:60,1', // レート制限
\App\Http\Middleware\ValidateUserAgent::class,
\App\Http\Middleware\DynamicIPBlock::class,
],
];
// routes/web.php
Route::middleware(['web'])->group(function () {
Route::get('/', function () {
return response()->json(['message' => 'Hello World']);
});
});
Node.js/Express での実装例
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// 複数の対策を組み合わせ
app.use(validateUserAgent); // User-Agent検証
app.use(rateLimit({ // レート制限
windowMs: 15 * 60 * 1000,
max: 100
}));
app.use(analyzeRequestPattern); // リクエストパターン分析
app.use(validateSession); // セッション検証
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 2. ホワイトリスト方式
信頼できるボット(検索エンジンなど)を許可します。
Laravel での実装例
// app/Http/Middleware/ValidateUserAgent.php を修正
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ValidateUserAgent
{
private $blockedAgents = [
'curl',
'wget',
'python-requests',
'scrapy',
'Go-http-client',
'Java/',
'Apache-HttpClient'
];
// 許可するUser-Agentのリスト
private $allowedUserAgents = [
'Googlebot',
'Bingbot',
'Slurp', // Yahoo
'DuckDuckBot',
'Baiduspider'
];
public function handle(Request $request, Closure $next)
{
$userAgent = $request->header('User-Agent');
if (empty($userAgent)) {
return response()->json(['error' => 'User-Agent is required'], 403);
}
// 許可リストに該当する場合は通過
$isAllowed = collect($this->allowedUserAgents)->contains(function ($agent) use ($userAgent) {
return str_contains($userAgent, $agent);
});
if ($isAllowed) {
return $next($request);
}
// ブロックリストに該当する場合
$userAgentLower = strtolower($userAgent);
foreach ($this->blockedAgents as $blocked) {
if (str_contains($userAgentLower, strtolower($blocked))) {
return response()->json(['error' => 'Access denied'], 403);
}
}
return $next($request);
}
}
Node.js/Express での実装例
const express = require('express');
const app = express();
// 許可するUser-Agentのリスト
const allowedUserAgents = [
'Googlebot',
'Bingbot',
'Slurp', // Yahoo
'DuckDuckBot',
'Baiduspider'
];
function validateUserAgent(req, res, next) {
const userAgent = req.get('User-Agent') || '';
// 許可リストに該当する場合は通過
const isAllowed = allowedUserAgents.some(agent =>
userAgent.includes(agent)
);
if (isAllowed) {
return next();
}
// その他の検証処理
// ...
next();
}
app.use(validateUserAgent);
# 3. ログとモニタリング
スクレイピングの試行を記録し、分析します。
Laravel での実装例
// app/Http/Middleware/LogRequest.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class LogRequest
{
public function handle(Request $request, Closure $next)
{
$logData = [
'timestamp' => now()->toISOString(),
'ip' => $request->ip(),
'user_agent' => $request->header('User-Agent'),
'path' => $request->path(),
'method' => $request->method(),
'referer' => $request->header('Referer'),
];
// ログに記録
Log::channel('access')->info('Request', $logData);
return $next($request);
}
}
config/logging.php にチャンネルを追加
'channels' => [
// ... 他のチャンネル
'access' => [
'driver' => 'daily',
'path' => storage_path('logs/access.log'),
'level' => 'info',
'days' => 14,
],
],
Node.js/Express での実装例
const express = require('express');
const fs = require('fs');
const app = express();
// ログ記録ミドルウェア
function logRequest(req, res, next) {
const logData = {
timestamp: new Date().toISOString(),
ip: req.ip,
userAgent: req.get('User-Agent'),
path: req.path,
method: req.method,
referer: req.get('Referer')
};
// ログファイルに記録
fs.appendFile('access.log', JSON.stringify(logData) + '\n', (err) => {
if (err) console.error('Log error:', err);
});
next();
}
app.use(logRequest);
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);
# 4. 段階的な対応
軽微な違反には警告、重大な違反にはブロックを適用します。
Laravel での実装例
// app/Http/Middleware/HandleViolation.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class HandleViolation
{
public function handle(Request $request, Closure $next)
{
$ip = $request->ip();
$violationKey = "violation:{$ip}";
$violation = Cache::get($violationKey, ['level' => 0, 'firstViolation' => now()->timestamp]);
// 違反レベルに応じた対応
if ($violation['level'] === 0) {
// 警告のみ
return $next($request)->header('X-Warning', 'Rate limit approaching');
} elseif ($violation['level'] === 1) {
// 一時的なブロック(5分)
return response()->json([
'error' => 'Temporary block',
'retryAfter' => 300
], 429)->header('Retry-After', 300);
} else {
// 永続的なブロック
return response()->json(['error' => 'IP address is blocked'], 403);
}
}
}
Node.js/Express での実装例
const express = require('express');
const app = express();
// 違反レベルを管理
const violationLevels = new Map();
function handleViolation(req, res, next) {
const ip = req.ip;
if (!violationLevels.has(ip)) {
violationLevels.set(ip, { level: 0, firstViolation: Date.now() });
}
const violation = violationLevels.get(ip);
// 違反レベルに応じた対応
if (violation.level === 0) {
// 警告のみ
res.setHeader('X-Warning', 'Rate limit approaching');
} else if (violation.level === 1) {
// 一時的なブロック(5分)
return res.status(429).json({
error: 'Temporary block',
retryAfter: 300
});
} else {
// 永続的なブロック
return res.status(403).json({ error: 'IP address is blocked' });
}
next();
}
app.use(handleViolation);
# 実装時の注意点
スクレイピング防止は、多層的なアプローチが重要。以下の対策を組み合わせることで、効果的に保護できる。
# 推奨される対策の組み合わせ
基本対策
- User-Agent検証
- レート制限
- IPブロック
中級対策
- CAPTCHA(reCAPTCHA v3、hCaptcha)
- セッション管理
- リクエストパターン分析
高度な対策
- WAFの利用
- 機械学習ベースの検出
- 動的コンテンツ生成
# 注意事項
- 過度な対策は避ける: 通常のユーザーへの影響を最小限に抑える
- 検索エンジンボットを許可: SEOに影響を与えないよう、主要な検索エンジンボットは許可する
- ログとモニタリング: スクレイピングの試行を記録し、対策の効果を測定する
- 法的対応: 技術的な対策に加えて、利用規約の明確化や法的対応も検討する
# バランスの取れたアプローチ
完全にスクレイピングを防ぐことは困難ですが、適切な対策を実装することで、大部分のスクレイピングを防ぎ、サーバーリソースとデータを保護できます。重要なのは、通常のユーザー体験を損なわずに、効果的な保護を実現することです。