curlは403を返すがブラウザは動作する:見逃している要素を見つける

ブラウザはロードするが、curlは403を返す。この2つのリクエストの間のギャップは常に有限であり、常に見つけることができます。ここでは、それを5分で二分する方法を紹介します。

URLをChromeに貼り付けるとページがロードされます。同じURLをcurlに貼り付けると403 Forbiddenが返されます。この2秒間でリソースに何も変わっていないので、違いはリクエストにあります。リクエストは有限で検査可能なものです。このガイドは二分法の手順です:ブラウザが送信したものを正確に再現し、403が戻るまで部分を削除します。最後に削除したものが答えです。通常の順序で疑わしいのは、User-Agent、Referer、クッキー、TLSフィンガープリント、JavaScriptです。

ステップ0:リクエストが本当に異なることを証明する

理論を立てる前に、curlが実際に送信するものを確認してください。-vを使用すると、リクエストライン、すべてのヘッダー、TLSハンドシェイクが表示されます。デフォルトのcurlリクエストは驚くほど薄いです。通常はHostUser-Agent: curl/8.xAccept: */*です。ブラウザはさらに十数個のヘッダーを送信します。

# what you send, what you get back, and the TLS details
curl -v -o /dev/null https://target.example/page

# just the response headers, quickly
curl -sS -o /dev/null -D - https://target.example/page

ステータスラインと同様にレスポンスヘッダーを注意深く読みます。最も一般的なケースでは、Vary: User-Agentがサーバーが誰であるかによって異なるレスポンスを意図的に提供することを意味します。よく文書化されたStack Overflowのケースでは、curl -fがプレーンなApache 2.4.38ホストに対して403を返し、wgetが同一のファイルを200で取得しました。そして、成功したレスポンスにはまさにそのVary: User-Agentヘッダーが含まれていました。curlに-A 'Wget/1.21.2'を渡すことで即座に修正されました。サイトの所有者は、乱用後にcurlのユーザーエージェントをブラックリストに載せていました。リクエストに関する他のことは重要ではありませんでした。

出力を読みながら:curl: (22) 要求されたURLはエラーを返しました:403は別の問題ではありません。終了コード22は、-f/--failがHTTPエラーで行うことです。このフラグは本文を抑制し、コマンドを失敗させます。-fを一時的に削除して、実際にブロックページを読み、通常はあなたを止めたシステムの名前を確認してください。

ステップ1:cURLとしてコピー、30秒の答え

主要なブラウザの両方が、ちょうど行った正確なリクエストを提供できます。DevToolsを開き、ネットワークタブに移動し、リクエストを右クリックして「cURLとしてコピー」を選択します。Chromeはバージョン26以降、Firefoxは31以降でこれを提供しており、出力にはすべてのヘッダー、すべてのクッキー、リファラーが含まれています。それをターミナルに貼り付けます:もし200が返されるなら、問題はリクエストの形にあり、ステップ2でどの部分かを見つけます。

ここで多くの時間を浪費する落とし穴があります。URLがリダイレクトする場合、ネットワークパネルはナビゲーションでクリアされ、間違ったリクエストをコピーします。Chromeでは「ログを保持」、Firefoxでは「永続的なログ」をチェックして、リダイレクトしたリクエストと最終的にコンテンツを提供したリクエストの両方を確認できるようにします。リダイレクトチェーンは重要です:よく知られたUnix Stack Exchangeスレッドでは、サーバーはRefererを確認し、次に何も確認しない場所に302でバウンスしました—これにより、チェーン全体が見えるまで失敗がランダムに見えました。

Chromeブラウザリクエストとデフォルトのcurlリクエストのヘッダー数、クッキー、TLSフィンガープリント、JavaScriptサポートの並列比較
このシナリオのすべての403はこのギャップの中に隠れています。1列ずつ閉じてください。

ステップ2:ヘッダーを二分する

動作する「cURLとしてコピー」コマンドから始めて、1つずつヘッダーを削除し、削除後に再実行します。403が戻る最初の削除が犯人を示します。実際には、ほとんどの場合、4つのうちの1つです。

curl -sS -o /dev/null -w '%{http_code}\n' \
  -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-GB,en;q=0.9' \
  -e 'https://target.example/' \
  -b 'session=abc123; consent=1' \
  -L \
  'https://target.example/page'

この段階での2つの締めくくりの詳細です。URLを引用符で囲みます:クエリ文字列に&やアクセストークンが含まれていると、シェルによって破損され、結果として403がサーバーとは無関係になります。そして、PHPやNodeからデバッグしている場合、同じヘッダーセットをそこに複製します。PHP内のlibcurlのデフォルトはコマンドラインツールのものと異なるため、同一のリクエストがターミナルで通過し、コードで失敗する理由です。curlプロキシレシピでは、フラグ構文を完全にカバーしています。

ステップ3:同一のヘッダーがまだ403を返す場合

ブラウザのヘッダーのバイト単位のコピーがまだ失敗する場合、決定はヘッダーが解析される前に行われました。2つのレイヤーがその下にあります。

TLSフィンガープリント。 あなたのClientHello — 暗号スイート、拡張、カーブの優先順位、ALPN、そしてそれに続くHTTP/2設定フレーム — はJA3またはJA4値にハッシュされます。OpenSSLに基づいて構築されたcurlは、ブラウザが生成することのないものを生成し、アンチボットシステムはそれを主張されたUser-Agentと比較します。OpenSSLのようにハンドシェイクしながらChromeであると主張することは、彼らが捕まえるために構築された矛盾です。修正はブラウザハンドシェイクを再現するクライアントです:コマンドラインでcurl-impersonate、またはPythonからcurl_cffi

# pip install curl_cffi
from curl_cffi import requests

proxy = "http://USER:PASS@gate.quantumproxies.io:8000"

r = requests.get(
    "https://target.example/page",
    impersonate="chrome",          # browser ClientHello + HTTP/2 settings
    proxies={"http": proxy, "https": proxy},
    timeout=20,
)
print(r.status_code, r.headers.get("content-type"))

あなたのIP。 動作するブラウザは通常あなたのホーム接続上にあり、curlはVPS上で動作します。ホスティングASNは公開されており、事前にスコアリングされていますので、住宅のアドレスからの同じリクエストは、読み取られる前に異なる評価を受けます。出口を交換するのは、住宅プロキシでの1行の変更です — 200以上の国で90M以上のIP、すべてのプランでHTTPとSOCKS5 — そしてそれはネットワークを除外する最速の方法です。フィンガープリントレイヤーのメカニズムはJA3/JA4 TLSフィンガープリントにあります。

住宅プロキシでネットワークを除外する

curl 403から200レスポンスへの決定木フロー:cURLとしてコピー、ヘッダーを二分、TLSを模倣、次にレンダリングまたはスクレイパーAPIを使用
4つのステップ、それぞれがテストです。ステータスが200になるとすぐに停止してください — 必要以上に登らないでください。

ステップ4:ページがクライアントではなくブラウザを必要とする場合

時には403があなたに対する判断ではなく、試みられなかったチャレンジの失敗モードであることがあります。npmjs.comにアクセスするリンクチェッカーについての公開GitHubディスカッションはそれを明確に述べています:curlは有効なチャレンジソリューションを生成できないため、リクエストは403でブロックされます。サーバーは小さなJavaScript問題を発行し、答えを待ち、実行できないものを拒否します。ヘッダーセット、フィンガープリント、IPのいずれもコードを実行する必要があるテストには合格しません。

その時点で、あなたには3つの正直な選択肢があります:実際のブラウザを駆動し、そのコストを支払う、ページ自体が呼び出すJSONエンドポイントを見つける(すでに開いているネットワークタブにしばしばあります)、または要求に応じてレンダリングするサービスにURLを渡す。QuantumProxiesのScraper APIは最後のものを行います — ブラウザグレードのTLS、住宅の出口、ページが必要とする場合のみJavaScriptレンダリング、そして1つのリクエストからmarkdown、JSONまたは生のHTMLを返します。得られるものが禁止されたページではなく空白のページである場合、それは異なる診断です:スクレイパーが空白のページを返す理由を参照してください。そして、ブロックページにCloudflare Ray IDが表示される場合は、Cloudflareエラー1020に進んでください。

よくある質問

なぜcurlは403を返すのにブラウザは返さないのですか?

curlはおおよそ3つのヘッダーを送り、クッキーやリファラーを送らず、非ブラウザのTLSフィンガープリントを送りますが、ブラウザは十数個のヘッダー、クッキージャー、Chromeのハンドシェイクを送ります。サーバーはリソースではなくリクエストを拒否しています。「cURLとしてコピー」でブラウザの正確なリクエストを再現し、1つずつヘッダーを削除してどの違いが重要かを見つけてください。

なぜwgetは成功するのにcurlは403を返すのですか?

ほとんどの場合、User-Agentです。いくつかのサーバーは、乱用後にcurlのUAを特にブラックリストに載せ、wgetのものはそのままにします — 文書化されたケースでは、サーバーがそれに基づいて分岐することを確認するVary: User-Agentレスポンスヘッダーが示され、curl -A 'Wget/1.21.2'が200を復元しました。wgetはまたAccept-EncodingConnectionをデフォルトで送信し、これも時折重要です。

curlでUser-Agentを設定するにはどうすればいいですか?

-A 'string'または同等の-H 'User-Agent: string'を使用します。省略されたMozilla/5.0よりも完全で最新のブラウザ文字列を優先してください。いくつかのサーバーは、実際のブラウザが2つのトークンのみを送信しないため、それを拒否します。セット全体が一貫性を保つように、対応するAcceptAccept-Languageの値とペアにしてください。

curlエラー22は何を意味しますか?

終了コード22は、サーバーがHTTPエラーを返すたびに-f/--failによって生成され、メッセージはステータスを引用します — 通常は403です。これは報告フラグであり、別の故障ではありません。-fを削除して、応答本文を確認してください。通常、終了コードよりもブロックをはるかに明確に説明します。

プロキシはcurlの403を修正できますか?

IPの評判や地理によって引き起こされるサブセットを修正します — スクリプトがクラウドホストで実行され、ブラウザがそうでない場合、大きなサブセットです。リファラーの欠如、クッキーの欠如、JavaScriptチャレンジは修正しません。ヘッダーを最初にテストしてください、それは何もコストがかからないので、次にネットワークレイヤーを分離するために出口IPを変更します。

ここには謎はなく、ギャップだけです:ブラウザは1つのリクエストを送り、あなたは別のリクエストを送りました。ブラウザのものをコピーし、それが壊れるまで縮小すれば、常に重要な部分を見つけることができます — 通常はヘッダー、時にはフィンガープリント、時には実際のブラウザが答える必要があるチャレンジです。

Scraper APIで任意のページを取得