このガイドでは、実際に動作する Guzzle プロキシ設定のあらゆる要素について解説します。具体的には、文字列形式および配列形式の proxy リクエストオプション、認証済みプロキシ URL、CONNECT トンネルを経由する HTTPS ターゲット、タイムアウトと証明書の検証、そしてプロキシがリクエストを拒否した際に Guzzle がスローする例外などです。 このガイドを読み終える頃には、住宅用プロキシを経由してルーティングを行い、セッション全体を通じて1つの出口IPを維持し、そのIPを検証し、リクエストが失敗した場合は新しいセッションで再試行する、再利用可能なPHPクライアントが完成しているはずです。
すべての例は、Guzzle 7およびPHP 8.1以降を対象としています。composer require guzzlehttp/guzzle:^7を使用してインストールしてください。
proxy オプション:文字列または配列
Guzzle は、docs.guzzlephp.org に記載されている proxy リクエストオプションを通じてプロキシを設定します。 これはリクエストごとに指定することも、クライアントのコンストラクタでデフォルト値として指定することもできます。最も単純な形式は文字列で、これはすべてのプロトコルに適用されます。
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'proxy' => 'http://USERNAME:PASSWORD@premium-residential.geonode.com:9000',
'timeout' => 30,
]);
$response = $client->get('https://api.ipify.org?format=json');
echo $response->getBody(), PHP_EOL;
配列形式では、URIスキーマごとに異なるプロキシを選択したり、ホストを除外したりできます。認識されるキーは、http、https、noの3つです。
<?php
$proxyUrl = 'http://USERNAME:PASSWORD@premium-residential.geonode.com:9000';
$client = new GuzzleHttp\Client([
'proxy' => [
'http' => $proxyUrl, // used for http:// targets
'https' => $proxyUrl, // used for https:// targets (CONNECT tunnel)
'no' => ['localhost', '.internal.example'], // never proxied
],
]);
すべてのリクエストを1つのプロキシ経由で送信したい場合は、文字列形式を使用します。noリストが必要な場合や、プレーンHTTPリクエストをデータセンターのプール経由で送信し、HTTPSリクエストを一般家庭回線経由で送信したい場合は、配列形式を使用します。
noリストとNO_PROXYに関する注意点
proxyオプションを設定していない場合、Guzzleは環境変数からHTTP_PROXY、HTTPS_PROXY、およびNO_PROXYを読み取ります。いずれかのオプションを設定した瞬間、除外リストを含め、この自動処理は停止します。 公式ドキュメントには、その場合は no を自分で渡す必要があることが明記されています。コンテナが内部サービス用に NO_PROXY を設定している場合、これを忘れると、それらの呼び出しが突然一般ユーザー向けプロキシを経由するようになってしまいます。
$no = getenv('NO_PROXY') ? explode(',', getenv('NO_PROXY')) : [];
$client = new GuzzleHttp\Client([
'proxy' => [
'http' => $proxyUrl,
'https' => $proxyUrl,
'no' => $no,
],
]);
Guzzleのプロキシ認証:URL内の認証情報
Guzzleには、proxy_authという個別のオプションはありません。 認証情報はプロキシのURLに「userinfo」(http://username:password@host:port)として指定され、cURLハンドラはそれらをProxy-Authorization: Basicとして送信します。これに基づいて、2つの実用的なルールが導き出されます。
まず、パスワード内のURLセーフでない部分はすべてパーセントエンコードしてください。 生のパスワード内に @、:、または / が含まれていると、URL の解析が失敗し、意味不明な 407 エラーが発生したり、誤ったホストに接続されたりします。文字列の連結ではなく、rawurlencode() を使用して URL を構築してください。
function proxyUrl(string $user, string $pass, int $port = 9000): string
{
return sprintf(
'http://%s:%s@premium-residential.geonode.com:%d',
rawurlencode($user),
rawurlencode($pass),
$port
);
}
第二に、認証情報をコード内に記述しないことです。環境変数やシークレットマネージャーから読み込み、完全なプロキシ URL をログに記録してはいけません。Guzzle の debug 出力には Proxy-Authorization ヘッダーが含まれているため、ログをどこかに送信する前にこれを削除してください。
HTTP CONNECT プロキシを経由した HTTPS ターゲット
よく誤解されがちな点として、プロキシの URL スキームはプロキシとの通信方法を規定するものであり、ターゲットが何を使用しているかを示すものではないという点があります。 https://というターゲットの場合、Guzzle(cURL経由)はプロキシに対してプレーンなHTTP接続を開き、CONNECT host:443を送信した後、そのトンネル内でターゲットとのTLSハンドシェイクを行います。 したがって、配列内の http および https キーは、通常、http:// プロキシ URL を指します。https://premium-residential.geonode.com:9000 と記述すると、cURL にプロキシホップ自体を TLS でラップするよう指示することになり、エンドポイントはこれを想定していません。
TLSはトンネル内でエンドツーエンドで実行されるため、プロキシはレスポンス本文を一切確認・変更せず、プロキシを使用しない場合と全く同じように、通常の証明書検証がターゲットサイトに適用されます。
verify と証明書の問題
verify オプションは、ターゲットに対する証明書チェックを制御します。 デフォルトの true(システム CA バンドルを使用)のままにしておいてください。PHP がバンドルを見つけられない場合(最小構成の Docker イメージではよくあること)、これを無効にするのではなく、verify で PEM ファイルを指定してください。
$client = new GuzzleHttp\Client([
'proxy' => $proxyUrl,
'verify' => '/etc/ssl/certs/ca-certificates.crt', // or true for the system default
]);
プロキシを追加した後にのみ発生したエラーを抑制するために verify => false を設定するのは誤りです。正しく設定された CONNECT トンネルでは証明書エラーは発生しないため、本当の問題は別の場所、通常は上記の https:// スキームの混同にあります。
タイムアウト:timeout および connect_timeout
Guzzle のデフォルト設定である timeout は 0 であり、これは「無期限に待機する」ことを意味します。 住宅用プロキシを経由する場合、これは不適切なデフォルト設定です。なぜなら、出口デバイスの速度が遅いと、ワーカーが無期限に停止してしまう可能性があるからです。2つの値を設定してください。connect_timeoutはプロキシへのTCP接続に制限を設けるもので、cURLハンドラーでのみサポートされています。timeoutは転送全体に秒単位の制限を設けるものです。
$client = new GuzzleHttp\Client([
'proxy' => $proxyUrl,
'connect_timeout' => 10, // seconds to reach the proxy
'timeout' => 45, // total seconds for the request
]);
住宅用エグジットはデータセンターのものよりも速度が遅いため、直接リクエストの場合よりも総タイムアウトに余裕を持たせてください。接続タイムアウトが頻繁に発生する場合は、ターゲット側の問題ではなく、ネットワーク経路の問題である可能性が高いです。
Guzzle プロキシからの例外の処理
Guzzle は型付き例外をスローします。これらはすべて GuzzleHttp\Exception\RequestException を継承しており、どの例外が発生したかを知ることで、どこで障害が発生したかがわかります。
| 例外 | 発生するタイミング | プロキシにおける典型的な原因 |
|---|
ConnectException | HTTPレスポンスが全くない | プロキシホストに到達できない、ポートが間違っている、接続タイムアウト、DNSエラー |
ClientException | 4xxレスポンス | 407(認証情報不一致)、ターゲットからの403/429 |
ServerException | 5xxレスポンス | ターゲット側のエラー、またはプロキシがターゲットに到達できなかった |
RequestException | その他すべて | ヘッダー処理後の合計タイムアウト、TLS 失敗 |
407 エラーは個別に調査する価値があります。これはターゲットではなくプロキシがリクエストを拒否しているため、認証情報を変更するまでは再試行しても解決しません。
<?php
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\RequestException;
try {
$response = $client->get('https://example.com/');
} catch (ConnectException $e) {
// Could not reach the proxy or the connect_timeout expired.
error_log('connect: ' . $e->getMessage());
} catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();
if ($status === 407) {
// Proxy rejected credentials: check username grammar and password.
throw new RuntimeException('proxy auth failed', 407, $e);
}
// 403, 404, 429 etc. come from the target site.
} catch (ServerException $e) {
// 5xx from the target (or the proxy failing to reach it).
} catch (RequestException $e) {
// Timeouts after connect, TLS errors and other transfer failures.
}
http_errors による例外処理の無効化
例外をキャッチするのではなく、ステータスコードに基づいて処理を分岐させたい場合は、http_errors => false を設定してください。そうすると、Guzzle は 407 を含むあらゆるステータスコードに対してレスポンスを返し、getStatusCode() の確認はユーザー自身で行うことになります。 ConnectExceptionは、返すレスポンスがないため、依然としてスローされます。
$response = $client->get('https://example.com/', ['http_errors' => false]);
if ($response->getStatusCode() === 407) {
// handle auth failure without an exception
}
診断:on_stats および debug
Guzzle プロキシのリクエストが遅くなったり、断続的に失敗したりする場合、ネットワーク上で何が起きたかを確認するための 2 つのオプションがあります。on_stats は、各転送後に GuzzleHttp\TransferStats オブジェクトを受け取り、合計時間や、名前解決、接続、TLS の所要時間などの cURL ハンドラの統計情報を取得します。
use GuzzleHttp\TransferStats;
$client->get('https://example.com/', [
'on_stats' => function (TransferStats $stats) {
printf("%s took %.3fs\n", $stats->getEffectiveUri(), $stats->getTransferTime());
if (!$stats->hasResponse()) {
var_dump($stats->getHandlerErrorData());
}
},
]);
debug => true を有効にすると、CURLOPT_VERBOSE が動作し、プロキシの応答行を含む CONNECT 交換が出力されます。トンネルが確立されていることを確認するために一度使用し、その後削除してください。このオプションは、認証情報を STDOUT に書き出します。 trueの代わりにfopen()ストリームを渡すと、出力がファイルにリダイレクトされます。
ポート 10000 でのスティッキーセッション
ポート 9000 では、リクエストごとに出口 IP が切り替わります。複数ステップのフロー(ログイン、ページの取得、ページネーション)では、同じ IP が必要になりますが、ポート 10000 ではこれが実現されます。 セッションはヘッダーではなくユーザー名によって選択されます。-session-XXXXXXXXの末尾に8文字の英数字IDを付加し、必要に応じて180から86400までの範囲で-lifetime-SECONDSを追加してください。 -country-us などのターゲット指定の接尾辞は、同じ文字列内に記述します。
<?php
function stickyProxyUrl(string $baseUser, string $pass, string $country = 'us', int $lifetime = 600): string
{
$session = substr(bin2hex(random_bytes(4)), 0, 8); // 8 alphanumeric chars
$user = sprintf('%s-country-%s-session-%s-lifetime-%d', $baseUser, $country, $session, $lifetime);
return sprintf(
'http://%s:%s@premium-residential.geonode.com:10000',
rawurlencode($user),
rawurlencode($pass)
);
}
$client = new GuzzleHttp\Client([
'proxy' => stickyProxyUrl(getenv('GEONODE_USER'), getenv('GEONODE_PASS')),
'timeout' => 45,
]);
$client からのすべてのリクエストは、10 分間、1 つの出口 IP を共有するようになります。別の IP を使用したい場合は、新しいクライアントを作成するか(または新しい proxy オプションを指定してください)。
出口IPの確認
セッションを信頼する前に、ターゲット側にどのように表示されるかを確認してください。同じクライアント経由でIPエコーサービスに2回アクセスしてみてください。スティッキーセッションの場合、両方の呼び出しで同じアドレスが返されますが、ポート9000を使用すると異なるアドレスが返されます。
$first = json_decode((string) $client->get('https://api.ipify.org?format=json')->getBody(), true);
$second = json_decode((string) $client->get('https://api.ipify.org?format=json')->getBody(), true);
printf("exit IP: %s (stable: %s)\n", $first['ip'], $first['ip'] === $second['ip'] ? 'yes' : 'no');
セッションをローテーションさせる小さなリトライヘルパー
住宅用回線はセッションの途中で接続が切断されることがあり、またターゲット側が特定のIPアドレスをブロックすることもあります。 実用的なアプローチとして、失敗したリクエストを新しいセッションIDで再試行する方法があります。これにより、出口IPが変更されるだけでなく、すでに異常な状態にある接続の再利用も回避できます。以下のヘルパーは、試行ごとにクライアントを再構築し、接続失敗、タイムアウト、およびブロックを示唆するステータスコードの場合にのみ再試行を行います。
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
function fetchWithNewSession(string $url, int $attempts = 3): ResponseInterface
{
$lastError = null;
for ($i = 1; $i <= $attempts; $i++) {
$client = new Client([
'proxy' => stickyProxyUrl(getenv('GEONODE_USER'), getenv('GEONODE_PASS')),
'connect_timeout' => 10,
'timeout' => 45,
'http_errors' => false,
]);
try {
$response = $client->get($url);
$status = $response->getStatusCode();
if ($status === 407) {
throw new RuntimeException('proxy credentials rejected'); // do not retry
}
if (!in_array($status, [403, 429, 502, 503, 504], true)) {
return $response;
}
$lastError = new RuntimeException("status $status");
} catch (ConnectException | RequestException $e) {
$lastError = $e;
}
usleep(500_000 * $i); // simple linear backoff
}
throw new RuntimeException("gave up after $attempts attempts", 0, $lastError);
}
セッションサフィックスを削除すれば、ポート9000でも同じコードが動作します。いずれにせよ、試行ごとに新しいIPが割り当てられるためです。試行回数は少なく抑えてください。3回連続で403を返すターゲットは、プロキシの問題ではなく、リクエストのフィンガープリントに関する何らかの情報を示している可能性があります。
よくある間違い
プロキシ URL で https:// 形式を使用する
プロキシホップはプレーン HTTP であり、ターゲットの HTTPS は CONNECT トンネル内で実行されます。両方の配列キーには、http:// 形式のプロキシ URL を指定する必要があります。 一般的な症状として、プロキシホストとのSSLハンドシェイクが記載されたConnectExceptionエラーが発生します。
proxyを設定した後、noリストを記述し忘れる
proxyを指定すると、NO_PROXYは無視されます。HTTP経由でアクセスされる内部ヘルスチェック、メタデータエンドポイント、およびデータベースは、一般ユーザー向けプロキシを経由して失敗するか、さらに悪い場合には情報が漏洩する可能性があります。常にnoを明示的に指定してください。
timeoutをゼロのままにする
Guzzleはデフォルトで無期限に待機します。1つの住宅用接続が停滞するだけで、スーパーバイザーがそれを強制終了するまでキューワーカーが拘束されてしまいます。プロキシ経由のすべてのクライアントで、connect_timeoutとtimeoutの両方を設定してください。
407エラーの再試行
407エラーは、プロキシが認証情報を拒否したことを意味します。同じユーザー名とパスワードで再試行しても時間の無駄です。速やかに失敗とみなして、ユーザー名(パスワードは絶対に記録しない)をログに記録し、構文サフィックスを確認してください。
次の手順
上記の設定は、Geonode レジデンシャルプロキシ(195カ国以上で250万のIPアドレスがオンライン、HTTP、HTTPS対応)および同一ホスト上のSOCKS5、さらにユーザー名のみで最大24時間のスティッキーセッションが選択可能な環境でも、変更なしで動作します。 国、州、都市、ASNのターゲティングには、ここに示したのと同じ構文を使用するため、出口ロケーションを絞り込む際にコードの変更は必要ありません。プランは契約不要のpay-as-you-goで、月額7.92ドルから10 GBが利用可能、3日間のトライアルは5ドルです。 詳細については住宅用プロキシをご覧いただくか、料金ページで各プランを比較してください。 プロキシの設定以外のスクレイピング手法については、ウェブスクレイピングのユースケースでパイプラインの残りの部分について解説しています。
よくある質問
1つのクライアントからのすべてのリクエストに対してGuzzleプロキシを設定するにはどうすればよいですか?
リクエストごとに設定するのではなく、クライアントのコンストラクタでプロキシオプションを渡してください。new Client(['proxy' => 'http://user:pass@host:port'])は、そのクライアントが送信するすべてのリクエストにプロキシを適用し、リクエストレベルのプロキシオプションが指定されている場合は、それが優先されます。これは、ワーカーからのすべてのトラフィックを同じプロバイダー経由で送信させる場合の標準的なパターンです。
通常、配列形式のhttpsキーが欠落しているか、https://スキームを指していることが原因です。 GuzzleはCONNECTを使用してHTTPS先をHTTPプロキシ経由でトンネルするため、httpsキーのプロキシURLは依然としてhttp://で始まる必要があります。配列形式を確認し、プロキシポートがCONNECTを受け入れることを確認してください(GeonodeポートはCONNECTを受け入れます)。
Guzzle において「407 Proxy Authentication Required」とは何を意味しますか?
プロキシが、データを転送する前に認証情報を拒否しました。Guzzle は、これが 4xx レスポンスであるため、ステータス 407 として ClientException を発生させます。 ユーザー名の構文、パスワード、およびパスワードに含まれる特殊文字がURL内でパーセントエンコードされているかを確認してください。http_errorsをfalseに設定すると、例外ではなく407レスポンスオブジェクトが返されます。
GuzzleはHTTP_PROXYおよびNO_PROXY環境変数を認識しますか?
はい。プロキシオプションを指定しない場合、Guzzleは環境変数からHTTP_PROXY、HTTPS_PROXY、およびNO_PROXYを読み取ります。 自分でプロキシオプションを指定した瞬間、NO_PROXYによる自動処理は停止するため、noキー(通常はexplode(',', getenv('NO_PROXY')))を明示的に指定する必要があります。
複数のGuzzleリクエストで同じ出口IPを維持することはできますか?
はい、スティッキーセッションを使用してください。Guzzleプロキシをポート10000に設定し、ユーザー名に-session-XXXXXXXX(8文字の英数字)と、オプションの-lifetime-SECONDSサフィックスを追加してください。 そのユーザー名を再利用するすべてのリクエストは、180秒から24時間の間に設定した有効期間中、1つの出口IPを共有します。
Guzzleプロキシを介してスクレイピングを行う際、考慮すべき制限はありますか?
一般家庭向け帯域幅はギガバイト単位で課金されるため、大容量のHTMLページや画像はデータ使用量としてカウントされます。スティッキーセッションの有効期間は最大24時間ですが、上流のデバイスが切断された場合はそれより早く終了する可能性があるため、コードでは出口IPの変更に対応できるようにする必要があります。また、対象サイトの利用規約、ロボットポリシー、レート制限を遵守してください。プロキシを使用しても、法的義務が免除されるわけではありません。