前阵子帮朋友做个小工具需要定期把几个资讯站点的文章列表抓下来做汇总。本来想直接上 Python 的 Scrapy但项目本身就是 PHP 的服务器上也没装 Python 环境折腾一圈不如用 PHP 自己写。说实话PHP 写爬虫没想象中那么难核心就那么几个函数真正麻烦的是反爬和各种边界情况。这篇就把我踩过的坑整理一下代码都是能直接跑的。一、先明确一下要抓什么爬虫这东西最忌讳上来就写代码。我一般习惯先打开目标页面F12 看看结构确认三件事列表页的 URL 规律比如是/list?page1还是/list/1.html每条数据在 DOM 里的位置用哪个 class 或标签能稳定定位详情页需不需要二次请求如果列表页已经包含摘要很多时候可以省掉这一步。这次我选了一个结构比较规整的资讯站做示例列表项大致长这样div classarticle-list div classitem h3a href/post/1024.html文章标题/a/h3 span classtime2026-10-20/span /div /div二、用 cURL 把页面拿下来PHP 抓网页有几种方式file_get_contents最简单但它对超时、Header、Cookie 的控制很弱遇到需要伪装 UA 的站点基本没戏。所以我一般直接用 cURL虽然代码多几行但可控性强太多。?php function fetchPage(string $url): ?string { $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL $url, CURLOPT_RETURNTRANSFER true, CURLOPT_FOLLOWLOCATION true, CURLOPT_MAXREDIRS 3, CURLOPT_TIMEOUT 10, CURLOPT_CONNECTTIMEOUT 5, CURLOPT_ENCODING gzip,deflate, CURLOPT_USERAGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, CURLOPT_HTTPHEADER [ Accept: text/html,application/xhtmlxml, Accept-Language: zh-CN,zh;q0.9, ], ]); $html curl_exec($ch); $code curl_getinfo($ch, CURLINFO_HTTP_CODE); $err curl_error($ch); curl_close($ch); if ($html false || $code 400) { error_log(抓取失败: {$url} code{$code} err{$err}); return null; } return $html; }这里有几个细节值得说一下。CURLOPT_ENCODING设成 gzip 是让服务器压缩传输能省不少流量FOLLOWLOCATION限制在 3 次以内防止被重定向到奇怪的地址超时一定要设不然一个卡死的请求能把整个脚本拖住。三、解析 HTML别用正则硬怼我见过不少人用正则去匹配div classitem.*?/div页面结构稍微一变就全崩。PHP 自带的 DOMDocument 配合 DOMXPath 才是正路虽然它对不规范 HTML 的容错一般但处理正常页面完全够用。function parseList(string $html): array { $items []; if (trim($html) ) { return $items; } $dom new DOMDocument(); libxml_use_internal_errors(true); // 加前缀避免中文乱码 $dom-loadHTML(?xml encodingUTF-8 . $html); libxml_clear_errors(); $xpath new DOMXPath($dom); $nodes $xpath-query(//div[classarticle-list]/div[classitem]); foreach ($nodes as $node) { $a $xpath-query(.//h3/a, $node)-item(0); $t $xpath-query(.//span[classtime], $node)-item(0); if (!$a) { continue; } $items[] [ title trim($a-nodeValue), url https://example.com . $a-getAttribute(href), time $t ? trim($t-nodeValue) : , ]; } return $items; }注意libxml_use_internal_errors(true)这行不加的话页面里有个没闭合的标签就会刷一屏警告。另外loadHTML默认按 ISO-8859-1 解析中文站点不加编码声明很容易变乱码加个 XML 声明是最省事的办法。四、把主流程串起来单页抓取验证没问题后就可以加翻页和入库了。翻页建议加个随机间隔别一秒钟发十个请求那基本等于自投罗网。$baseUrl https://example.com/list?page; $all []; for ($page 1; $page 5; $page) { $html fetchPage($baseUrl . $page); if ($html null) { break; } $list parseList($html); if (empty($list)) { break; // 没有数据了提前结束 } $all array_merge($all, $list); // 随机休眠 1~3 秒 usleep(random_int(1000000, 3000000)); } file_put_contents(result.json, json_encode($all, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); echo 共抓取 . count($all) . 条\n;这里用usleep而不是sleep是因为它支持微秒能做出更自然的间隔。另外判断empty($list)就 break比死循环到最大页数要稳妥很多站点翻到最后一页会返回空列表或者直接 404。五、反爬这块说点实在的写爬虫绕不开反爬但我想先说一句抓取前一定看下目标站点的 robots.txt 和服务条款别抓人家明确禁止的内容也别把频率拉满影响别人正常服务。技术上常见的几道坎User-Agent 校验最基础的一层默认的 curl UA 一眼就被识别。上面代码里已经伪装了但别用太老的版本号。频率限制同一 IP 短时间请求过多会被封加随机延时是最有效的办法必要时配合代理池轮换。Cookie 与登录态有些站点需要先访问首页拿 Cookie可以用CURLOPT_COOKIEJAR和CURLOPT_COOKIEFILE保存复用。JS 渲染如果列表是前端异步加载的DOMDocument 拿到的就是空壳。这种情况要么找它的接口直接请求 JSON要么上无头浏览器PHP 这边可以用 Panther 之类的库。验证码碰到这个就别硬刚了说明对方不欢迎自动化访问换个数据源更省心。还有个小技巧请求头里的Referer也补上很多站点会校验来源页加上之后成功率会明显高一些。六、几个容易忽略的坑最后补充几点实战经验。一是编码问题有些站点是 GBK抓下来要先mb_convert_encoding($html, UTF-8, GBK)再解析不然中文全是问号。二是相对链接href可能是/post/1.html这种拼 URL 时要注意别拼出双斜杠。三是数据去重翻页过程中列表可能更新入库前按 URL 做一次唯一性判断比较保险。整体来说PHP 写个中小规模的采集脚本完全够用代码量也不大。真正花时间的不是写代码而是摸清目标站点的结构、调试各种异常返回。建议先用一页数据把解析逻辑跑通再逐步加翻页、重试和存储别一上来就写个大而全的框架那样调试起来反而更痛苦。