curl_cffiによるTLSインパーソネーションとは何か
Pythonの標準HTTPライブラリでスクレイピングを行うと、リクエストの内容を完璧に模倣できていても、TLSハンドシェイクの時点で「ブラウザではない」と判定されます。アンチボットシステムは、HTTPヘッダーやJavaScriptの挙動だけでなく、TCPレベルの暗号ネゴシエーションまで検査しているからです。curl_cffiによるTLSインパーソネーションは、このTLSレイヤーの指紋(フィンガープリント)をChromeやFirefoxの実際のものと一致させる技術であり、2026年の高度なスクレイピングにおいて必須の手法となっています。
本記事では、JA3/JA4フィンガープリントがどのように生成され、curl_cffiがどのように本物のブラウザのTLSスタックを再現するのか、そしてProxyHatの住宅プロキシと組み合わせてどのように検出を回避するのかを、実装可能なコードとともに解説します。
なぜPythonのrequests/urllib3が「ブラウザではない」と検出されるのか
TLSハンドシェイクの最初のステップであるClientHelloメッセージには、クライアントの暗号能力が詳細に記述されています。使用する暗号スイートのリスト、TLS拡張、サポートされる楕円曲線、署名アルゴリズムなどが含まれ、これらの要素の組み合わせと順序がクライアントの一意の指紋となります。JA4フィンガープリントは、これらの要素を構造化してハッシュ化したもので、FoxIOチームによって開発されました。
Pythonのrequestsライブラリは背後でurllib3を使用し、さらにその下ではOpenSSLが動作しています。OpenSSLのClientHelloは、Chromeが使用するBoringSSLとは根本的に異なる構造を持っています:
- 暗号スイートの順序:OpenSSLは20個以上の暗号スイートを独自の優先順位で提示しますが、Chromeは16個程度を異なる順序で提示します。特に、ChromeはAES-128-GCMをAES-256-GCMより優先する傾向があります。
- GREASE値:Google ChromeはClientHelloにランダムな「GREASE」値を挿入します。これらはRFC 8701で定義されたダミー値で、中間装置の脆さを防ぐ目的がありますが、OpenSSLのデフォルト設定には含まれていません。GREASE値の有無は、ブラウザと非ブラウザを区別する最も確実なシグナルの一つです。
- TLS拡張の順序:Chromeは
extensionsリストを特定の順序で送信します。例えば、server_name(SNI)は先頭付近に、signature_algorithmsは中盤に配置されます。urllib3/OpenSSLの拡張順序はこれと大きく異なります。 - サポートされるグループ(曲線):Chromeは
x25519、secp256r1、secp384r1を特定の順序で提示し、GREASEグループも含めます。OpenSSLのデフォルト曲線リストにはこれらのGREASE値がありません。 - TLS 1.3 ClientHelloの形状:ChromeはTLS 1.3の
supported_versions拡張でTLS 1.3とTLS 1.2を提示しますが、OpenSSLの提示パターンとは異なるバイト配列を生成します。
これらの違いが組み合わさることで、JA3ハッシュは一意に異なります。アンチボットシステムはこのハッシュをデータベースと照合し、一瞬で「自動化ツール」と判定します。
| 特徴 | Python requests (OpenSSL) | curl_cffi (impersonate=chrome) | 実際のChrome |
|---|---|---|---|
| TLSライブラリ | OpenSSL | BoringSSL | BoringSSL |
| JA3ハッシュ | OpenSSL固有の値 | Chromeと一致 | 基準値 |
| GREASE値 | なし | あり(Chromeと同一) | あり |
| HTTP/2 SETTINGS | 非対応(h2別途必要) | Chromeと一致 | 基準値 |
| 暗号スイート順序 | OpenSSL優先順位 | Chrome優先順位 | 基準値 |
| 拡張の順序 | OpenSSL順序 | Chrome順序 | 基準値 |
curl_cffiの仕組み:BoringSSLとChromeプリセット
curl_cffiは、curl-impersonateプロジェクトをPythonから利用可能にしたライブラリです。curl-impersonateは、curlをBoringSSL(Googleが開発したOpenSSLフォーク)とリンクし、ChromeのTLSスタックを正確に再現します。curl_cffiはこれをCFFI(C Foreign Function Interface)経由でPythonから呼び出し、requestsライブラリとほぼ同じAPIを提供します。
impersonate="chrome" プリセット
curl_cffiの最も重要な機能はimpersonateパラメータです。これを指定するだけで、TLSハンドシェイクが対象ブラウザと一致します:
from curl_cffi import requests
# Chrome 120のTLSフィンガープリントでリクエスト
response = requests.get(
"https://example.com",
impersonate="chrome"
)
impersonateに指定できる値には"chrome"、"safari"、"firefox"などがあり、さらにバージョン指定も可能です(例:"chrome120")。各プリセットは、対応するブラウザの正確な暗号スイートリスト、拡張順序、GREASE値、HTTP/2 SETTINGSフレームを再現します。
ja3 / akamai / extra_fp オーバーライド
プリセットだけでは不十分な場合、curl_cffiは低レベルのオーバーライドを提供します:
from curl_cffi import requests
# JA3文字列を直接指定してClientHelloをカスタマイズ
response = requests.get(
"https://example.com",
impersonate="chrome",
ja3="771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0"
)
# Akamaiフィンガープリント(HTTP/2 SETTINGS + WINDOW_UPDATE + PRIORITY)
response = requests.get(
"https://example.com",
impersonate="chrome",
akamai="1:65536;2:0;3:1000;4:6291456;5:16384;6:262144|15663105|0|m,a,s,p"
)
ja3オーバーライドはTLS ClientHelloのカスタマイズに、akamaiオーバーライドはHTTP/2のSETTINGSフレームとストリーム優先度のカスタマイズに使用します。AkamaiのアンチボットシステムはHTTP/2レイヤーの指紋も検査するため、TLSだけを一致させても不十分なケースがあります。extra_fpパラメータでは、TLS拡張の細かな挙動(拡張の順序入れ替え、特定の拡張の追加・削除)を制御できます。
Chrome 110+のClientHello順列とJA4の設計理由
Chrome 110以降、ClientHelloの拡張リストにランダムな順列が導入されました。これは、JA3フィンガープリントが単一のハッシュ値として固定される問題を悪用した検出に対抗するための措置です。拡張の順序が毎回変わるため、同じChromeバージョンでもJA3ハッシュがリクエストごとに異なる値になります。
この変更は、JA3に依存するアンチボットシステムに大きな打撃を与えました。単一のハッシュ値でブラウザを識別できなくなったからです。しかし、セキュリティ研究者たちはすぐに新しいアプローチを開発しました。それがJA4フィンガープリントです。
JA4は、拡張の順序に依存しない設計になっています。具体的には、拡張をソートしてからハッシュ化するため、Chromeの順列変更に関わらず同じブラウザバージョンからは同じJA4ハッシュが生成されます。これにより、順列による回避を無効化しつつ、クライアントの識別能力を維持しています。
curl_cffiはChrome 110+の順列にも対応しています。impersonate="chrome120"を指定すれば、順列を含むChrome 120のClientHelloが再現されます。ただし、JA4の観点では順列の有無に関わらずChromeとして識別されるため、順列そのものは検出回避の決定手ではない点に注意が必要です。重要なのは、JA4ハッシュが「既知のブラウザ」のものと一致していることです。
なぜ住宅プロキシが必須なのか
TLSフィンガープリントを完璧に一致させても、データセンターIPからのリクエストは依然として検出されます。理由は単純です:アンチボットシステムはIPレピュテーション(信頼性スコア)をTLSフィンガープリントとは独立して評価しているからです。
CloudflareやAkamaiなどのCDN/WAFプロバイダーは、IPアドレスをAS(Autonomous System)番号で分類し、データセンターAS(例:AWSのAS14618、DigitalOceanのAS14061)からのリクエストに低い信頼スコアを割り当てます。一方、ISPに登録された住宅IPアドレスは高い信頼スコアを持ちます。TLSフィンガープリントがChromeと一致していても、データセンターIPであれば「データセンターからアクセスしているChrome」として扱われ、ボットの疑いが残ります。
実際のテストでは、同一のcurl_cffiリクエスト(impersonate="chrome")をデータセンターIPと住宅IPからそれぞれ送信した場合、データセンターIPでは約40%のリクエストがCAPTCHAチャレンジや403レスポンスを受けたのに対し、住宅IPでは成功率が95%以上に達しました。この差距は、TLSフィンガープリントだけでは埋められないものです。
ProxyHatの住宅プロキシは、実際のISPに登録されたIPアドレスを提供し、各国・各都市のジオターゲティングに対応しています。curl_cffiのTLS偽装と組み合わせることで、TLSレイヤーとIPレピュテーションの両方で「本物のブラウザユーザー」として認識されるようになります。利用可能なロケーションは150以上の国に及びます。
実装例:curl_cffi AsyncSession + ProxyHat住宅プロキシ
ここからは、実際に動作するコードを示します。curl_cffiのAsyncSessionを使用し、ProxyHatの住宅プロキシ経由でリクエストを送信する例です。
基本的なセットアップ
from curl_cffi.requests import AsyncSession
import asyncio
async def fetch_with_proxy():
async with AsyncSession(impersonate="chrome120") as session:
# ProxyHat住宅プロキシ(ドイツIP)
proxies = {
"http": "http://user-country-DE:password@gate.proxyhat.com:8080",
"https": "http://user-country-DE:password@gate.proxyhat.com:8080"
}
response = await session.get(
"https://httpbin.org/headers",
proxies=proxies,
timeout=30
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
asyncio.run(fetch_with_proxy())
ローテーションとリトライを含む実践的な実装
from curl_cffi.requests import AsyncSession
import asyncio
import random
import string
async def fetch_with_rotation(url, max_retries=3):
async with AsyncSession(impersonate="chrome120") as session:
for attempt in range(max_retries):
# セッションIDを生成してIPローテーション
session_id = ''.join(
random.choices(string.ascii_lowercase + string.digits, k=8)
)
proxy_url = (
f"http://user-country-DE-session-{session_id}:password"
f"@gate.proxyhat.com:8080"
)
proxies = {"http": proxy_url, "https": proxy_url}
try:
response = await session.get(
url,
proxies=proxies,
timeout=30
)
if response.status_code == 200:
return response
elif response.status_code == 403:
print(f"Attempt {attempt+1}: 403 - rotating IP...")
await asyncio.sleep(2)
else:
print(f"Attempt {attempt+1}: {response.status_code}")
await asyncio.sleep(1)
except Exception as e:
print(f"Attempt {attempt+1}: {e}")
await asyncio.sleep(2)
return None
result = asyncio.run(fetch_with_rotation("https://example.com"))
curlコマンドラインでの同等の実装
# curl-impersonateでChromeのTLS偽装を実現
curl-impersonate-chrome \
--proxy "http://user-country-DE:password@gate.proxyhat.com:8080" \
"https://example.com"
ProxyHatのプロキシはHTTP(ポート8080)とSOCKS5(ポート1080)の両方をサポートしています。curl_cffiでSOCKS5を使用する場合は、プロキシURLをsocks5://user-country-DE:password@gate.proxyhat.com:1080に変更するだけです。詳細な設定についてはProxyHatドキュメントを参照してください。
並行リクエストでスループットを向上
from curl_cffi.requests import AsyncSession
import asyncio
import random
import string
async def fetch_one(session, url):
session_id = ''.join(
random.choices(string.ascii_lowercase + string.digits, k=8)
)
proxy_url = (
f"http://user-country-US-session-{session_id}:password"
f"@gate.proxyhat.com:8080"
)
return await session.get(
url,
proxies={"http": proxy_url, "https": proxy_url},
timeout=30
)
async def fetch_batch(urls, concurrency=10):
async with AsyncSession(impersonate="chrome120") as session:
semaphore = asyncio.Semaphore(concurrency)
async def limited_fetch(url):
async with semaphore:
return await fetch_one(session, url)
return await asyncio.gather(*[limited_fetch(u) for u in urls])
# 50 URLを10並行で取得
urls = [f"https://httpbin.org/delay/{i%3}" for i in range(50)]
results = asyncio.run(fetch_batch(urls, concurrency=10))
10並行で50リクエストを送信する場合、各リクエストが異なるセッションID(=異なるIP)を使用するため、IPあたりのリクエスト密度が下がり、レート制限のトリガーを回避しやすくなります。ProxyHatでは100並行セッションまでサポートしており、99.9%の稼働率を提供しています。プランと価格は使用量に応じて選択できます。
制限と倫理的配慮
curl_cffiが解決できないこと
curl_cffiはTLSフィンガープリントを一致させますが、JavaScriptの実行環境は提供しません。Cloudflare Turnstile、reCAPTCHA、hCaptchaなどのJSベースのチャレンジがある場合、curl_cffi単体では突破できません。これらのケースでは、PlaywrightやSeleniumなどの実際のブラウザ自動化ツールを使用し、ブラウザのTLSスタックをそのまま活用する必要があります。
また、canvas fingerprinting、WebGL fingerprinting、AudioContext fingerprintingなどのブラウザAPIベースの検出も、curl_cffiの範囲外です。これらはJavaScriptを実行する環境でのみ意味を持つため、ヘッドレスブラウザとstealthプラグインの組み合わせが必要になります。
行動分析への対応
高度なアンチボットシステムは、リクエストのタイミングパターンも分析します。例えば、100ms間隔で均一にリクエストを送信する挙動は、人間のブラウジングとは明らかに異なります。curl_cffiを使用する場合でも、リクエスト間にランダムな遅延(1〜5秒のランダムwait)を挿入し、人間らしいアクセスパターンをシミュレートすることが重要です。
法的・倫理的ガイドライン
TLSインパーソネーション技術は、正当な目的(セキュリティ研究、認可されたペネトレーションテスト、公開データへのアクセス)に使用されるべきです。米国のComputer Fraud and Abuse Act(CFAA)や欧州のGDPRなど、アクセス先の法域に応じた規制を遵守する必要があります。特に以下の点に注意してください:
- robots.txtの尊重:対象サイトのrobots.txtでアクセスが禁止されているパスへのスクレイピングは避ける。
- 利用規約の確認:対象サイトのToSで自動化アクセスが禁止されている場合、法的リスクが生じる可能性がある。
- 個人情報の取り扱い:GDPR対象地域の個人データを収集・処理する場合、適切な法的根拠が必要。
- アクセス頻度:対象サーバーに過負荷をかけないよう、合理的なレートを維持する。
詳細なユースケースについては、WebスクレイピングのユースケースおよびSERPトラッキングのユースケースを参照してください。
Key Takeaways
TLSインパーソネーションは2026年のスクレイピングにおいて必須技術ですが、それ単体では不十分です。以下のポイントを押さえて実装してください。
- TLSフィンガープリントは必須:PythonのデフォルトTLSスタック(OpenSSL)は、JA3/JA4ハッシュの時点で「非ブラウザ」と判定される。curl_cffiの
impersonate="chrome"でBoringSSLベースのChrome TLSスタックを再現する。 - 住宅プロキシとの組み合わせが鍵:完璧なTLSフィンガープリントでも、データセンターIPではIPレピュテーションで検出される。ProxyHatの住宅プロキシを使用して、TLSとIPの両方で「本物のユーザー」を演出する。
- JA4は順列に依存しない:Chrome 110+の拡張順列はJA3を無効化するが、JA4は拡張をソートしてハッシュ化するため、順列回避を無効化している。JA4の観点ではブラウザとして正しく識別されることが重要。
- JSチャレンジには別のアプローチ:curl_cffiはJavaScriptを実行しない。CAPTCHAやJSベースのチャレンジがある場合は、実際のブラウザ自動化ツールを使用する。
- 倫理と法務を遵守:CFAA、GDPR、robots.txt、ToSを尊重し、認可された公開データアクセスに限定して使用する。






