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 对象而不是资源;选项调用保持不变。

人们盲从的选项(和一个真正的安全错误)

PHP cURL 代理选项设置与避免选项的清单,包括 SSL_VERIFYPEER 错误
六个选项承担了整个任务;右栏是 2011 年教程的复制粘贴债务。

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,而不是轮换。

通过轮换住宅代理网关发送 curl_multi 批次的 PHP 抓取管道图
工作线程保持简单,网关保持智能:轮换、粘性和地理定位都在代理 URL 中。

头信息和 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 选项也接受一个包含 httphttpsno 键的数组,当您需要按方案路由或排除内部主机时使用。

当 PHP cURL 不再足够时

cURL 获取 HTML;它不执行 JavaScript。如果响应返回一个带有空 div 的骨架页面,数据是客户端渲染的——我们的 空页面响应 指南展示了如何检测它。在加固目标上,无论 IP 多么干净,cURL 的 TLS 握手本身都会暴露您(浏览器工作但 cURL 403 的谜团)。PHP 无法合理地为每个请求运行一个无头浏览器,因此实用的逃生出口是 Scraper API:一个 cURL 调用到单个端点,渲染、浏览器指纹、轮换和解析以干净的 markdown、JSON 或 HTML 返回。

从 PHP 尝试 Scraper API

常见问题解答

如何在 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 用于对抗的页面。

在 QuantumProxies 住宅 IP 上运行您的 PHP 抓取器