使用Node.js + Axios批量下载图片时无报错突然停止
问题分析与解决方案
核心问题原因
无限制并发请求
一次性发起4500个HTTP请求,远超服务器和本地系统的资源承载上限:目标服务器会触发限流(IP临时封禁、请求丢弃),本地网络连接数、文件描述符也会被耗尽,直接导致下载中途停滞,部分请求无响应、流写入中断,产生残缺或空文件。错误处理缺失
当前代码仅监听了文件流的写入错误,未捕获axios请求本身的异常(如网络断开、HTTP 4xx/5xx状态码);加上Promise.allSettled会自动忽略失败的Promise,导致你看不到任何报错,无法感知请求失败。流写入无异常兜底
当请求中途失败时,已创建的空WriteStream未被清理,残留空文件;部分中断的流也会生成残缺文件。
具体修复方案
1. 控制并发请求数
用工具(如p-limit)将同时发起的请求数控制在10-20之间,避免资源耗尽和被限流。
2. 完善全链路错误处理
捕获axios请求、流写入的所有异常,记录失败任务;请求失败时自动清理空文件。
3. 添加重试机制
对失败的请求进行有限次数重试,应对临时网络波动或服务器限流。
4. 校验文件完整性
下载完成后对比文件大小(通过响应头Content-Length),确保文件完整,残缺文件自动删除。
修改后的代码示例
const fs = require("fs"); const path = require("path"); const axios = require("axios"); const pLimit = require("p-limit"); // 先安装依赖:npm install p-limit // 配置项 const collection = "rebels"; const folder = `${collection}_json`; const outputDir = `${collection}_images2`; const concurrencyLimit = 15; // 并发数可根据网络调整 const retryTimes = 2; // 重试次数 // 创建输出目录(不存在则自动创建) if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // 获取目录下所有JSON文件 const getFiles = (folder) => { const directoryPath = path.join(__dirname, folder); return fs.readdirSync(directoryPath); }; // 解析JSON提取图片URL const urlsArray = getFiles(folder).map(file => { const filePath = path.join(__dirname, folder, file); const fileContent = fs.readFileSync(filePath, "utf8"); const parsedJson = JSON.parse(fileContent); const imageFromMetadata = parsedJson.image; const url = imageFromMetadata.replace("ipfs://", "https://ipfs.io/ipfs/"); const nr = file.replace(".json", ""); return { url, nr }; }); // 带重试的下载函数 const downloadImageWithRetry = async (url, nr, retryCount = 0) => { const outputPath = path.join(outputDir, `${nr}.png`); // 清理之前残留的空文件 if (fs.existsSync(outputPath) && fs.statSync(outputPath).size === 0) { fs.unlinkSync(outputPath); } try { const response = await axios({ url, method: "GET", responseType: "stream", validateStatus: status => status === 200 // 仅接受200状态码的响应 }); // 获取响应头中的文件大小 const contentLength = parseInt(response.headers["content-length"], 10); if (!contentLength) throw new Error("无法获取文件大小"); return new Promise((resolve, reject) => { const writer = fs.createWriteStream(outputPath); let receivedSize = 0; // 统计已接收数据大小 response.data.on("data", chunk => { receivedSize += chunk.length; }); response.data.pipe(writer); writer.on("finish", () => { // 校验文件完整性 if (receivedSize !== contentLength) { fs.unlinkSync(outputPath); reject(new Error(`文件大小不匹配,已删除残缺文件: ${nr}.png`)); } else { console.log(`下载完成: ${nr}.png`); resolve(); } }); // 捕获文件写入错误 writer.on("error", err => { fs.unlinkSync(outputPath); reject(new Error(`写入文件失败: ${err.message}`)); }); // 捕获数据流错误 response.data.on("error", err => { writer.destroy(); fs.unlinkSync(outputPath); reject(new Error(`数据流错误: ${err.message}`)); }); }); } catch (err) { if (retryCount < retryTimes) { console.log(`下载失败,正在重试(${retryCount+1}/${retryTimes}): ${nr}.png,原因: ${err.message}`); return downloadImageWithRetry(url, nr, retryCount + 1); } else { console.error(`最终下载失败: ${nr}.png,原因: ${err.message}`); throw err; } } }; // 并发控制执行下载 const limit = pLimit(concurrencyLimit); const downloadAll = async () => { const promises = urlsArray.map(data => { return limit(() => downloadImageWithRetry(data.url, data.nr)); }); const results = await Promise.allSettled(promises); // 统计任务结果 const failedTasks = results.filter(res => res.status === "rejected").length; console.log(`全部任务完成,成功: ${results.length - failedTasks},失败: ${failedTasks}`); }; downloadAll();
额外建议
- 若当前IPFS网关限制严格,可更换为
https://cloudflare-ipfs.com/ipfs/这类备用网关。 - 运行前执行
npm install axios p-limit安装依赖。 - 可将失败任务记录到本地文件,方便后续单独处理。
内容的提问来源于stack exchange,提问作者ThomasDEV
相关产品推荐
相关产品推荐

