使用Node.js Fetch API批量下载图片的异步问题及优化咨询
批量图片下载优化方案
先解决异步循环等待问题
你之前用async/await没成功,大概率是循环写法不对(比如用了forEach,它不支持异步等待)。改用for...of循环就能让每一步下载完成后再执行下一次循环:
const fs = require('fs').promises; const path = require('path'); const axios = require('axios'); async function downloadSingleImage(volumeId, chapterId, pageId) { const url = `https://你的域名/${volumeId}/${chapterId}/${pageId}.jpg`; const savePath = path.join('./downloads', volumeId, chapterId); // 先创建目录 await fs.mkdir(savePath, { recursive: true }); try { const response = await axios.get(url, { responseType: 'stream' }); // 检查响应状态码判断图片是否存在 if (response.status === 200) { const writeStream = fs.createWriteStream(path.join(savePath, `${pageId}.jpg`)); await new Promise((resolve, reject) => { response.data.pipe(writeStream); writeStream.on('finish', resolve); writeStream.on('error', reject); }); console.log(`成功下载:${url}`); return true; } } catch (err) { // 404等错误直接返回false,停止当前章节的页数遍历 if (err.response?.status === 404) { return false; } console.log(`下载失败:${url},错误:${err.message}`); // 非404错误可以考虑重试,这里简单跳过 return true; } } async function downloadVolume(volumeId, chapterCount) { for (let chapterId = 1; chapterId <= chapterCount; chapterId++) { let pageId = 1; while (pageId <= 30) { const exists = await downloadSingleImage(volumeId, chapterId.toString(), pageId.toString()); if (!exists) { // 图片不存在,当前章节结束 break; } pageId++; } } } // 主函数,遍历所有卷 async function main() { // 假设你从数据库拿到的卷列表是这个格式:[{volumeId: 'v1', chapterCount: 5}, ...] const volumes = await getVolumesFromDB(); for (const volume of volumes) { await downloadVolume(volume.volumeId, volume.chapterCount); } } main().catch(err => console.error('批量下载失败', err));
用流下载的优势
你提到的流方案确实更适合批量场景:
- 内存占用低:不需要把整张图片加载到内存,边接收边写入磁盘,避免大量图片同时下载导致内存溢出
- 稳定性高:大文件下载时,流不会因为内存不足崩溃
- 进度可控:可以监听流的
data事件获取下载进度(如果需要的话)
进阶:并发控制优化
如果想提高下载效率,又不想因为并发过高被服务器限制,可以引入并发控制,比如限制同时下载5张图:
const pLimit = require('p-limit'); const limit = pLimit(5); // 限制并发数为5 const completedChapters = new Set(); async function downloadVolumeWithConcurrency(volumeId, chapterCount) { const tasks = []; for (let chapterId = 1; chapterId <= chapterCount; chapterId++) { for (let pageId = 1; pageId <= 30; pageId++) { tasks.push(limit(async () => { const key = `${volumeId}-${chapterId}`; // 如果章节已标记完成,直接跳过 if (completedChapters.has(key)) return false; const exists = await downloadSingleImage(volumeId, chapterId.toString(), pageId.toString()); if (!exists) { completedChapters.add(key); } return exists; })); } } await Promise.all(tasks); }
注:p-limit是一个轻量的并发控制包,也可以自己实现简单的并发队列逻辑。
其他注意事项
- 重试机制:针对网络波动导致的失败,可以给下载函数加重试逻辑(比如失败后延迟1秒重试2次)
- 日志记录:把失败的URL记录到文件,方便后续补下
- 请求头模拟:加上浏览器的User-Agent等请求头,避免被服务器识别为异常请求(虽然你有授权,友好请求更稳妥)
内容的提问来源于stack exchange,提问作者Nicolò Rancan
相关产品推荐
相关产品推荐

