
本文详解php脚本在批量插入多列数据时中途停止(通常20–50条后)的根本原因——api请求频率超限(http 429),并提供延迟控制、错误捕获、sql安全加固及批量更新优化等实用解决方案。
在使用 PHP 批量从外部 API 获取数据并写入 MySQL 数据库时,看似逻辑正确的脚本却频繁在处理第 20–50 条记录时中断,这并非数据库连接或 SQL 语法问题,而极大概率源于上游 API 的速率限制(Rate Limiting)。正如实际调试所揭示的:调用 curl_error($curl) 后明确返回了 {"error":"429 - Too many requests in too short timeframe..."} —— 这是典型的 HTTP 429 Too Many Requests 响应,表明目标 API(如 Statistics Norway 的 SSB API)为防止滥用,对单位时间内的请求数做了严格限制。
? 根本原因分析
- 原脚本对每个市镇(共约 360 个)发起独立的 cURL POST 请求,无任何节流机制;
- 每次请求需建立 TCP 连接、传输 JSON 查询体、等待响应、解析 JSON,耗时虽短但累积密集;
- 多数政府/公共 API(如 SSB、GeoNorge)默认限制为 ~60 次/分钟 或更严,超出即返回 429;
- 错误未被捕获,curl_exec() 返回 false,后续 $test = json_decode($resp, true) 得到 null,导致 $test['value'][0] 等访问触发 Notice,最终 mysqli_query() 因空值或非法 SQL 报错中断流程。
✅ 推荐解决方案(四步优化)
1. 添加健壮的 cURL 错误与 HTTP 状态检查
$resp = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$curlErr = curl_error($curl);
if ($resp === false || $httpCode !== 200) {
$errorMsg = "cURL failed for kommune $kommune ({$kommune_navn}): ";
$errorMsg .= $curlErr ?: "HTTP {$httpCode}";
error_log($errorMsg); // 记录到日志,避免阻塞页面输出
echo "⚠️ Skipped $kommune due to API error.
";
curl_close($curl);
continue; // 跳过当前市镇,继续下一轮
}
curl_close($curl);2. 实施请求节流(简单有效)
在循环末尾添加 usleep() 实现毫秒级延迟,例如每请求间隔 1–2 秒:
// 在本次更新完成后、下一次循环开始前插入 usleep(1500000); // 1.5 秒,确保远低于 60req/min 限制
? 提示:usleep(1500000) ≈ 1.5 秒,360 条 × 1.5s ≈ 9 分钟完成,安全且可接受。可根据实际限流策略微调(如 usleep(1000000) 对应 60req/min)。
3. 修复 SQL 安全隐患与变量错误
原代码存在两处高危问题:
- ❌ 使用未转义的变量直接拼接 SQL(易受 SQL 注入);
- ❌ mysqli_error($conn) 中 $conn 未定义(应为 $tilkobling);
✅ 正确做法:使用预处理语句(Prepared Statements):
立即学习“PHP免费学习笔记(深入)”;
$stmt = $tilkobling->prepare(
"UPDATE kommuneinfo SET
befolkning1970 = ?, befolkning1975 = ?, befolkning1980 = ?,
befolkning1985 = ?, befolkning1990 = ?, befolkning1995 = ?,
befolkning2000 = ?, befolkning2005 = ?, befolkning2010 = ?,
befolkning2015 = ?, befolkning2020 = ?
WHERE kommunenummer = ?"
);
$stmt->bind_param(
"iiiiiiiiiii",
$befolkning1970, $befolkning1975, $befolkning1980,
$befolkning1985, $befolkning1990, $befolkning1995,
$befolkning2000, $befolkning2005, $befolkning2010,
$befolkning2015, $befolkning2020,
$kommune
);
if (!$stmt->execute()) {
error_log("DB update failed for $kommune: " . $stmt->error);
echo "❌ DB Error for {$kommune_navn} ({$kommune}): " . $stmt->error . "
";
}
$stmt->close();4. (进阶)减少 API 调用次数 —— 探索批量查询可能
查阅 SSB API 文档 可知,其支持在单个请求中查询多个区域(values 数组可包含多个 "K_XXXX")。若市镇列表已知,可尝试分批(如每批 5–10 个市镇)提交,大幅降低总请求数。示例片段:
// 构建 values 数组:["K_0101","K_0102",...,"K_0110"] $regionValues = array_map(fn($code) => "K_$code", array_slice($allKommuner, $i, 10)); // 替换原 data 中的 "values": ["K_$kommune"] → "values": $regionValues // 注意:需同步解析返回的多维 value 结构(按 region × year 排列)
此举可将 360 次请求压缩至约 40 次,从根本上规避限流。
? 总结与最佳实践
| 问题类型 | 解决方案 | 优先级 |
|---|---|---|
| API 429 限流 | usleep() 节流 + curl_error() 监控 | ⭐⭐⭐⭐⭐ |
| SQL 注入风险 | 全面改用 mysqli::prepare() + bind_param() | ⭐⭐⭐⭐⭐ |
| 错误静默失败 | error_log() 记录 + continue 跳过异常项 | ⭐⭐⭐⭐ |
| 性能瓶颈 | 探索 API 批量查询能力,合并请求 | ⭐⭐⭐ |
✅ 最终建议:先实施 1–3 步(快速见效),稳定运行后再评估第 4 步。同时,将脚本拆分为「获取数据」与「写入数据库」两个阶段,并加入进度持久化(如记录已处理 kommunenummer 到临时表),避免意外中断后全量重跑。
通过以上优化,您的 PHP 数据导入脚本将具备生产环境所需的健壮性、安全性与可维护性,轻松完成数百条记录的可靠写入。











