Node.js使用request+fs批量下载图片仅首图、文件丢失损坏问题排查
问题原因
- 变量名不匹配:代码中声明存储图片链接的数组为
links,但循环遍历时使用了未定义的images变量,直接导致循环逻辑异常,这是仅能下载1张甚至无法下载的核心原因。 - 异步闭包陷阱:你使用全局变量
downloadedImages作为文件名,而download是异步函数,回调触发时循环早已执行完毕,downloadedImages已经被累加为最大值,所有回调打印的都是最终的downloadedImages值,对应的文件根本不存在,就会出现日志打印成功但找不到文件的问题。 - 无错误处理逻辑:没有判断请求是否成功,当请求被反爬拦截、返回403/404/500等错误响应时,你直接把错误页面的文本内容写入了图片后缀的文件,自然会出现文件损坏无法打开的问题;同时也没有监听文件写入的错误事件,写入失败时没有任何提示。
- 正则匹配风险:你用正则匹配后缀时没有判断匹配结果是否存在,遇到链接中没有指定后缀的图片时,直接取
[0]会抛出报错,打断后续下载逻辑。 - 高频请求触发反爬:批量同步发起大量下载请求,很容易被目标站点的反爬策略拦截,后续请求直接被拒绝,自然无法下载成功。
- 你正则中写的
webo应为webp,匹配不到对应后缀也会导致逻辑异常。
修复方案
- 修正变量名错误,统一使用同一个数组存储链接。
- 用循环索引作为文件名,避免异步回调的变量污染。
- 增加全链路错误捕获,请求失败、写入失败时打印提示,避免错误内容写入图片文件。
- 增加请求头模拟浏览器行为,降低反爬拦截概率。
- 控制并发下载数,避免请求过于集中被拦截。
修复后完整代码
const request = require("request") const fs = require("fs") const path = require("path") // 存储图片链接的数组,注意变量名统一 const links = [/* 填写你的图片链接 */] // 本地保存目录,自动创建 const SAVE_DIR = './download_images' if (!fs.existsSync(SAVE_DIR)) fs.mkdirSync(SAVE_DIR, { recursive: true }) // 最大并发下载数,可根据站点情况调整 const MAX_CONCURRENT = 5 let currentIndex = 0 const download = (uri, filename) => { return new Promise((resolve, reject) => { // 增加请求头模拟浏览器请求,降低反爬拦截概率 const options = { uri, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': new URL(uri).origin }, timeout: 15000 } request.head(options, (err, res) => { if (err || res.statusCode !== 200) { return reject(new Error(`请求失败:${err?.message || '状态码' + res.statusCode} | ${uri}`)) } // 校验返回内容是否为图片 const contentType = res.headers['content-type'] if (!contentType?.startsWith('image/')) { return reject(new Error(`返回内容非图片:${contentType} | ${uri}`)) } const writeStream = fs.createWriteStream(path.join(SAVE_DIR, filename)) request(options) .on('error', (err) => reject(new Error(`下载失败:${err.message} | ${uri}`))) .pipe(writeStream) .on('error', (err) => reject(new Error(`写入失败:${err.message} | ${uri}`))) .on('finish', () => resolve(filename)) }) }) } // 并发控制执行下载 const run = async () => { const tasks = [] while (currentIndex < links.length) { const url = links[currentIndex] // 匹配后缀,增加空判断避免报错 const formatMatch = url.match(/(png|jpg|webp|jpeg|gif)/i) const format = formatMatch ? formatMatch[1] : 'jpg' const filename = `${currentIndex}.${format}` const task = download(url, filename) .then((saveName) => { console.log(`下载成功:${saveName} [${currentIndex + 1}/${links.length}]`) }) .catch((err) => { console.error(`下载失败:${err.message}`) }) .finally(() => { if (currentIndex < links.length - 1) { currentIndex++ const nextUrl = links[currentIndex] const nextFormatMatch = nextUrl.match(/(png|jpg|webp|jpeg|gif)/i) const nextFormat = nextFormatMatch ? nextFormatMatch[1] : 'jpg' const nextFilename = `${currentIndex}.${nextFormat}` tasks.push(download(nextUrl, nextFilename)) } }) tasks.push(task) currentIndex++ if (tasks.length >= MAX_CONCURRENT) { await Promise.all(tasks) tasks.length = 0 } } await Promise.all(tasks) console.log('全部下载任务执行完毕') } run()
内容的提问来源于stack exchange,提问作者shenny tan
相关产品推荐
相关产品推荐

