先把代理挂上:cURL 的最小可用写法
PHP 里发请求,大部分老项目用的还是 cURL 扩展。挂代理的核心其实就一个参数 CURLOPT_PROXY,其他都是围绕它做加固。先看一段能直接跑的最小版本:
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://example.com/data.html',
CURLOPT_PROXY => 'http://123.45.67.89:8080',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$html = curl_exec($ch);
curl_close($ch);
几个细节说一下。代理地址前面的协议头不能省,HTTP 代理写 http://,SOCKS5 代理写 socks5://,写错了或者干脆不写,请求可能莫名其妙走不通,报错还不一定指向真正的原因。超时参数两个都要设,后面坑点部分细说为什么。CURLOPT_RETURNTRANSFER 设成 true,才能拿到响应内容,否则成功时返回的是 true 而不是 HTML。
还有一个专门针对 HTTPS 目标站的参数,新手最容易在这撞墙:
curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, true);
目标站是 https 的时候,代理要用 CONNECT 方式先建一条隧道,请求再从隧道里过去。不加这行,http 的目标站一切正常,换成 https 就开始抽风,要么直接失败,要么时好时坏。把这行固定写进你的请求封装里,一劳永逸。
代理要认证?407 报错这样处理
正规付费代理一般都带用户名密码。cURL 里代理认证是独立的一组参数,别跟目标网站自己的登录搞混:
curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'username:password');
也可以偷懒,直接把账号密码拼进代理地址,效果一样:
curl_setopt($ch, CURLOPT_PROXY, 'http://username:password@123.45.67.89:8080');
如果响应里出现 407 状态码,就是认证环节出了岔子。按概率排查:密码里有特殊字符没做 urlencode;账号密码填到了目标站认证的位置上;还有一种隐蔽情况,服务商开了 IP 白名单校验,而你服务器的出口 IP 不在名单里,这时候密码填得再对也是 407。
用 Guzzle 的同学更省事,一个 proxy 选项搞定:
use GuzzleHttp\Client;
$client = new Client([
'proxy' => 'http://username:password@123.45.67.89:8080',
'timeout' => 15,
'connect_timeout' => 5,
]);
$response = $client->get('https://example.com/data.html');
echo $response->getBody();
注意密码里的 @、: 这类符号要先 urlencode,不然 Guzzle 解析地址时会切错位置。timeout 和 connect_timeout 的道理跟 cURL 一样,两个都给。
实战五个坑,一个比一个隐蔽
配置写对只是及格线。下面这几个坑,每一个我都见过有人在里面耗掉一整天。
坑一:只设总超时,不设连接超时。代理池里的 IP 质量参差,有的 IP 光建立连接就要十几秒。CURLOPT_TIMEOUT 管的是整个请求的生死,CURLOPT_CONNECTTIMEOUT 管的是握手阶段。只设前者,一个坏 IP 就能把你的任务拖到怀疑人生。连接超时给 3 到 5 秒,快速跳过坏 IP,才是正解。
坑二:证书报错就关验证。本地 Windows 环境跑 cURL,十有八九会碰到 SSL certificate problem。图省事的做法是把 CURLOPT_SSL_VERIFYPEER 和 CURLOPT_SSL_VERIFYHOST 关掉,能跑,但等于让数据在代理链路上裸奔。正确姿势是下载一份 CA 证书包,在 php.ini 里给 curl.cainfo 配上路径,两分钟的事,一劳永逸。
坑三:IP 有寿命,不做重试等于白搭。短效动态代理的 IP 生命周期可能就几分钟,提取一批存进池子,跑到后面全是失效的。重试加剔除的逻辑必须有,参考这个骨架:
function fetchWithRetry($url, $proxyPool, $maxRetry = 3)
{
$pool = $proxyPool;
while ($maxRetry > 0 && count($pool) > 0) {
$maxRetry--;
$proxy = $pool[array_rand($pool)];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $proxy,
CURLOPT_HTTPPROXYTUNNEL => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$html = curl_exec($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno === 0 && $html !== false) {
return $html;
}
// 失败就把这个 IP 踢出池子,下次用新的
$pool = array_diff($pool, [$proxy]);
}
return false;
}
失败就随机换一个 IP 再来,同时把坏 IP 剔出去。池子还要有补充机制,定时从提取接口拉新 IP 进来,让整个池子流动起来。
坑四:返回 200 不代表拿到数据。目标站不会明着拒绝你,更多时候是返回一个看起来正常的验证页,状态码照样 200。所以拿到响应后要做内容校验,关键元素在不在、数据长度在不在正常区间。校验不过就按失败处理,走换新 IP 重试那套逻辑。
坑五:并发跑完内存居高不下。用 curl_multi 做并发时,每个子句柄结束后要 curl_multi_remove_handle 摘下来再 curl_close,全部跑完 curl_multi_close 收尾。漏了收尾这步,脚本跑得越久内存吃得越多,挂一夜直接被系统杀进程。
排查问题的时候,对照这张表能省不少时间:
| 现象 / 报错 | 多半是这个问题 | 处理办法 |
|---|---|---|
| CURLE 7 无法连接代理 | 地址或端口写错 | 核对提取接口返回的格式 |
| CURLE 5 代理解析失败 | 拼接时混入空格或换行 | 对 IP 字符串做 trim |
| 407 认证失败 | 密码没 urlencode 或白名单没加 | 检查认证参数和白名单 |
| CURLE 28 超时 | 连接超时太短或 IP 质量差 | 调整超时,换个新 IP 重试 |
| CURLE 60 证书问题 | 本地缺 CA 证书包 | 配置 curl.cainfo 路径 |
| 200 但内容是验证页 | 请求特征被识别 | 降频、补新 IP、补全请求头 |
IP 池从哪来:自己维护不如接个现成的
写到这里,配置和坑点都齐了,剩下一个源头问题:IP 从哪来。免费列表看着香,实际可用率低得可怜,验证失效 IP 的时间比写业务逻辑还长;自己搭代理集群,服务器和维护成本又压不下来。对正经的公开数据采集、市场研究这类项目,直接对接一家代理服务商的 API 是性价比最高的路子。
我们团队目前在用的是神龙HTTP,说两个实际体验比较深的点。一是池子够大也够干净:3000万+正规授权代理IP储备,国内三大运营商正规授权,可用率标称 99.9%,短效动态IP按生命周期自动轮换,配合上面那套重试剔除逻辑,任务跑一整晚基本不用人工干预。二是接入成本低:API 兼容主流编程语言,文档和示例代码齐全,PHP 这边就是把提取接口返回的 IP 填进 CURLOPT_PROXY,半天就能跑通,中间卡壳还能找技术支持,7×24 小时有人应。
另外提一句,它的个人中心里有可视化的用量统计和实时监控,每个时间段的请求量、成功率一眼可见,任务出异常时定位特别快,连自己搭监控面板的功夫都省了。
常见问题
Q:怎么确认请求真的从代理发出去了?
最直接的办法是让脚本请求一个回显出口 IP 的接口,把返回结果和你本机宽带的 IP 对比,不一致就说明代理生效了。也可以用 curl_getinfo($ch, CURLINFO_PRIMARY_IP) 查看实际连接的对端地址,显示的是代理的 IP 就对了。
Q:SOCKS5 代理的写法有什么不一样?
只改协议头,CURLOPT_PROXY 的值写成 socks5://IP:端口,其他参数通用。有个进阶细节:写成 socks5h:// 的话,域名解析也会放到代理端去做,目标站域名解析有讲究的场景下更稳。Guzzle 同理,proxy 值换成 socks5h:// 开头就行。
Q:提取的一批 IP 里总有几个连不上,正常吗?
正常。短效 IP 天然有生命周期,加上网络波动,任何池子都做不到百分之百即时可用。关键是你的重试和剔除逻辑要跟上,同时尽量选池子大、可用率高的服务商,比如神龙HTTP 这种 99.9% 可用率的,实际失败比例会小很多,重试一次基本就能过。
Q:请求频率怎么控制比较稳妥?
别把目标站当自己的测试环境。请求之间加随机间隔,比如 1 到 3 秒,并发数从小往大试,边跑边看返回内容的质量。配合服务商后台的监控看成功率曲线,曲线往下走就主动降速或者补充新 IP,别等被拦了才反应。





