# 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
<?php
function fetch(string $url): string|false {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_PROXY          => 'http://residentialpremium.quantumproxies.io:8080',
        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 curl_setopt_array，包含需要设置的六个选项：CURLOPT_PROXY、PROXYUSERPWD、RETURNTRANSFER、CONNECTTIMEOUT 10、TIMEOUT 30 和 ENCODING，以及应当删除的复制粘贴遗留：HTTPPROXYTUNNEL、CUSTOMREQUEST GET、SSL_VERIFYPEER false 和重复的 curl_exec。卡片建议改用 curl.cainfo 解决证书问题。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335147/blog/generated/qp-b9-008-diag1.png)
*六个选项承担了整个任务；右栏是 2011 年教程的复制粘贴债务。*

## PHP 中的 SOCKS5 代理

```php
<?php
// scheme style: socks5h = DNS resolved by the proxy (use for scraping)
curl_setopt($ch, CURLOPT_PROXY, 'socks5h://USER:PASS@residentialpremium.quantumproxies.io:1080');

// split style: same result via CURLOPT_PROXYTYPE
curl_setopt($ch, CURLOPT_PROXY, 'residentialpremium.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](https://quantumproxies.io/socks5-proxies)，因此切换协议只需一行更改，而不是新的订阅。相同的 DNS 规则也适用于命令行——请参阅我们的 [curl 代理示例](https://quantumproxies.io/blog/curl-with-proxy-examples) 以获取此页面上所有内容的 shell 等价物。

## 使用 curl_multi 进行轮换和并行请求

PHP 没有 async/await，但并行抓取并不需要它——`curl_multi` 在单个进程中同时运行数十个传输。将每个句柄指向一个轮换网关，每个请求自动从不同的 [住宅 IP](https://quantumproxies.io/residential-proxies) 退出，代码中无需进行代理列表管理：

```php
<?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@residentialpremium.quantumproxies.io:8080',
        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-sid-a1b2c3-ttl-30m` 在整个过程中保持相同的出口 IP，而不是轮换。

![一个 PHP worker 通过同一个代理 URL 发送每批 8 到 16 个句柄的 curl_multi 批次，由网关决定出口：轮换的新住宅 IP、保持同一出口的粘性会话，或地理定向的出口。卡片说明：轮换、USER-session-a1b2c3 这样的粘性用户名和地理定向都写在代理 URL 中。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335147/blog/generated/qp-b9-008-diag2.png)
*工作线程保持简单，网关保持智能：轮换、粘性和地理定位都在代理 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
<?php
use GuzzleHttp\Client;

$client = new Client([
    'proxy'           => 'http://USER:PASS@residentialpremium.quantumproxies.io:8080',
    '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-sid-a1b2c3-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080',
]);
```

Guzzle 底层驱动 cURL，所以以上所有内容仍然适用——它只是更易读。一个警告：Guzzle 的默认 `timeout` 是 0，意味着无限等待。一个在传输中途停滞的代理出口会无限期地固定一个 PHP-FPM 工作线程，因此在构建的每个客户端上设置超时。`proxy` 选项也接受一个包含 `http`、`https` 和 `no` 键的数组，当您需要按方案路由或排除内部主机时使用。

## 当 PHP cURL 不再足够时

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

[从 PHP 尝试 Scraper API](https://quantumproxies.io/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 抓取器](https://quantumproxies.io/residential-proxies)
