如果您正在编写解析器,自动化数据收集或绕过 IP 限制 — 在 PHP 中使用代理是必不可少的。在本指南中,我们将讨论两个主要工具:内置的 cURL 和流行的库 Guzzle HTTP Client — 提供现成的代码示例,可以立即在项目中使用。
为什么在 PHP 项目中使用代理
PHP 仍然是服务器自动化中最流行的语言之一,涉及代理的任务经常出现。以下是一些主要场景,在这些场景中,代理是必不可少的:
- 解析网站和市场 — Wildberries、Ozon、Avito、AliExpress 在几次请求后会阻止 IP。代理允许在不同地址之间分配负载,避免收到 403/429 错误。
- 从地理依赖资源收集数据 — 价格、搜索引擎结果、内容可能因国家而异。具有所需地理位置的代理解决了这个问题。
- 绕过速率限制 — 许多 API 限制来自单个 IP 的请求数量。代理轮换可以绕过这些限制。
- 测试地理依赖内容 — 检查不同国家用户看到的网站外观,而无需离开办公室。
- 自动化时的匿名性 — 在对外资源进行大量请求时隐藏服务器的真实 IP。
- 竞争对手监控 — 定期收集竞争对手网站的价格、产品范围、促销信息,而不必担心被封禁。
在 PHP 中,进行 HTTP 请求时通常使用两个工具:内置的 cURL 和库 Guzzle HTTP Client。两者都支持 HTTP、HTTPS、SOCKS4 和 SOCKS5 代理 — 我们将详细讨论每一个。
选择哪种类型的代理用于 PHP
在编写代码之前,重要的是要了解哪种类型的代理适合您的任务。不同类型的代理在速度、可靠性和匿名性方面具有不同的特性。
| 代理类型 | 速度 | 匿名性 | 最适合于 |
|---|---|---|---|
| 数据中心代理 | 非常高 | 中等 | 没有严格反机器人系统的解析、API 请求 |
| 住宅代理 | 中等 | 高 | 解析受保护的网站、市场、地理数据 |
| 移动代理 | 中等 | 最高 | 具有严格反机器人保护的网站、社交网络 |
对于从 Wildberries 或 Ozon 等市场解析数据的大多数任务,最佳选择是住宅代理 — 它们的 IP 属于真实的家庭用户,使请求几乎无法与普通浏览器流量区分开。数据中心代理适用于速度比隐蔽性更重要且保护较弱的地方。
所有三种类型的代理都支持 HTTP/HTTPS 和 SOCKS5 协议,因此从代码的角度来看,设置是相同的。唯一的区别在于连接字符串。
cURL 中的代理:基本设置
cURL 扩展在 PHP 中是开箱即用的,是在服务器上执行 HTTP 请求的标准方式。连接代理使用两个主要选项:CURLOPT_PROXY 和 CURLOPT_PROXYTYPE。
无身份验证的 HTTP 代理
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://httpbin.org/ip',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
// 指定代理地址
CURLOPT_PROXY => '185.199.100.1:8080',
// 代理类型:HTTP(默认)
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP 代码: $httpCode\n";
echo $response;
在这里,CURLOPT_PROXY 接受格式为 host:port 的字符串。对于通过 HTTP 代理的 HTTPS 流量,cURL 会自动使用 CONNECT 方法 — 隧道,因此请求内容保持加密。
与代理相关的 cURL 有用选项
| 选项 | 描述 |
|---|---|
CURLOPT_PROXY |
代理服务器地址(host:port) |
CURLOPT_PROXYTYPE |
类型:CURLPROXY_HTTP、CURLPROXY_SOCKS4、CURLPROXY_SOCKS5 |
CURLOPT_PROXYUSERPWD |
以 user:password 格式的用户名和密码 |
CURLOPT_HTTPPROXYTUNNEL |
启用通过 HTTP CONNECT 的隧道 |
CURLOPT_SSL_VERIFYPEER |
验证 SSL 证书(false — 关闭) |
CURLOPT_TIMEOUT |
请求超时(秒) |
CURLOPT_CONNECTTIMEOUT |
连接到代理的超时 |
cURL 中的身份验证和 SOCKS5
大多数商业代理需要通过用户名和密码进行身份验证。此外,当需要完全匿名时,SOCKS5 是首选协议,因为它不会添加像 X-Forwarded-For 这样的头部,这可能会暴露代理的使用。
带用户名和密码的 HTTP 代理
<?php
$proxyHost = '185.199.100.1';
$proxyPort = '8080';
$proxyUser = 'your_login';
$proxyPass = 'your_password';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://httpbin.org/ip',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
// 带身份验证的代理
CURLOPT_PROXY => "$proxyHost:$proxyPort",
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
CURLOPT_PROXYUSERPWD => "$proxyUser:$proxyPass",
// HTTPS 的隧道
CURLOPT_HTTPPROXYTUNNEL => true,
// 浏览器头部以伪装
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: ru-RU,ru;q=0.9,en;q=0.8',
],
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo 'cURL 错误:' . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
cURL 中的 SOCKS5 代理
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://httpbin.org/ip',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_PROXY => '185.199.100.1:1080',
// SOCKS5 通过代理解析 DNS(推荐!)
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
// 身份验证(如果需要)
CURLOPT_PROXYUSERPWD => 'login:password',
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
💡 重要提示:SOCKS5 与 SOCKS5_HOSTNAME
使用 CURLPROXY_SOCKS5_HOSTNAME 而不是 CURLPROXY_SOCKS5。区别在于,使用 SOCKS5_HOSTNAME 时,DNS 请求也通过代理进行,这可以防止 DNS 泄漏并提高匿名性。而普通 SOCKS5 的 DNS 是在本地解析的 — 这可能会暴露您的真实 IP。
cURL 中的代理轮换
单个代理在大量请求时很快会被封锁。正确的策略是拥有一个代理池并轮换它们。以下是一个简单但有效的实现:
<?php
class ProxyRotator
{
private array $proxies;
private int $currentIndex = 0;
public function __construct(array $proxies)
{
$this->proxies = $proxies;
shuffle($this->proxies); // 随机顺序打乱
}
/**
* 获取池中的下一个代理
*/
public function getNext(): string
{
$proxy = $this->proxies[$this->currentIndex];
$this->currentIndex = ($this->currentIndex + 1) % count($this->proxies);
return $proxy;
}
/**
* 获取随机代理
*/
public function getRandom(): string
{
return $this->proxies[array_rand($this->proxies)];
}
}
// 代理列表,格式为 login:password@host:port
$proxyList = [
'user1:[email protected]:8080',
'user2:[email protected]:8080',
'user3:[email protected]:8080',
'user4:[email protected]:8080',
];
$rotator = new ProxyRotator($proxyList);
/**
* 使用自动选择代理的请求函数
*/
function fetchWithProxy(string $url, ProxyRotator $rotator): ?string
{
$proxyStr = $rotator->getNext();
// 解析代理字符串
preg_match('/^(.+):(.+)@(.+):(\d+)$/', $proxyStr, $m);
[, $user, $pass, $host, $port] = $m;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_PROXY => "$host:$port",
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
CURLOPT_PROXYUSERPWD => "$user:$pass",
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("代理 $host:$port 不可用:$error");
return null;
}
return $response;
}
// 示例:使用代理轮换解析 10 个页面
$urls = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
// ...
];
foreach ($urls as $url) {
$html = fetchWithProxy($url, $rotator);
if ($html) {
echo "获取到: " . strlen($html) . " 字节\n";
}
usleep(500000); // 请求之间暂停 0.5 秒
}
请注意 usleep(500000) — 请求之间的暂停至关重要。即使轮换代理,过于频繁的请求也可能导致因行为模式而被封锁。建议的间隔为 500 毫秒到 2 秒,具体取决于网站。
Guzzle HTTP Client 中的代理:基本设置
Guzzle 是一个强大的 PHP 库,用于 HTTP 请求,广泛用于现代 PHP 框架(如 Laravel、Symfony)。与原始 cURL 相比,它提供了更方便和可读的 API,支持异步请求、中间件和方便的错误处理。
通过 Composer 安装
composer require guzzlehttp/guzzle
无身份验证的 HTTP 代理
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
// 基本 URL(可选)
'base_uri' => 'https://httpbin.org',
// 所有请求的默认设置
'timeout' => 30,
'connect_timeout' => 10,
// 客户端所有请求的代理
'proxy' => 'http://185.199.100.1:8080',
]);
$response = $client->get('/ip');
echo $response->getStatusCode() . "\n"; // 200
echo $response->getBody() . "\n"; // {"origin": "185.199.100.1"}
在 Guzzle 中,代理通过 proxy 选项以 URL 格式指定。这比在 cURL 中更直观。代理可以在客户端级别(适用于所有请求)或单个请求级别指定。
带身份验证的 HTTP 代理
<?php
use GuzzleHttp\Client;
$login = 'your_login';
$password = 'your_password';
$host = '185.199.100.1';
$port = '8080';
$client = new Client([
'timeout' => 30,
]);
// 带身份验证的代理以 URL 格式传递
$response = $client->get('https://httpbin.org/ip', [
'proxy' => "http://$login:$password@$host:$port",
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9',
],
]);
echo $response->getBody();
Guzzle 中的代理 URL 格式:scheme://user:password@host:port。对于 HTTP 代理使用 http://,对于 SOCKS5 使用 socks5://。
Guzzle 的高级用法:超时、头部、SOCKS5
让我们考虑更复杂的场景:HTTP 和 HTTPS 使用不同的代理、SOCKS5、禁用 SSL 验证和设置头部以模拟浏览器。
HTTP 和 HTTPS 使用不同的代理
<?php
use GuzzleHttp\Client;
$client = new Client([
'timeout' => 30,
'proxy' => [
// HTTP 请求的代理
'http' => 'http://login:[email protected]:8080',
// HTTPS 请求的代理
'https' => 'http://login:[email protected]:8080',
// 排除项 — 这些主机不使用代理
'no' => ['localhost', '127.0.0.1', '.internal.corp'],
],
]);
Guzzle 中的 SOCKS5
<?php
use GuzzleHttp\Client;
// Guzzle 中的 SOCKS5 需要额外的包
// composer require clue/socks-react(或使用 curl handler)
$client = new Client([
'timeout' => 30,
'proxy' => 'socks5://login:[email protected]:1080',
'curl' => [
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
],
]);
$response = $client->get('https://httpbin.org/ip');
echo $response->getBody();
带浏览器头部的完整配置
<?php
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
$client = new Client([
'timeout' => 30,
'connect_timeout' => 10,
'verify' => false, // 禁用 SSL 验证(用于调试)
'allow_redirects' => [
'max' => 5,
'strict' => false,
'referer' => true,
'protocols' => ['http', 'https'],
],
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language' => 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding' => 'gzip, deflate, br',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
],
]);
$response = $client->get('https://example.com/catalog', [
'proxy' => 'http://login:[email protected]:8080',
]);
$statusCode = $response->getStatusCode();
$body = (string) $response->getBody();
echo "状态:$statusCode, 大小:" . strlen($body) . " 字节\n";
⚠️ 关于 verify => false
禁用 SSL 验证('verify' => false)仅在调试或与内部服务交互时是可以接受的。在生产代码中,始终保持 SSL 验证开启,否则您将面临中间人攻击的风险。
Guzzle 中的代理轮换
在 Guzzle 中,轮换代理可以通过中间件方便地实现 — 这是一个拦截每个请求并为其添加代理的中间层。这比在每次调用中手动传递代理更优雅。
<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
/**
* 自动轮换代理的中间件
*/
function proxyRotationMiddleware(array $proxies): callable
{
$index = 0;
$total = count($proxies);
return Middleware::mapRequest(
function (RequestInterface $request) use ($proxies, &$index, $total) {
$proxy = $proxies[$index % $total];
$index++;
// 将代理添加到请求属性中
// (通过选项传递给处理程序)
return $request->withHeader('X-Selected-Proxy', $proxy);
}
);
}
// 代理列表
$proxies = [
'http://user1:[email protected]:8080',
'http://user2:[email protected]:8080',
'http://user3:[email protected]:8080',
];
// 简单的方式:Guzzle 的包装类,带轮换功能
class GuzzleWithRotation
{
private Client $client;
private array $proxies;
private int $index = 0;
public function __construct(array $proxies, array $clientConfig = [])
{
$this->proxies = $proxies;
$this->client = new Client($clientConfig);
}
public function get(string $url, array $options = []): \GuzzleHttp\Psr7\Response
{
$options['proxy'] = $this->proxies[$this->index % count($this->proxies)];
$this->index++;
return $this->client->get($url, $options);
}
public function post(string $url, array $options = []): \GuzzleHttp\Psr7\Response
{
$options['proxy'] = $this->proxies[$this->index % count($this->proxies)];
$this->index++;
return $this->client->post($url, $options);
}
}
// 使用示例
$guzzle = new GuzzleWithRotation($proxies, [
'timeout' => 30,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
],
]);
$urls = [
'https://example.com/product/1',
'https://example.com/product/2',
'https://example.com/product/3',
];
foreach ($urls as $url) {
try {
$response = $guzzle->get($url);
echo "成功 [{$response->getStatusCode()}]: $url\n";
sleep(1);
} catch (\Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
}
错误处理和调试
在使用代理时,错误是不可避免的:代理可能不可用、响应缓慢,或者目标网站可能返回错误。重要的是要妥善处理所有这些情况。
cURL 中的错误处理
<?php
function fetchWithErrorHandling(string $url, string $proxy): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_PROXY => $proxy,
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
curl_close($ch);
// cURL 的代理错误代码
$proxyErrors = [
CURLE_COULDNT_CONNECT => '无法连接到代理',
CURLE_OPERATION_TIMEDOUT => '连接代理超时',
CURLE_RECV_ERROR => '接收数据时出错',
CURLE_SSL_CONNECT_ERROR => '通过代理的 SSL 错误',
];
if ($errno) {
$message = $proxyErrors[$errno] ?? "cURL 错误 #$errno: $error";
return ['success' => false, 'error' => $message, 'code' => $errno];
}
if ($httpCode >= 400) {
return ['success' => false, 'error' => "HTTP $httpCode", 'code' => $httpCode];
}
return [
'success' => true,
'body' => $body,
'code' => $httpCode,
'time' => round($totalTime, 3),
];
}
// 使用带重试的函数
function fetchWithRetry(string $url, array $proxies, int $maxRetries = 3): ?string
{
foreach ($proxies as $proxy) {
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$result = fetchWithErrorHandling($url, $proxy);
if ($result['success']) {
echo "通过 $proxy 成功(尝试 $attempt,{$result['time']}秒)\n";
return $result['body'];
}
echo "通过 $proxy 错误:{$result['error']}\n";
if ($attempt < $maxRetries) {
sleep(2); // 重试前暂停
}
}
}
return null; // 所有代理均未成功
}
Guzzle 中的错误处理
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\ClientException;
$client = new Client(['timeout' => 30]);
$proxies = [
'http://user1:[email protected]:8080',
'http://user2:[email protected]:8080',
];
function fetchGuzzleWithFallback(Client $client, string $url, array $proxies): ?string
{
foreach ($proxies as $proxy) {
try {
$response = $client->get($url, [
'proxy' => $proxy,
'timeout' => 20,
]);
return (string) $response->getBody();
} catch (ConnectException $e) {
// 代理不可用或连接超时
echo "代理不可用 ($proxy): " . $e->getMessage() . "\n";
} catch (ClientException $e) {
// HTTP 4xx — 客户端错误(403、404、429)
$code = $e->getResponse()->getStatusCode();
echo "HTTP $code 对于 $url 通过 $proxy\n";
if ($code === 429) {
echo "速率限制 — 暂停 5 秒\n";
sleep(5);
}
} catch (ServerException $e) {
// HTTP 5xx — 服务器错误
echo "服务器错误: " . $e->getResponse()->getStatusCode() . "\n";
} catch (RequestException $e) {
// 其他请求错误
echo "请求错误: " . $e->getMessage() . "\n";
}
}
return null;
}
cURL 与 Guzzle:选择哪个
这两种工具都能很好地与代理配合使用,但各自有不同的优势。以下是按关键标准的比较:
| 标准 | cURL | Guzzle |
|---|---|---|
| 安装 | ✅ 内置于 PHP | ⚠️ 需要 Composer |
| 代码可读性 | ⚠️ 选项多,冗长 | ✅ 干净、方便的 API |
| 性能 | ✅ 稍快(没有包装) | ✅ 可比,有异步 |
| 异步请求 | ⚠️ curl_multi(复杂) | ✅ 内置支持 |
| 中间件 / 钩子 | ❌ 没有 | ✅ HandlerStack、中间件 |
| SOCKS5 支持 | ✅ 原生支持 | ✅ 通过 curl handler |
| 错误处理 | ⚠️ 手动错误代码 | ✅ 带类型的异常 |
| 在 Laravel/Symfony 中的集成 | ⚠️ 手动集成 | ✅ 原生支持 |
| 适合于 | 简单脚本,无依赖 | 基于框架的项目,复杂逻辑 |
推荐: 如果您正在编写简单的脚本或在没有 Composer 的主机上工作 — 使用 cURL。对于 Laravel、Symfony 或任何现代 PHP 应用程序中的项目 — Guzzle 将更加方便,并提供更多扩展的可能性。
🚀 启动前的快速检查清单
- ✅ 检查代理是否工作:
curl -x http://login:pass@host:port https://httpbin.org/ip - ✅ 设置超时:
timeout和connect_timeout - ✅ 添加真实的 User-Agent
- ✅ 在请求之间实现暂停(至少 500 毫秒)
- ✅ 考虑错误处理和回退到其他代理
- ✅ 对于 SOCKS5 使用 CURLPROXY_SOCKS5_HOSTNAME(而不是 SOCKS5)
- ✅ 在生产环境中不要禁用 SSL 验证
结论
在 PHP 中设置代理并不复杂,只要知道正确的选项和格式。对于基本任务,cURL 结合 CURLOPT_PROXY 和 CURLOPT_PROXYUSERPWD 就足够了。对于框架项目,Guzzle 提供了更方便的 API、类型化异常和中间件支持以进行代理轮换。
可靠工作的关键原则:使用通过代理解析 DNS 的 SOCKS5(CURLPROXY_SOCKS5_HOSTNAME),始终设置超时,在请求之间添加暂停,并实现代理轮换以处理大规模任务。错误处理和回退到其他代理是任何生产代码的必备元素。
通过遵循这些原则,您将能够有效地在 PHP 中使用代理,确保您的数据收集和自动化任务顺利进行。
```