PHP 單線程模型導致串行抓取效率極低:100 個 URL × 200ms = 20 秒。curl_multi 利用非阻塞 I/O 多路複用,在單進程內實現「並發」網絡請求,同樣 100 個 URL 可在 ~200ms 內完成(受限於帶寬與目標服務器)。
| 模式 | 特點 | 適用場景 |
|---|---|---|
直接寫文件CURLOPT_FILE | 內存佔用極低,流式寫入磁盤 無法獲取 HTTP 狀態碼、響應頭 難以做錯誤重試、內容預處理 | 超大文件下載、日誌歸檔、不需解析內容 |
內存緩衝CURLOPT_RETURNTRANSFER | 可獲取完整響應(體、頭、狀態碼) 支援後續解析、篩選、重試 佔用內存 = 所有響應體大小之和 | API 聚合、頁面解析、數據清洗、中等體積文件 |
<?php
$urls = [
'https://www.sina.com.cn/',
'https://www.sohu.com/',
'https://www.163.com/'
];
$saveTo = '/tmp/fetch_results.txt';
$fp = fopen($saveTo, 'a');
$mh = curl_multi_init();
foreach ($urls as $i => $url) {
$conn[$i] = curl_init($url);
curl_setopt_array($conn[$i], [
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHP Bot)',
CURLOPT_HEADER => 0,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_FILE => $fp, // 直接寫入文件句柄
CURLOPT_FOLLOWLOCATION => true,
]);
curl_multi_add_handle($mh, $conn[$i]);
}
// 事件循環
$active = null;
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
while ($active && $mrc === CURLM_OK) {
if (curl_multi_select($mh, 0.5) !== -1) {
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
}
}
// 清理
foreach ($urls as $i => $url) {
curl_multi_remove_handle($mh, $conn[$i]);
curl_close($conn[$i]);
}
curl_multi_close($mh);
fclose($fp);
?>
<?php
$urls = [ /* ... */ ];
$mh = curl_multi_init();
$connections = [];
foreach ($urls as $i => $url) {
$conn = curl_init($url);
curl_setopt_array($conn, [
CURLOPT_USERAGENT => 'Mozilla/5.0',
CURLOPT_HEADER => 0,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_RETURNTRANSFER => true, // 關鍵:返回字符串而非輸出
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false, // 僅測試環境
]);
curl_multi_add_handle($mh, $conn);
$connections[$i] = $conn;
}
// 執行循環(同上)
$active = null;
do { $mrc = curl_multi_exec($mh, $active); }
while ($mrc === CURLM_CALL_MULTI_PERFORM);
while ($active && $mrc === CURLM_OK) {
if (curl_multi_select($mh, 0.5) !== -1) {
do { $mrc = curl_multi_exec($mh, $active); }
while ($mrc === CURLM_CALL_MULTI_PERFORM);
}
}
// 獲取結果並處理
$results = [];
foreach ($connections as $i => $conn) {
$content = curl_multi_getcontent($conn);
$info = curl_getinfo($conn);
if ($info['http_code'] === 200) {
$results[$urls[$i]] = $content;
// 可在此解析、入庫、寫文件
} else {
error_log("URL {$urls[$i]} 失敗: HTTP {$info['http_code']}");
}
curl_multi_remove_handle($mh, $conn);
curl_close($conn);
}
curl_multi_close($mh);
// 後續處理 $results...
?>
const MAX_CONCURRENT = 20;
$queue = new SplQueue($urls);
$running = [];
while ($queue->count() || $running) {
// 啟動新連接直到達到上限
while ($queue->count() && count($running) < MAX_CONCURRENT) {
$url = $queue->dequeue();
$conn = curl_init($url);
// ... 設置選項 ...
curl_multi_add_handle($mh, $conn);
$running[(int)$conn] = $url;
}
// 執行一次循環
curl_multi_exec($mh, $active);
// 檢查完成的連接
while ($done = curl_multi_info_read($mh)) {
$conn = $done['handle'];
$url = $running[(int)$conn];
// 處理結果...
curl_multi_remove_handle($mh, $conn);
curl_close($conn);
unset($running[(int)$conn]);
}
// 短暫休眠讓出 CPU
usleep(10000);
}
$maxRetries = 3;
$retryCount = [];
// 在結果處理邏輯中
if ($info['http_code'] >= 500 || $info['http_code'] === 0) {
if (($retryCount[$url] ?? 0) < $maxRetries) {
$retryCount[$url]++;
$queue->enqueue($url); // 重新加入隊列
continue;
}
}
現代項目建議使用 ReactPHP 或 Amp 等事件驅動框架,代碼更優雅:
// ReactPHP 示例
$client = new React\HttpClient\Browser($loop);
$promises = array_map(fn($url) => $client->get($url), $urls);
\React\Promise\all($promises)->then(function($responses) {
foreach ($responses as $response) {
// 處理 $response->getBody()
}
});
$loop->run();
| 併發數 | 10 URL 耗時 | 50 URL 耗時 | 內存峰值 |
|---|---|---|---|
| 1 (串行) | 2.1s | 10.5s | 2 MB |
| 5 | 0.45s | 2.2s | 8 MB |
| 20 | 0.22s | 1.1s | 25 MB |
| 50 | 0.21s | 0.9s | 55 MB |
最佳併發數取決於目標站點限流策略、本地帶寬、文件描述符限制(ulimit -n)。
curl_multi 是 PHP 單進程高併發網絡 I/O 的標準解法。掌握「隊列控制併發數 → 事件循環 → 異步獲取結果 → 統一錯誤處理」這一模式,即可構建高性能採集器。生產系統建議遷移至 ReactPHP/Amp 等成熟異步框架,獲得更好的可維護性與背壓控制。