Node.js Request+Cheerio循环爬取CPU数据的异步变量问题求助
解决Node.js循环中异步回调共享变量的问题
这个问题是JavaScript里异步操作与作用域的经典坑,我来帮你拆解原因和解决方案:
问题根源
- 变量作用域问题:你用
var i声明循环变量,var是函数级作用域,整个循环里只有一个i变量。当异步的request回调执行时,循环已经全部跑完了,此时i的值已经变成了box.length(也就是24),所以所有回调里打印的i都是24。 - 对象引用共享:你在循环里用
var cpu声明对象,同样因为var的作用域,所有迭代共享同一个cpu对象引用。当后续异步回调修改这个对象时,前面已经push到allCpu里的条目也会被覆盖,最终所有数据都是最后一个CPU的信息。
修复方案
我会用ES6的块级作用域和Promise来解决这两个问题,同时优化异步流程避免回调地狱:
const request = require('request'); const cheerio = require('cheerio'); const fs = require('fs'); const url = 'https://www.alza.sk/cpu-procesory/18842843-p1.htm'; let allCpu = []; // 封装request为Promise,方便异步控制 const requestPromise = (url) => { return new Promise((resolve, reject) => { request(url, (error, response, html) => { if (error) reject(error); else resolve(html); }); }); }; requestPromise(url) .then(html => { const $ = cheerio.load(html); const box = $('#boxc .box'); // 用let声明i,创建块级作用域 const fetchCpuPromises = []; for (let i = 0; i < box.length; i++) { console.log(i); // 正常输出0-23 // 用let声明cpu,每个迭代有独立的对象 let cpu = { meno: '', link: '', socket: '', radaCpu: '', xJadier: '', frekvenciaCpu: '', maxFrekvenciaCpu: '' }; const $boxItem = box.eq(i); cpu.meno = $boxItem.find('.fb a').text(); cpu.link = $boxItem.find('.fb a').attr('href'); // 把每个CPU的请求Promise存入数组 const fetchPromise = requestPromise('https://www.alza.sk/' + cpu.link) .then(detailHtml => { const $socket = cheerio.load(detailHtml); const params = $socket('#popis .params .groupValues .row a'); params.each(function() { const text = $(this).text(); const value = $(this).parents('.row').find('.value').text(); switch(text) { case 'Socket': cpu.socket = value; break; case 'Rad procesora': cpu.radaCpu = value; break; case 'Počet jadier procesora': cpu.xJadier = value; break; case 'Frekvencie procesora': cpu.frekvenciaCpu = value; break; case 'Maximálna frekvencia (OC)': cpu.maxFrekvenciaCpu = value; break; } }); return cpu; // 返回处理好的cpu对象 }) .catch(err => { console.error(`获取CPU详情失败: ${err}`); return null; // 出错时返回null,不影响整体流程 }); fetchCpuPromises.push(fetchPromise); } // 等待所有CPU详情请求完成 return Promise.all(fetchCpuPromises); }) .then(cpuList => { // 过滤掉出错的null条目 allCpu = cpuList.filter(cpu => cpu !== null); // 写入文件 fs.writeFile('output.json', JSON.stringify(allCpu, null, 4), err => { if (err) console.error('写入文件失败:', err); else console.log('File successfully written! - Check your project directory for the output.json file'); }); }) .catch(err => { console.error('获取CPU列表失败:', err); });
关键优化点
- 块级作用域:用
let i和let cpu替代var,确保每个循环迭代有独立的变量实例,避免回调共享变量。 - Promise封装:把
request封装成Promise,用Promise.all等待所有异步请求完成,这样能保证所有CPU数据都获取到后再写入文件,解决了原来靠i == box.length -1判断的不可靠问题(因为异步请求的完成顺序不一定和循环顺序一致)。 - 代码可读性:用
switch替代多个if判断,结构更清晰;用Promise链式调用替代嵌套回调,避免回调地狱。
内容的提问来源于stack exchange,提问作者Simon
相关产品推荐
相关产品推荐

