# スクレイピング防止方法

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);

# 実装時の注意点

スクレイピング防止は、多層的なアプローチが重要。以下の対策を組み合わせることで、効果的に保護できる。

# 推奨される対策の組み合わせ

  1. 基本対策

    • User-Agent検証
    • レート制限
    • IPブロック
  2. 中級対策

    • CAPTCHA(reCAPTCHA v3、hCaptcha)
    • セッション管理
    • リクエストパターン分析
  3. 高度な対策

    • WAFの利用
    • 機械学習ベースの検出
    • 動的コンテンツ生成

# 注意事項

  • 過度な対策は避ける: 通常のユーザーへの影響を最小限に抑える
  • 検索エンジンボットを許可: SEOに影響を与えないよう、主要な検索エンジンボットは許可する
  • ログとモニタリング: スクレイピングの試行を記録し、対策の効果を測定する
  • 法的対応: 技術的な対策に加えて、利用規約の明確化や法的対応も検討する

# バランスの取れたアプローチ

完全にスクレイピングを防ぐことは困難ですが、適切な対策を実装することで、大部分のスクレイピングを防ぎ、サーバーリソースとデータを保護できます。重要なのは、通常のユーザー体験を損なわずに、効果的な保護を実現することです。

2026-01-01

同じタグを持つ記事をピックアップしました。