Pythonのrequestsライブラリでスクレイピングを書いた経験があるなら、次の現象に心当たりがあるはずだ。コードは正しい、ヘッダーも完璧に設定した、User-Agentは最新のChromeを模している——それでも403やCAPTCHAチャレンジが返ってくる。原因の多くはTLSレイヤーにある。curl_cffiを使ったTLS偽装は、この「見えない壁」を突破するための実践的なアプローチだ。
2026年現在、CloudflareやAkamai、Datadomeなどのアンチボットシステムは、HTTPヘッダーだけでなくTLSハンドシェイクそのものを指紋として読み取っている。PythonのデフォルトTLSスタック(OpenSSLベース)が発するClientHelloメッセージは、本物のブラウザとは決定的に異なる構造を持っており、JA3/JA4ハッシュという形で「これはボットだ」と宣言してしまっている。本記事では、curl_cffiがどのようにこの問題を解決するか、そしてなぜレジデンシャルプロキシとの組み合わせが不可欠なのかを、実装可能なコードとともに解説する。
curl_cffiを使ったTLS偽装とは何か
TLS偽装(TLS Impersonation)とは、HTTPクライアントが発するTLS ClientHelloメッセージを、特定のブラウザ(Chrome、Firefox、Safariなど)が送信するものと完全に一致するように再現する技術だ。curl_cffiは、この技術をPythonから利用できるようにしたライブラリで、curl-impersonateプロジェクトをベースに構築されている。
具体的には、curl_cffiはimpersonate="chrome"というパラメータを渡すだけで、Chromeの正確なTLS指紋——暗号スイートの順序、拡張機能の並び、サポートされる曲線、GREASE値、HTTP/2 SETTINGSフレーム——をすべて再現する。これにより、アンチボットシステムはTLSレイヤーでの識別ができなくなる。
ただし、TLS偽装だけでは十分ではない。完璧なTLS指紋を持っていても、データセンタIPからのリクエストはIP評価スコアで弾かれる。後述する通り、ウェブスクレイピングで安定した成功率を達成するには、TLS偽装とレジデンシャルプロキシの両方が必要だ。
なぜPythonのClientHelloが「ブラウザではない」と検出されるのか
JA3フィンガープリントの仕組み
JA3は、Salesforceのエンジニアチームによって2017年に発表されたTLSクライアント指紋化手法で、ClientHelloメッセージ内の特定フィールドを元にMD5ハッシュを生成する(元のJA3記事を参照)。具体的には以下の4つの要素を連結してハッシュ化する:
- SSL/TLSバージョン(例: TLS 1.3 = 773)
- 暗号スイートのリスト(例: 4865-4866-4867-49195-49199-...)
- 拡張機能のリスト(例: 0-23-65281-10-11-...)
- サポートされる楕円曲線(例: 29-23-24)
Pythonのrequestsライブラリは背後にOpenSSLを使用する。OpenSSLのClientHelloは、ChromeのBoringSSLベースのClientHelloとは以下の点で決定的に異なる:
- 暗号スイートの順序: Chrome 120は約17個の暗号スイートを特定の順序で送信するが、OpenSSLは異なる順序で異なるスイートを送信する
- 拡張機能の順序: Chromeは25以上のTLS拡張機能を独自の順序で並べる。OpenSSLの拡張機能リストは短く、順序も異なる
- GREASE値: ChromeはRFC 8701に従ってGREASE(Generate Random Extensions And Sustain Extensibility)値を暗号スイートと拡張機能に挿入する。OpenSSLはこれをデフォルトで行わない
- 署名アルゴリズム: ChromeとOpenSSLは異なる署名アルゴリズムリストを送信する
結果として、Python requestsのJA3ハッシュはChromeのハッシュと完全に異なり、アンチボットシステムは1回のTLSハンドシェイク(約1-RTT、通常10ms以内)で「これはPythonボットだ」と識別できる。
JA4とその安定性
JA4は、FoxIO社によって開発された次世代のTLS指紋化手法で、JA3の問題点を解決している(JA4仕様を参照)。最大の改善点は順序の安定性だ。
Chrome 110以降、GoogleはClientHello内の暗号スイートと拡張機能の順序をランダム化し始めた。これにより、同じChromeバージョンでも接続ごとに異なるJA3ハッシュが生成されるようになった。JA3は順序依存型のハッシュであるため、このランダム化により実質的に機能しなくなった。
JA4はこの問題に対処するため、暗号スイートと拡張機能を値でソートしてからハッシュ化する。つまり、順序が変わってもハッシュ値は変わらない。これにより、Chrome 110+の順列ランダム化を回避しつつ、クライアントを安定して識別できる。
curl_cffiの最新バージョンは、このChrome 110+の順列にも対応している。impersonate="chrome120"などのプリセットを使用すると、Chrome 120の順列パターンを再現し、JA4ハッシュも一致する。
OpenSSLとBoringSSLの決定的な違い
PythonのTLSスタックはOpenSSL(またはその派生)を使用するのに対し、ChromeはGoogleがメンテナンスするBoringSSLを使用する。この2つは同じTLS 1.3プロトコル(RFC 8446)を実装しているが、ClientHelloの構造には根本的な違いがある:
| 要素 | OpenSSL (Python requests) | BoringSSL (Chrome) |
|---|---|---|
| 暗号スイート数 | 約25個(過剰) | 約17個(厳選) |
| GREASE値 | なし(デフォルト) | あり(RFC 8701準拠) |
| 拡張機能の順序 | OpenSSL固有の順序 | Chrome固有の順序 |
| ALPNプロトコル | h2, http/1.1 | h2, http/1.1(順序が異なる場合あり) |
| 署名アルゴリズム | OpenSSLのデフォルトセット | Chromeの厳選セット |
この違いは、User-Agentヘッダーで「Chrome」を偽装しても隠せない。アンチボットシステムはTLSレイヤーとHTTPレイヤーの指紋を比較し、不一致があれば即座にブロックする。
curl_cffiがChromeのTLS指紋を再現する仕組み
BoringSSLへの置き換え
curl_cffiの核心は、標準のlibcurl/OpenSSLスタックを、curl-impersonateプロジェクトが提供するBoringSSLベースのビルドに置き換えることだ。これにより、PythonコードからChromeと同じTLSエンジンを使用できる。curl_cffiはCFFI(C Foreign Function Interface)を使用して、このカスタムビルドされたlibcurlをPythonから直接呼び出す。
impersonateプリセット
curl_cffiの最も一般的な使用方法は、impersonateパラメータを指定することだ:
from curl_cffi.requests import AsyncSession
async with AsyncSession(impersonate="chrome") as session:
response = await session.get("https://example.com")
利用可能なプリセットには以下が含まれる:
chrome— 最新のChrome安定版chrome120,chrome124,chrome131— 特定バージョンのChromesafari17_0— Safari 17.0edge99— Edge 99firefox133— Firefox 133
各プリセットは、対応するブラウザの正確なClientHello構造、HTTP/2 SETTINGSフレーム、およびその他のプロトコルレベルの挙動を再現する。2026年時点で10種類以上のプリセットが利用可能だ。
ja3/akamai/extra_fpオーバーライド
より細かい制御が必要な場合、curl_cffiはプリセットを上書きするオプションを提供する:
from curl_cffi.requests import AsyncSession
async with AsyncSession(
impersonate="chrome",
ja3="773,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_fingerprint="1:65536;2:0;3:1000;4:6291456;6:262144|15663105|0|m,a,s,p",
extra_fp={
"tls_signature_algorithms": [
"ecdsa_secp256r1_sha256",
"rsa_pss_rsae_sha256",
"rsa_pkcs1_sha256",
],
},
) as session:
response = await session.get("https://example.com")
ja3パラメータはClientHelloの生の構造を直接指定し、akamai_fingerprintはHTTP/2 SETTINGSフレームの内容を制御する。extra_fpは署名アルゴリズムなどの追加フィンガープリント要素を上書きする。これらは、アンチボットシステムが特定のJA3やAkamai指紋をブロックリストに登録している場合に有用だ。
HTTP/2 SETTINGSフレームの重要性
TLSハンドシェイクが完了した後、ブラウザはHTTP/2接続を確立する際にSETTINGSフレームを送信する。このフレームには、最大ストリーム数、初期ウィンドウサイズ、ヘッダーテーブルサイズなどのパラメータが含まれる。Chrome、Firefox、Safariはそれぞれ異なるSETTINGS値を使用する。
Akamaiのアンチボットシステムは、このHTTP/2 SETTINGSフレームの指紋化(Akamai fingerprint)を行うことで、TLS指紋が一致してもHTTP/2レイヤーで不一致を検出できる。curl_cffiは、impersonateプリセットを使用することで、このSETTINGSフレームも同時に再現する。
Chrome 110+のClientHello順列とJA4の設計意図
Chrome 110以降、GoogleはClientHello内の暗号スイートと拡張機能の順序をセッションごとにランダム化するようになった。これは、JA3のような順序依存型指紋を無効化するための措置だ。具体的には、同じChrome 120ブラウザから2回連続で接続しても、暗号スイートの並び順が変わるため、異なるJA3ハッシュが生成される。
しかし、このランダム化は値のランダム化ではなく順序のランダム化である。送信される暗号スイートのセット自体は変わらない。JA4はこの性質を利用し、値をソートしてからハッシュ化することで、順序の変動を吸収する。
curl_cffiでChrome 110+を偽装する場合、impersonate="chrome120"などのプリセットがこの順列パターンを再現する。重要なのは、JA4ハッシュが一致することだ。curl_cffiのChrome 120プリセットを使用すれば、JA4ハッシュは実際のChrome 120と一致する。
なお、JA3フィンガープリント偽装(JA3 fingerprint spoofing)の観点からは、Chrome 110+の順列によりJA3ハッシュ自体は毎回変化する。しかし、アンチボットシステムもJA4に移行しているため、JA4の一致がより重要な指標となっている。
なぜレジデンシャルプロキシがTLS偽装と不可分なのか
完璧なTLS指紋を持つリクエストでも、データセンタIPから送信されればブロックされる。理由はシンプルだ。アンチボットシステムは複数のシグナルを組み合わせて評価するからだ。
CloudflareやAkamaiは、IPアドレスのASN(Autonomous System Number)を確認し、それがデータセンタプロバイダー(AWS、Azure、GCPなど)に属するか、ISP(Deutsche Telekom、Comcastなど)に属するかを判定する。データセンタASNからのリクエストは、TLS指紋が完璧であっても、初期評価スコアが低く設定される。実際、データセンタIPはレジデンシャルIPと比較して40〜60%高いブロック率を持つとされている。
以下の表は、TLS偽装とプロキシタイプの組み合わせによる成功率の傾向を示している:
| アプローチ | TLS指紋 | IPタイプ | 予想成功率 |
|---|---|---|---|
| Python requests + プロキシなし | 不一致(OpenSSL) | 自宅/オフィス | 約40% |
| Python requests + データセンタプロキシ | 不一致(OpenSSL) | データセンタ | 約10% |
| curl_cffi (impersonate=chrome) + データセンタプロキシ | 一致 | データセンタ | 約45% |
| curl_cffi (impersonate=chrome) + レジデンシャルプロキシ | 一致 | ISP | 約85-95% |
| Playwright (本物のChrome) + レジデンシャルプロキシ | 完全一致 | ISP | 約90-98% |
このデータから分かるように、TLS偽装とレジデンシャルプロキシは補完的であり、どちらか一方では不十分だ。ProxyHatのレジデンシャルプロキシは、世界中のロケーションでISP由来のIPアドレスを提供し、TLS偽装と組み合わせることで最高の成功率を実現する。
実装例: curl_cffi AsyncSession + ProxyHatでTLS偽装を実現する
以下に、curl_cffiのAsyncSessionとProxyHatのレジデンシャルプロキシを組み合わせた実装例を示す。ProxyHatのゲートウェイはgate.proxyhat.com:8080(HTTP)またはgate.proxyhat.com:1080(SOCKS5)でアクセスし、ユーザー名でジオターゲティングとセッション制御を指定する。詳細な設定についてはProxyHat公式ドキュメントを参照されたい。
基本例: Chrome偽装 + ドイツレジデンシャルプロキシ
import asyncio
from curl_cffi.requests import AsyncSession
async def fetch_with_tls_impersonation():
proxy = "http://user-country-DE:your_password@gate.proxyhat.com:8080"
async with AsyncSession(impersonate="chrome") as session:
response = await session.get(
"https://httpbin.org/headers",
proxies={"https": proxy},
timeout=30,
)
print(f"Status: {response.status_code}")
print(f"Headers: {response.json()}")
asyncio.run(fetch_with_tls_impersonation())
このコードは、ChromeのTLS指紋を持ち、ドイツのISP IPアドレスから送信されるリクエストを生成する。アンチボットシステムにとって、これはドイツのChromeユーザーからの通常のアクセスに見える。
応用例: IPローテーション + リトライ
import asyncio
from curl_cffi.requests import AsyncSession
PROXY_TEMPLATE = (
"http://user-session-{sid}-country-DE:your_password@gate.proxyhat.com:8080"
)
async def scrape_with_rotation(url, max_retries=3):
async with AsyncSession(impersonate="chrome120") as session:
for attempt in range(max_retries):
session_id = f"rot-{attempt}-{int(asyncio.get_event_loop().time())}"
proxy = PROXY_TEMPLATE.format(sid=session_id)
try:
response = await session.get(
url,
proxies={"https": proxy},
timeout=30,
)
if response.status_code == 200:
return response.text
elif response.status_code == 403:
print(f"Attempt {attempt}: 403, rotating IP...")
continue
else:
print(f"Attempt {attempt}: {response.status_code}")
except Exception as e:
print(f"Attempt {attempt} error: {e}")
await asyncio.sleep(2)
return None
asyncio.run(scrape_with_rotation("https://example.com/api/data"))
この例では、各リトライでセッションIDを変更することで新しいIPアドレスを取得し、同時にChrome 120のTLS指紋を維持する。ProxyHatのセッションID機能により、リクエストごとに異なるレジデンシャルIPアドレスを使用できる。403応答を受信した場合は即座にIPをローテーションし、2秒の待機後に再試行する。
SOCKS5プロキシの使用
SOCKS5プロトコルが必要な場合は、ポート1080を使用する:
import asyncio
from curl_cffi.requests import AsyncSession
async def fetch_via_socks5():
proxy = "socks5://user-country-DE:your_password@gate.proxyhat.com:1080"
async with AsyncSession(impersonate="chrome") as session:
response = await session.get(
"https://example.com",
proxies={"https": proxy},
timeout=30,
)
print(f"Status: {response.status_code}")
asyncio.run(fetch_via_socks5())
SOCKS5はTCPレベルでトンネリングを行うため、HTTPプロキシよりも低いオーバーヘッドで通信できる。ただし、プロキシチェーンが必要な場合や特定のネットワーク環境でのみ使用を推奨する。ほとんどのユースケースではHTTPプロキシ(ポート8080)で十分だ。
curlコマンドラインでの使用
curl-impersonateのCLIツールを使用する場合も、ProxyHatプロキシを同様に指定できる:
curl-impersonate-chrome \
--proxy "http://user-country-DE:your_password@gate.proxyhat.com:8080" \
"https://httpbin.org/headers"
CLIツールはデバッグやワンショットリクエストに便利だが、本格的なスクレイピングにはPythonのcurl_cffiを使用することを推奨する。非同期処理、リトライロジック、レスポンス処理を柔軟に実装できる。
よくある間違いとエッジケース
1. impersonateとUser-Agentの不一致
impersonate="chrome"を使用しているのに、User-AgentヘッダーにFirefoxの文字列を設定すると、TLS指紋とHTTPヘッダーが矛盾し、逆に検出されやすくなる。impersonateを使用する場合は、対応するブラウザのUser-Agentを維持するか、ヘッダーを明示的に設定しない(curl_cffiが自動的に適切なヘッダーを送信する)。
2. HTTP/2の優先順位の忘れ
HTTP/2接続では、ストリームの優先順位(PRIORITYフレームまたはPRIORITY_UPDATEフレーム)もブラウザごとに異なる。curl_cffiのimpersonateはこれも再現するが、カスタムのakamai_fingerprintを指定する場合は、HTTP/2優先順位も正確に設定する必要がある。
3. 古いプリセットの使用
impersonate="chrome99"のような古いプリセットを使用すると、2026年の時点で古いブラウザを模すことになり、それ自体が不自然になる。可能な限り最新のプリセット(chrome131など)を使用し、ターゲットサイトのユーザーベースに合わせる。
4. JavaScriptチャレンジへの対処不足
TLS偽装はネットワークレベルの指紋を回避するが、Cloudflare TurnstileやDatadomeのJavaScriptチャレンジは解決できない。これらのチャレンジは、ブラウザのJavaScriptエンジンで特定のコードを実行し、その結果をサーバーに送信することを要求する。curl_cffiはJavaScriptを実行しないため、この種のチャレンジには対応できない。この場合は、PlaywrightやPuppeteerなどの実際のブラウザを使用する必要がある。
5. レジデンシャルプロキシの不使用
最もよくある間違いは、curl_cffiだけで十分だと考えてデータセンタプロキシを使用することだ。前述の表の通り、TLS偽装 + データセンタIPの成功率は約45%に留まる。レジデンシャルプロキシを追加することで、成功率は85-95%に跳ね上がる。ProxyHatの料金プランは、レジデンシャルプロキシを手頃な価格で提供している。
限界と倫理的考慮
curl_cffiを使ったTLS偽装は強力なツールだが、万能ではない。以下の限界を理解しておく必要がある:
- JavaScriptチャレンジ: Cloudflare Turnstile、Datadome、PerimeterXなどのJSチャレンジは、実際のブラウザエンジンでのJavaScript実行を必要とする。curl_cffiはこれを解決できない。
- キャンバスフィンガープリント: ブラウザのCanvas APIレンダリング結果に基づく指紋は、ブラウザ環境でのみ生成可能で、curl_cffiでは再現できない。
- 行動分析: マウス移動、スクロールパターン、キーストロークタイミングなどの行動シグナルは、実際のブラウザでのみ生成できる。
倫理と合法性については、TLS偽装技術は正当な目的——セキュリティ研究、認可されたペネトレーションテスト、公開データへのアクセス——に使用すべきだ。米国のComputer Fraud and Abuse Act(CFAA)や欧州のGDPRは、アクセス権限のないシステムへのアクセスや、個人データの無断収集を制限している。スクレイピング対象のrobots.txtと利用規約(ToS)を確認し、公開データのみを収集し、収集したデータの取り扱いが適用されるデータ保護法に準拠すること。
SERPトラッキングや価格モニタリングなど、公開データへの正当なアクセスにおいて、TLS偽装とレジデンシャルプロキシの組み合わせは、過度に攻撃的なアンチボットシステムに対する正当な防御手段として位置づけられる。
Key Takeaways
TLS偽装の要点をまとめると:
- PythonのデフォルトTLSスタックは即座に検出される: OpenSSLのClientHelloはChromeのBoringSSLベースのものと構造が異なり、JA3/JA4ハッシュで識別される。
- curl_cffiはBoringSSLを使用してChromeの指紋を再現する:
impersonate="chrome"でTLS、HTTP/2、その他のプロトコルレベルの指紋を一致させる。 - Chrome 110+の順列ランダム化はJA3を無効化したがJA4は対応済み: JA4は値をソートしてからハッシュ化するため、順序の変動に影響されない。
- TLS偽装だけでは不十分: データセンタIPはIP評価で低スコアとなり、レジデンシャルプロキシとの組み合わせが必須。
- curl_cffiはJavaScriptチャレンジを解決できない: JSチャレンジが必要なサイトではPlaywrightなどの実際のブラウザを使用する。
- 倫理的な使用を心がける: 公開データへの正当なアクセスに限定し、CFAA/GDPRなどの法規制を遵守する。






