1. 結論(この記事で得られること)
この記事では、Cloudflareのbot対策(Bot Fight Mode、Turnstile、WAF等)に引っかかってしまう具体的なパターンと、実務で使える回避策を解説します。
得られること:
- HTTPクライアントが403/429エラーになる原因の特定方法
- 正規のクライアントなのにbotと誤判定される典型的な実装ミス
- JavaScriptチャレンジを突破できない原因と対処法
- スクレイピング・自動テスト・監視ツールでの実装例
- AI(Claude/GPT)を使った効率的なトラブルシューティング方法
昔、私もAPIクライアントで突然403が返ってきて、ログには何も残ってないし、curlでは通るのに本番だけダメで半日溶かした経験があります。結局User-Agentとか初歩的なところだったんですが、当時は切り分け方を知らなくて…。
この記事では、そういう時間を無駄にしないための実践的な知識をまとめます。
2. 前提(環境・読者層)
想定読者:
- Cloudflare保護下のサイトにHTTPリクエストを送る必要があるエンジニア
- スクレイピング、E2Eテスト、監視ツール、APIクライアントを実装している方
- 403 Forbidden、429 Too Many Requests、Challenge画面で困っている方
前提知識:
- HTTP通信の基礎(リクエスト/レスポンス、ヘッダー)
- 基本的なPython/Node.js/Goなどのプログラミング経験
- Cloudflareが保護しているサイトにアクセスする必要性があること
環境:
- Cloudflareで保護されたWebサイト(Free〜Enterpriseプラン)
- クライアント側:Python 3.9+、Node.js 18+、Go 1.20+ など
注意:
この記事は正規の目的でのアクセスを前提としています。不正アクセスや利用規約違反を推奨するものではありません。
3. Before:よくあるつまずきポイント
3-1. 典型的なエラーパターン
パターン1:即座に403が返る
import requests
response = requests.get("https://example.com")
# => 403 Forbidden (HTML内に "Attention Required! | Cloudflare" が含まれる)
curlやブラウザでは普通にアクセスできるのに、スクリプトだけ弾かれる。
パターン2:Challenge画面(5秒待機)が返る
<!DOCTYPE html>
<html>
<head><title>Just a moment...</title></head>
<!-- JavaScriptチャレンジが含まれる -->
パターン3:429 Too Many Requests
HTTP/1.1 429 Too Many Requests
CF-RAY: xxxxx-NRT
cf-mitigated: challenge
レート制限に引っかかっている場合と、bot判定されている場合の両方あり。
3-2. なぜ引っかかるのか?
Cloudflareは複数の指標でbot判定しています:
① User-Agentの異常
・デフォルトのUA(「python-requests/2.28.0」など)
・UAが空、または古すぎる
② TLS Fingerprint
・HTTPライブラリごとに異なるTLS handshakeパターン
・ブラウザと明らかに違う接続パターン
③ HTTPヘッダーの不自然さ
・Accept、Accept-Language、Accept-Encodingの欠如
・ヘッダーの順序がブラウザと異なる
④ JavaScriptチャレンジ未対応
・JSを実行できないクライアント
・Cookieを保存しない実装
⑤ IPレピュテーション
・VPN、データセンターIP、過去の不正アクセス履歴
3-3. 私がハマったケース
昔、社内のヘルスチェック監視ツールで、突然Cloudflare導入後に全部アラートが飛ぶようになりました。調べたら:
// 問題のあったコード
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
Go標準の「http.Client」はUser-Agentを 「Go-http-client/1.1」 にするので、即座にbot判定。しかもTLS fingerprintも独特。
4. After:基本的な解決パターン
4-1. User-Agentとヘッダーの正規化
基本方針:
できるだけ実際のブラウザに見せかける。
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'ja,en-US;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
response = requests.get("https://example.com", headers=headers)
注意点:
- User-Agentは定期的に更新する(古いバージョンは逆に怪しい)
- 実際にそのブラウザから送られるヘッダーをコピーする
4-2. TLS Fingerprintの模倣
Pythonの「requests」はOpenSSLベースなので、TLS fingerprintがブラウザと異なります。
解決策:curl_cffiを使う
# pip install curl_cffi
from curl_cffi import requests
response = requests.get(
"https://example.com",
impersonate="chrome120" # Chrome 120のTLS fingerprintを模倣
)
print(response.status_code) # => 200
これだけで、TLSレベルの判定を回避できます。私も最近知って「もっと早く知りたかった…」ってなりました。
4-3. JavaScriptチャレンジへの対応
方法1:Seleniumでブラウザ自動化
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--headless')
options.add_argument('--disable-blink-features=AutomationControlled')
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
# 5秒待機してチャレンジをクリア
import time
time.sleep(6)
html = driver.page_source
driver.quit()
方法2:Playwrightを使う(推奨)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
)
page = context.new_page()
page.goto("https://example.com")
# Cloudflare challengeを自動で待機
page.wait_for_load_state("networkidle")
content = page.content()
browser.close()
Playwrightの方がSeleniumより検出されにくく、動作も速いです。
4-4. Node.jsでの実装例
// npm install playwright
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
});
const page = await context.newPage();
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');
const content = await page.content();
console.log(content);
await browser.close();
})();
4-5. 判定フローチャート
403エラーが出た
↓
User-Agent確認
├─ デフォルト → 変更
└─ 問題なし
↓
curl_cffi等でTLS対策
↓
まだダメ?
↓
JavaScriptチャレンジ
↓
Playwright/Selenium導入