PHP cURL 代理抓取:CURLOPT_PROXY、认证和 Guzzle
大多数在线 PHP 代理代码片段仍然存在自 2011 年以来的三个相同错误。这里是一个干净的 CURLOPT_PROXY 设置——认证、SOCKS5、curl_multi 轮换、Guzzle 和超时——在生产环境中表现良好。
PHP cURL 代理代码有一个特殊问题:排名靠前的代码片段已有十年历史,并且许多存在真正的错误——双 curl_exec() 调用导致每个请求执行两次,CURLOPT_SSL_VERIFYPEER 被禁用以“使 HTTPS 工作”,选项设置为已经是默认值的值。本指南是当前正确版本:CURLOPT_PROXY 认证、SOCKS5、curl_multi 并行轮换、Guzzle 中的相同设置,以及防止工作线程整夜挂起的超时。
一个简洁、正确的 PHP cURL 代理示例
<?php
function fetch(string $url): string|false {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_PROXY => 'http://gate.quantumproxies.io:8000',
CURLOPT_PROXYUSERPWD => 'USER:PASS',
CURLOPT_RETURNTRANSFER => true, // return the body, don't echo it
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_CONNECTTIMEOUT => 10, // seconds to reach the proxy
CURLOPT_TIMEOUT => 30, // seconds for the whole transfer
CURLOPT_ENCODING => '', // accept gzip/br, decode automatically
]);
$body = curl_exec($ch);
if ($body === false) {
error_log('cURL error ' . curl_errno($ch) . ': ' . curl_error($ch));
}
curl_close($ch);
return $body;
}
echo fetch('https://httpbin.org/ip'); // -> the proxy exit IP
凭据可以如所示存放在 CURLOPT_PROXYUSERPWD 中,或内嵌在代理 URL 中(http://USER:PASS@host:port)——两者都有效。CURLOPT_RETURNTRANSFER 是初学者常常忽略的:没有它,curl_exec() 会直接将响应输出并返回 true,这绝不是抓取器想要的。在 PHP 8 上,句柄是一个 CurlHandle 对象而不是资源;选项调用保持不变。
人们盲从的选项(和一个真正的安全错误)
CURLOPT_HTTPPROXYTUNNEL——对于 HTTPS 目标不需要。cURL 在目标为https://时自动建立 CONNECT 隧道;此选项仅强制对普通 HTTP 目标进行隧道化,而抓取几乎不需要。CURLOPT_CUSTOMREQUEST => 'GET'——GET 已经是默认值;设置它除了在边缘情况下破坏重定向处理外没有任何作用。- 调用
curl_exec()两次——经典的 Stack Overflow 错误:请求执行两次,第二个返回值覆盖第一个。每个句柄调用一次。 CURLOPT_SSL_VERIFYPEER => false——真正危险的一个。它通过允许每个请求进行中间人攻击来“修复”证书错误。根据 PHP 手册,真正的修复方法是将 php.ini 中的curl.cainfo指向最新的cacert.pem捆绑包。

PHP 中的 SOCKS5 代理
<?php
// scheme style: socks5h = DNS resolved by the proxy (use for scraping)
curl_setopt($ch, CURLOPT_PROXY, 'socks5h://USER:PASS@gate.quantumproxies.io:1080');
// split style: same result via CURLOPT_PROXYTYPE
curl_setopt($ch, CURLOPT_PROXY, 'gate.quantumproxies.io:1080');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'USER:PASS');
方案决定了 DNS 的位置:socks5://(和 CURLPROXY_SOCKS5)在您的服务器上解析主机名,将每个目标域泄露给本地解析器;socks5h://(CURLPROXY_SOCKS5_HOSTNAME)在代理出口处解析。建议选择后者。QuantumProxies 在每个计划中都提供 HTTP 和 SOCKS5,因此切换协议只需一行更改,而不是新的订阅。相同的 DNS 规则也适用于命令行——请参阅我们的 curl 代理示例 以获取此页面上所有内容的 shell 等价物。
使用 curl_multi 进行轮换和并行请求
PHP 没有 async/await,但并行抓取并不需要它——curl_multi 在单个进程中同时运行数十个传输。将每个句柄指向一个轮换网关,每个请求自动从不同的 住宅 IP 退出,代码中无需进行代理列表管理:
<?php
$urls = ['https://example.com/p/1', 'https://example.com/p/2', 'https://example.com/p/3'];
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
// one gateway, a fresh residential exit per request
CURLOPT_PROXY => 'http://USER:PASS@rotating.quantumproxies.io:8000',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_ENCODING => '',
]);
curl_multi_add_handle($mh, $ch);
$handles[$url] = $ch;
}
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh); // wait for activity instead of spinning
} while ($running > 0);
$results = [];
foreach ($handles as $url => $ch) {
$results[$url] = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
保持批次适中——8 到 16 个并发句柄对于大多数目标已经足够,节奏比原始速度更重要:在批次之间 usleep() 几百毫秒几乎不费什么成本,并保持请求速率在触发 429 的阈值以下。检查每个句柄的 curl_getinfo($ch, CURLINFO_HTTP_CODE) 并将失败路由到重试队列,而不是在线重新获取。当一个流程跨越多个请求且必须共享一个身份(登录、购物车、带有 cookie 的分页)时,通过在用户名后添加后缀将该句柄切换到粘性会话:USER-session-a1b2c3 在整个过程中保持相同的出口 IP,而不是轮换。

头信息和 cookies:看起来像您声称的浏览器
一个干净的出口 IP 加上默认的 cURL 头信息仍然会被识别为机器人——PHP 会自我宣布,除非您装饰请求。三个选项可以缩小大部分差距:使用当前浏览器字符串的 CURLOPT_USERAGENT,用于 Accept-Language 等的 CURLOPT_HTTPHEADER,以及指向同一文件的 cookie 对 CURLOPT_COOKIEJAR / CURLOPT_COOKIEFILE,这会像浏览器一样在句柄间持久化会话 cookie。cookie 持久性比人们预期的更重要:许多网站在第一次访问时设置一个令牌,并悄悄挑战那些从不返回它的客户端。保持声明的身份一致——一个没有 Accept-Language 和一个简单头信息集的 Chrome User-Agent 本身就是一个指纹,而一致性检查正是中级反机器人供应商在第二次请求时抓住 PHP 抓取器的方法。
Guzzle:相同的代理,更好的操作体验
<?php
use GuzzleHttp\Client;
$client = new Client([
'proxy' => 'http://USER:PASS@gate.quantumproxies.io:8000',
'timeout' => 30,
'connect_timeout' => 10,
]);
$res = $client->get('https://httpbin.org/ip');
echo $res->getBody();
// per-request override, e.g. a sticky session for a login flow
$res = $client->get('https://example.com/account', [
'proxy' => 'http://USER-session-a1b2c3:PASS@gate.quantumproxies.io:8000',
]);
Guzzle 底层驱动 cURL,所以以上所有内容仍然适用——它只是更易读。一个警告:Guzzle 的默认 timeout 是 0,意味着无限等待。一个在传输中途停滞的代理出口会无限期地固定一个 PHP-FPM 工作线程,因此在构建的每个客户端上设置超时。proxy 选项也接受一个包含 http、https 和 no 键的数组,当您需要按方案路由或排除内部主机时使用。
当 PHP cURL 不再足够时
cURL 获取 HTML;它不执行 JavaScript。如果响应返回一个带有空 div 的骨架页面,数据是客户端渲染的——我们的 空页面响应 指南展示了如何检测它。在加固目标上,无论 IP 多么干净,cURL 的 TLS 握手本身都会暴露您(浏览器工作但 cURL 403 的谜团)。PHP 无法合理地为每个请求运行一个无头浏览器,因此实用的逃生出口是 Scraper API:一个 cURL 调用到单个端点,渲染、浏览器指纹、轮换和解析以干净的 markdown、JSON 或 HTML 返回。
常见问题解答
如何在 PHP cURL 中使用代理?
将 CURLOPT_PROXY 设置为完整的代理 URL,如果需要凭据,添加 CURLOPT_PROXYUSERPWD,并始终启用 CURLOPT_RETURNTRANSFER 加上明确的超时。这是所有必需的设置——HTTPS 目标自动隧道化,其他都是可选调整。
如何在 PHP 中传递代理用户名和密码?
可以使用 curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'user:pass') 或将其嵌入 URL 中:http://user:pass@host:port。如果收到 HTTP 407,说明代理拒绝了这些凭据——检查是否有拼写错误并对特殊字符进行 URL 编码。使用 IP 白名单认证的提供商在您的服务器 IP 授权后无需任何凭据。
PHP cURL 可以使用 SOCKS5 代理吗?
可以。将 socks5h:// URL 传递给 CURLOPT_PROXY,或将 CURLOPT_PROXYTYPE 设置为 CURLPROXY_SOCKS5_HOSTNAME。建议使用主机名变体而不是普通 socks5,这样 DNS 会在代理出口而不是您的服务器上解析——否则每个目标域都会通过本地解析器泄露。
什么是 CURLOPT_HTTPPROXYTUNNEL,我需要它吗?
它强制 cURL 通过 CONNECT 隧道穿过代理,而不是请求代理获取 URL。对于 https:// 目标,cURL 自动隧道化,因此在几乎所有抓取代码中该选项都是多余的。仅在需要对普通 HTTP 目标进行隧道化语义时设置它。
如何为通过代理的 PHP cURL 设置超时?
同时设置:CURLOPT_CONNECTTIMEOUT 涵盖到达代理和打开隧道,CURLOPT_TIMEOUT 限制整个传输。毫秒变体(_MS)存在于更严格的预算中。没有它们,停滞的出口会保持请求——在 PHP-FPM 下,整个工作线程——直到 Web 服务器自身的限制将其终止。
复制第一个代码片段,牢记清单,PHP 是一个非常稳固的抓取语言:curl_multi 用于并行处理,轮换网关用于身份,Guzzle 用于操作体验,Scraper API 用于对抗的页面。