cURL multi 这套骨架方向是对的,但帖子到第三步的循环体之前就断了——补三个地方它才能真正跑起来:multi 的调度循环、PHP 7.4/8.x 的句柄映射兼容、失败请求的收尾重试。

调度循环必须写成「先 exec 再 select,循环到 running 归零」,只在外面 select 一次会卡死:

do {
    $status = curl_multi_exec($mh, $running);
    if ($running) curl_multi_select($mh, 1.0);
} while ($running && $status == CURLM_OK);
// 之后 while ($info = curl_multi_info_read($mh)) 收割结果

句柄映射是最大的坑:spl_object_id() 只接受对象,PHP 7.4 里 curl_init() 返回的是 resource,直接传会 fatal。写成 $key = is_object($ch) ? spl_object_id($ch) : (int)$ch; 才能两端通杀。

另外 array_chunk 分批其实是串行——一批跑完才开下一批,白白丢掉 IO 重叠。正确做法是一次性把所有句柄挂上去,用 curl_multi_setopt($mh, CURLMOPT_MAX_TOTAL_CONNECTIONS, 8) 控制并发数,让 cURL 自己排队。

解析环节两个必做:libxml_use_internal_errors(true),否则 HTML5 标签会让 loadHTML 直接抛一堆 warning;编码先用 meta charset 判断再 mb_convert_encoding($html, 'UTF-8', $charset) 统一转码,再喂给 DOMXPath,否则中文全变问号。

坑提醒:multi 模式下 CURLOPT_TIMEOUT 仍然生效,但重试要在 curl_multi_info_read 拿到失败项后重新入队,别指望 cURL 自动重试。顺手加个 usleep 控速,别把人家站打挂。如果只是给自己的站做采集同步,这段逻辑包成插件用 Cron::register 挂到计划任务里跑更稳,不用单开常驻进程。