PHP curl 发起 GET 请求最简版本是什么样?
PHP curl 发起 GET 请求最简版本只需要三步:curl_init 初始化、curl_setopt 设置参数、curl_exec 执行请求,加上必要的错误检查和资源释放。
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com/api/data');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Curl error: ' . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);这段代码可以跑起来,但只适合调试和调用完全开放的公共 API。真正的数据采集场景,这段代码在生产环境活不过一天——原因不在 curl 本身,而在缺失的三样东西:合理的 headers、代理 IP、错误处理与重试。
生产环境最小可用版本至少要加上 headers 设置、代理配置、超时控制、SSL 校验开关、跟随重定向选项。下文分五个模块拆解。
GET 请求必须带的 headers 有哪些?
GET 请求必须带的 headers 至少包括:User-Agent、Accept、Accept-Language、Accept-Encoding,更贴近浏览器的还需要 Referer、Connection、Sec-Fetch 系列。
对采集来说,headers 组合的一致性比单个字段的具体值更重要。一个"看起来像浏览器"的请求头组合,比只设了一个 UA 但其他字段全空的组合成功率高得多。
<?php
$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,*/*;q=0.8',
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding: gzip, deflate, br',
'Connection: keep-alive',
'Upgrade-Insecure-Requests: 1',
'Sec-Fetch-Site: same-origin',
'Sec-Fetch-Mode: navigate',
'Sec-Fetch-User: ?1',
'Sec-Fetch-Dest: document',
'Referer: https://example.com/',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com/api/data');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_ENCODING, ''); // 自动解压
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
$response = curl_exec($ch);
curl_close($ch);CURLOPT_ENCODING 传空字符串是关键——它会让 curl 主动接受 gzip、deflate、br 压缩,并自动解压;不设这一项,拿到的响应可能是一堆乱码。
curl 如何接入代理 IP?
curl 接入代理只需两行:CURLOPT_PROXY 设代理地址、CURLOPT_PROXYUSERPWD 设账密。类型上支持 HTTP、HTTPS、SOCKS4、SOCKS5,通过 CURLOPT_PROXYTYPE 指定。
HTTP 代理接入
<?php
curl_setopt($ch, CURLOPT_PROXY, 'proxy.example.com:8080');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'your_user:your_pass');SOCKS5 代理接入
<?php
curl_setopt($ch, CURLOPT_PROXY, 'proxy.example.com:1080');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'your_user:your_pass');从 API 拉取代理 IP 并轮换
<?php
function fetchProxyList($apiUrl) {
$raw = file_get_contents($apiUrl);
$lines = array_filter(array_map('trim', explode("\n", $raw)));
return array_values($lines);
}
function pickProxy(array $pool) {
return $pool[array_rand($pool)];
}
$pool = fetchProxyList('http://proxy-api.example.com/fetch?num=20');
$proxy = pickProxy($pool);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://target.example.com/api/list');
curl_setopt($ch, CURLOPT_PROXY, $proxy);
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
curl_close($ch);生产环境不建议每次请求都调 API 拉 IP——API 有调用频次限制,并且每次拉取有网络延迟。建议启动时拉一批缓存到本地(内存或 Redis),按失效反馈动态补充。
| 代理类型 | 适用场景 | 注意点 |
|---|---|---|
| HTTP 代理 | 大部分 Web 采集 | HTTPS 目标同样通过 HTTP 代理转发 |
| HTTPS 代理 | 需要代理与客户端加密通信 | 较少用,配置需匹配代理端证书 |
| SOCKS5 代理 | 非 HTTP 协议采集、部分特殊网络 | 支持 UDP 转发,通用性强 |
curl 超时、重试、错误处理怎么做?
超时、重试、错误处理是决定采集稳定性的三个基础模块,缺一个就等于把稳定性交给运气。
双超时策略
<?php
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 连接建立超时
curl_setopt($ch, CURLOPT_TIMEOUT, 15); // 整个请求超时连接超时短(3-5 秒),整体超时稍长(10-30 秒)。连接超时短是为了快速淘汰不可用的代理 IP,整体超时长是为了容忍目标站点的慢响应。
带重试的完整封装
<?php
function fetchWithRetry($url, array $pool, array $headers, $maxRetry = 3) {
for ($i = 0; $i < $maxRetry; $i++) {
$proxy = $pool[array_rand($pool)];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $proxy,
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($response !== false && $code === 200 && strlen($response) > 500) {
return ['ok' => true, 'body' => $response];
}
usleep(mt_rand(500000, 1500000)); // 0.5-1.5 秒随机延迟
}
return ['ok' => false, 'body' => null, 'last_error' => $err ?? null];
}错误处理要区分四种情况:网络层错误(curl_error 非空)、HTTP 4xx、HTTP 5xx、内容异常(200 但内容为空或结构错误)。四种情况的重试策略不同——网络错误立即换 IP 重试,HTTP 4xx 中的 403、429 换 IP 并延长冷却,5xx 稍等再试,内容异常先记录再重采。
采集乱码怎么解决?
采集乱码 99% 的情况是编码不匹配,解决顺序是:先看响应头 Content-Type 的 charset 声明,再看 HTML 里的 meta charset,最后靠 mb_detect_encoding 兜底。
<?php
function normalizeEncoding($html) {
// 优先从 Content-Type 或 meta 提取
if (preg_match('/<meta[^>]+charset=["\']?([a-zA-Z0-9\-]+)/i', $html, $m)) {
$srcCharset = strtoupper($m[1]);
} else {
$srcCharset = mb_detect_encoding($html, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
}
if ($srcCharset && $srcCharset !== 'UTF-8') {
$html = mb_convert_encoding($html, 'UTF-8', $srcCharset);
}
return $html;
}启用了 CURLOPT_ENCODING => '' 之后,gzip、deflate、br 压缩会自动解压,这一步无需手动处理。剩下要处理的是字符集(UTF-8、GBK 之间的转换)。
如果响应体本身声明的字符集与实际字节流不符(部分老站点有这种问题),mb_detect_encoding 可能判错。这时可以传入一个候选列表按顺序尝试,直到转换后无乱码为止。
UA、Referer 如何轮换?
UA 与 Referer 轮换的关键是"组合合理"——UA 与 Referer 要匹配,不能出现"移动端 UA + 桌面版 Referer"这种破绽。
UA 池分类管理
<?php
$uaPool = [
'desktop_chrome' => [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
],
'desktop_firefox' => [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
],
'mobile_safari' => [
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
],
];
function pickUa($category, array $uaPool) {
return $uaPool[$category][array_rand($uaPool[$category])];
}
function pickReferer($category) {
$map = [
'desktop_chrome' => 'https://www.google.com/',
'desktop_firefox' => 'https://www.baidu.com/',
'mobile_safari' => 'https://m.google.com/',
];
return $map[$category] ?? 'https://www.google.com/';
}对同一目标站点,建议一个采集任务内固定使用一类 UA(比如都用 desktop_chrome),不要在 desktop 与 mobile 之间频繁横跳。频繁横跳会让站点侧看到"同一 IP 一会儿桌面一会儿手机"的可疑组合。
GET 返回被降级或被限制怎么判断?
GET 返回被降级或被限制的判断有四个信号:HTTP 状态码、响应体长度、关键字段完整性、页面标题内容。四个信号交叉印证,单看任何一个都可能误判。
判断清单:
- HTTP 状态码:403、429、503 明确表示限制;301、302 跳转到验证页也是限制信号
- 响应体长度:如果历史成功响应普遍 20KB 以上,突然只有 2KB,大概率是精简版或错误页
- 关键字段:JSON 接口检查关键字段是否存在,HTML 检查页面结构标签是否完整
- 页面标题:检查
<title>是否包含"验证"、"访问受限"、"稍后再试"等提示 - 响应时间:异常快(几十毫秒)或异常慢(超过 10 秒)的响应都值得警惕,常伴随简化返回
php
<?php
function isDegraded($response, $code, $expectedFields = []) {
if ($code !== 200) return true;
if (strlen($response) < 500) return true;
if (preg_match('/(访问受限|请稍后|验证|blocked|access denied)/iu', $response)) return true;
foreach ($expectedFields as $field) {
if (strpos($response, $field) === false) return true;
}
return false;
}被判定降级的响应不应直接进入主数据库,而是走一个"待复采队列",换 IP、换 UA、隔一段时间再试。三次以上都被降级的目标,升级到人工复核队列。
FAQ
Q:PHP curl 采集为什么经常出现 SSL 证书错误?
SSL 证书错误多数是本地 CA 证书不全或过期。快速方案是设置 CURLOPT_SSL_VERIFYPEER => false 跳过校验,但只适合内部采集环境;正规做法是从 curl 官网下载最新的 cacert.pem,通过 CURLOPT_CAINFO 指定路径,这样既保证安全又不会出错。
Q:curl 采集速度慢怎么优化?
优化点从上到下:一是用 curl_multi_* 系列并发发起多个请求;二是启用 keep-alive 减少连接建立开销;三是合理设置连接超时(3-5 秒)快速淘汰慢代理;四是把 DNS 解析结果缓存起来,避免每次请求都走 DNS。并发数不建议超过 50,再高会给本机 IO 和目标站点都造成压力。
Q:curl_multi 并发采集怎么写?
curl_multi_init 建一个多任务句柄,循环 curl_multi_add_handle 加入子句柄,然后 curl_multi_exec 执行,curl_multi_select 等待。完成后 curl_multi_getcontent 取结果、curl_multi_remove_handle 清理。并发数建议控制在 20-50 之间,过高会导致本机文件描述符耗尽。
Q:采集到的 JSON 解析失败是什么原因?
大多数情况是响应内容前后有多余字符——比如 BOM 头、HTML 错误页残留、或者站点用 JSONP 包了一层 callback。解决方式是先 trim 掉两端空白与 BOM,再检查是否被 callback 包裹,必要时正则提取花括号之间的内容再 json_decode。
Q:PHP curl 支持 HTTP/2 吗?
支持,前提是编译时链接了 nghttp2 库。通过 CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0 指定。部分站点强制要求 HTTP/2 才返回完整数据,遇到"数据显著缺失"的情况可以试试切到 HTTP/2。
Q:代理 IP 生效但采集依然失败,可能是哪些原因?
按可能性从大到小:请求 headers 不完整或组合可疑、单 IP 请求间隔太短、同一 UA 反复使用、Cookie 没有 IP 级隔离、TLS 指纹被识别、目标接口需要额外的签名或时间戳参数。IP 只是入场券,行为特征才是留场券。
