Nodejs怎样去避免同步写法,写出真正的异步代码?
Nodejs怎样去避免同步写法,写出真正的异步代码?
我有两段代码,用来抓取学校的新闻图片,由于每个页面的url都是大概这样的格式*****.com/1 *****.com/2 *****.com/3 所以我就用一个循环把所有的页面穷举一遍,然后对每个循环请求一次url,抓取里面的新闻链接,存在news里面,在访问news里面的url,把照片down下来。代码大概是这样的:
var cheerio = require('cheerio'),
request = require('request'),
_ = require('underscore'),
gm = require('gm'),
fs = require('fs');
var fileNumber = 0;
var picNum = 0;
var news = [];
const address = 'http://ssdut.dlut.edu.cn';
const url = "http://ssdut.dlut.edu.cn/index.php/News/index/p/";
const _dirname= "E:/pictures/"
function newsdown (){
for(var i=0;i<82;i++){
request(url+i,function(err,res,body){
//上面的i如果换成filenumber,结果永远是访问第一个页面
fileNumber++;
//这里如果我fileNumber写成i,结果永远是82
//我也不知道,为什么,所以用了两个计数器
console.log(url+fileNumber+'<-page');
var $ = cheerio.load(body);
//console.log($('table').eq(2).find('table').eq(1).find('tr').slice(1,13).html())
$('table').eq(2).find('table').eq(1).find('tr').slice(1,13)
.each(function(index,ele){
//console.log(address+this.find('a').attr('href'));
//把url存在news里面
news.push(this.find('a').attr('href'));
});
});
};
};
//访问news里的url,把img标签里的图片存在电脑里
function picdown (){
_.each(news,function(){
request(address+this.find('a').attr('href'),function(err,res,body){
var $ = cheerio.load(body);
$('img').slice(1).each(function(index,ele){
gm(address+this.attr('src')).write(_dirname+picNum+'.jpg',function(err){
if(!err) console.log('No:'+picNum+':done!');
picNum++;
});
});
});
});
}
newsdown();
picdown();
这段代码的问题在于……我写成了同步的,newsdown这部分还没只得到结果,picdown就运行了,结果news里面全是null。
然后我写成回调的形式在最后调用picdown,但实际上还是上面的代码没把news得出结果,回调函数就执行了。最后没有办法,我直接把picdown嵌套在迭代的内部,变成下面这段:
var cheerio = require('cheerio'),
request = require('request'),
_ = require('underscore'),
gm = require('gm'),
fs = require('fs');
//23
var fileNumber = 0;
var picNum = 0;
var news = [];
const address = 'http://ssdut.dlut.edu.cn';
const url = "http://ssdut.dlut.edu.cn/index.php/News/index/p/";
const _dirname= "E:/pictures/"
function newsdown (){
for(var i=0;i<82;i++){
request(url+i,function(err,res,body){
fileNumber++;
console.log(url+fileNumber+'<-page');
var $ = cheerio.load(body);
//console.log($('table').eq(2).find('table').eq(1).find('tr').slice(1,13).html())
$('table').eq(2).find('table').eq(1).find('tr').slice(1,13)
.each(function(index,ele){
//console.log(address+this.find('a').attr('href'));
request(address+this.find('a').attr('href'),function(err,res,body){
var $ = cheerio.load(body);
$('img').slice(1).each(function(index,ele){
gm(address+this.attr('src')).write(_dirname+picNum+'.jpg',function(err){
if(!err) console.log('No:'+picNum+':done!');
picNum++;
});
});
});
});
});
};
};
newsdown();
图片是能down下来了,但是运行的测试信息是这样的:
部分截图
http://ssdut.dlut.edu.cn/index.php/News/index/p/77<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/78<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/79<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/80<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/81<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/82<-page
No:0:done!
No:1:done!
No:3:done!
No:4:done!
No:7:done!
也就是外层循环全部执行完了,才来执行内层的循环。(还是说外层的循环先返回结果,而I\O量更大的内层才返回?)
还有就是每次执行都会漏掉一些图片,我就跑了很多遍,我又是按迭代的number来命名的,所以有些图片可能占了 4.jpg ,8.jpg,9.jpg几个名字的情况,要么就是把前面down的图片覆盖了,要么就是被别的图片覆盖了。
然后我又写了一个模拟登陆QQ的代码,发送信息,奇怪的地方写在注释里了,代码大概是这样的:
var request = require('request');
var qqinfo = {
loginType:2,//1:不登录 2:隐身登录 3:在线登录
qq:123456,//qq号
pwd:‘123456’//qq密码
};
var Vdata ={
sid:’’
};
var userAgend = ‘Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13’;
var PostHead = {‘User-Agent’ : userAgend , ‘Content-Type’ : ‘application/x-www-form-urlencoded’};
var address = ‘http://pt.3g.qq.com/handleLogin’;
var Post = {
url:address,
headers:PostHead,
method:‘POST’,
form:qqinfo
};
var Friends = [];
//模拟webqq post消息,登陆以后用正则表达式抓取sid
request(Post,function(err,res,body){
if(res.statusCode == 302){
var reg = new RegExp(‘sid=(.[^&]+)’,‘ig’);
//console.log(res.headers.location);
reg.exec(res.headers.location);
var sid = RegExp.$1
Vdata.sid = sid;
//console.log(sid);
console.log(‘login!’);
getFriendsPage(1,function(){
console.log(’\nonline friends:’+Friends.length+’\n’);
for(var i = 0 ;i<Friends.length;i++){
var qqInfo = Friends[i];
console.log(i+1+’:’+qqInfo.qq+’ ‘+’\t[’+qqInfo.name+’]’);
};
if(sendmsg(1,‘hi’)) console.log(‘ok!’);
});
return;
}
if(res.body.indexOf(‘验证码’)>=0){
console.log(‘需要输入验证码’);
return;
}
});
//用拿到的sid请求好友列表界面
function getFriendsPage(page,cb) {
Friends =[];
var chatMain = ‘http://q16.3g.qq.com/g/s?sid=$SID&aid=nqqchatMain&p=$Page’;
chatMain = chatMain.replace(’$SID’,Vdata.sid);
chatMain = chatMain.replace(’$Page’,page);
request({url:chatMain,headers:{‘User-Agent’:userAgend,‘Cache-Control’:‘max-age=0’},method:‘GET’},function(err,res,body){
var regx = /u=(\d+)[\s\S]+?class=“name” >(.*?)</span>/ig
//如果我在这里写一个console.log(boody)什么的,下面的body却是null。
while(regx.exec(body)){
console.log('qq'+RegExp.$1);
var qqInfo = {qq:RegExp.$1,name:RegExp.$2};
Friends.push(qqInfo);
};
cb();
return;
});
};
//模拟信息发送
function sendmsg(index,msg){
var qqInfo = Friends[index-1];
var form ={
‘u’:qqInfo.qq,
‘msg’:msg,
‘aid’:‘发送’
}
if(!qqInfo) return false;
var pUrl = ‘http://q32.3g.qq.com/g/s?sid=$SID’;
pUrl = pUrl.replace(’$SID’,Vdata.sid);
request({url:pUrl,form:form,headers:{‘User-Agent’:userAgend,‘Cache-Control’:‘max-age=0’,‘Content-Type’: ‘application/x-www-form-urlencoded’},method:‘POST’},function(err,res,body){
if(body.indexOf(‘重新登录’)>=0 && body.indexOf(‘书签可能有误’)>=0){
console.log(‘发送失败’);
console.log(form);
return false;
}else{
return true;
}
});
}
如果我删掉之前的调试信息,就勉强能够运行,是不是就是说,我的同步代码要建立在之前的I\O要比之后的I\O更快得出结果的前提下?
这让我很困惑,怎样写才能符合异步的风格呢? 仅仅回调好像也不代表异步………… 如果用事件触发又该是怎样的呢……
为了将你的同步代码转换为异步代码,并且避免回调地狱,可以使用Promise或者async/await语法。以下是修改后的代码示例:
使用Promises
const cheerio = require('cheerio');
const request = require('request-promise-native'); // 使用request-promise-native库
const fs = require('fs');
const gm = require('gm');
const address = 'http://ssdut.dlut.edu.cn';
const url = "http://ssdut.dlut.edu.cn/index.php/News/index/p/";
const _dirname = "E:/pictures/";
async function fetchNewsLinks() {
let news = [];
for (let i = 1; i <= 82; i++) {
const response = await request(`${url}${i}`);
const $ = cheerio.load(response);
const links = [];
$('table').eq(2).find('table').eq(1).find('tr').slice(1, 13).each((index, ele) => {
links.push($(ele).find('a').attr('href'));
});
news = news.concat(links);
}
return news;
}
async function downloadImages(newsLinks) {
for (const link of newsLinks) {
const response = await request(`${address}${link}`);
const $ = cheerio.load(response);
const images = [];
$('img').slice(1).each((index, ele) => {
images.push($(ele).attr('src'));
});
for (const imgSrc of images) {
await new Promise((resolve, reject) => {
gm(`${address}${imgSrc}`)
.write(`${_dirname}${picNum}.jpg`, (err) => {
if (!err) {
console.log(`No:${picNum}:done!`);
}
picNum++;
resolve();
});
});
}
}
}
(async () => {
try {
const newsLinks = await fetchNewsLinks();
await downloadImages(newsLinks);
} catch (error) {
console.error(error);
}
})();
解释
- 使用
request-promise-native
库:这个库允许你使用Promise来处理请求,而不是传统的回调。 - 使用
async
和await
:这样可以使代码更易于阅读和维护,同时保持异步特性。 - 分离逻辑:将获取新闻链接和下载图片的逻辑分离到不同的函数中,每个函数返回一个Promise。
- 异步循环:使用
for...of
循环来处理数组中的每个元素,并等待每个异步操作完成。
通过这种方式,你可以更好地管理异步流程,避免回调地狱,并使代码更清晰和易于维护。
去看看 async 或者 iced coffeescript
在循环中绑定事件处理如果需要用到循环变量,注意要用传值的方式传递,否则事件处理只是保留了对循环变量的引用,最后触发时使用的都是同一个值
还没理解异步的编程思想,再多看看案例
这个就是大家通常吐槽的 }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
如果觉得自己能力还可以的话可以去看一下 promise 规范…你的问题这个规范里面都有说
也可以试下EventProxy~~
嗯,我去看看。
用js的闭包方法写,就一定是异步的了。。。。。
这样……
async+eventproxy 这两个基本是必装的模块啊!
iced 看上去很美,但生成的出来的代码根本无法看啊。
我大概看了一下eventproxy,它可以让串行的回调并行运行,然后不用深度嵌套写回调函数。 比如我要创建一个正则表达式,然后去exec。 我想知道,比如 regexp.exec(a)但是这个regexp没有回调的方式,I/O还没有结束,下面的代码就已经拿a去跑了,这个时候怎么办?
异步确实还是Promises 推荐RSVP
还是推荐async,和promise比起来,代码更易维护,也方便写测试用例,同时也符合node社区中的异步回调规范
要将上述同步代码改为异步代码,可以使用Promise、async/await等方法来避免回调地狱,并确保异步操作顺序正确。以下是改进后的代码示例:
使用async/await
const cheerio = require('cheerio');
const request = require('request-promise-native'); // 使用request-promise-native替代原生request
const fs = require('fs');
const path = require('path');
const baseUrl = 'http://ssdut.dlut.edu.cn';
const apiUrl = `${baseUrl}/index.php/News/index/p/`;
const downloadDir = path.join(__dirname, 'pictures');
async function fetchNewsUrls() {
let newsUrls = [];
for (let i = 1; i <= 82; i++) {
const response = await request(apiUrl + i);
const $ = cheerio.load(response);
const newsLinks = $('table')
.eq(2)
.find('table')
.eq(1)
.find('tr')
.slice(1, 13)
.map((_, el) => $(el).find('a').attr('href'))
.get();
newsUrls = newsUrls.concat(newsLinks);
}
return newsUrls;
}
async function downloadImages(newsUrls) {
for (const newsUrl of newsUrls) {
try {
const response = await request(baseUrl + newsUrl);
const $ = cheerio.load(response);
const imgSrcs = $('img').slice(1).map((_, el) => $(el).attr('src')).get();
for (let i = 0; i < imgSrcs.length; i++) {
const imgSrc = imgSrcs[i];
const imagePath = path.join(downloadDir, `${i}.jpg`);
await new Promise((resolve, reject) => {
gm(fs.createReadStream(imgSrc))
.write(imagePath, (err) => {
if (err) return reject(err);
resolve();
});
});
console.log(`Downloaded: ${imagePath}`);
}
} catch (error) {
console.error(`Failed to download images from ${newsUrl}:`, error);
}
}
}
(async () => {
try {
const newsUrls = await fetchNewsUrls();
await downloadImages(newsUrls);
} catch (error) {
console.error('Error:', error);
}
})();
解释
- 使用
request-promise-native
:它返回一个Promise对象,这样我们可以更方便地使用async/await
语法。 - fetchNewsUrls函数:通过循环获取所有新闻链接,并将其存储在
newsUrls
数组中。 - downloadImages函数:对于每个新闻链接,下载所有图片并保存到本地目录。
- 异步主函数:首先调用
fetchNewsUrls
获取新闻链接,然后调用downloadImages
下载图片。
这种方式可以确保所有异步操作都按顺序执行,避免了回调地狱,并且代码结构更加清晰。