前记:
想写一个电影天堂的爬虫,因为node很长时间落下,就想用node去写一下。结果遇到了一些列的问题,这些问题归根到底就是异步流程控制的问题,在以前就一直会接触到很多回调地狱,Promise为什么会出现诸如此类的话题,现在终于是深刻体会到了!
开始的代码是:
const cheerio = require('cheerio');
const http = require('http');
const iconv = require('iconv-lite');
let baseUrl = "http://www.ygdy8.net/html/gndy/dyzz/list_23_";
let Host = "http://www.ygdy8.net/";
let titleHref = [];
const totalPage = 1; //指定爬多少页数据
let res = [];
//获取页面电影数据
function getTitleHref(url,page) {
let startUrl = url+page+".html";
http.get(startUrl,function(res) {
let chunks = [];
res.on('data',function(chunk){
chunks.push(chunk);
});
res.on('end',function(){
let title = [];
let html = iconv.decode(Buffer.concat(chunks),'gb2312');
let $ = cheerio.load(html, {decodeEntities: false});
// console.log($);
$('.co_content8 .ulink').each(function(i,d) {
let $d = $(d);
titleHref.push({
href: $d.attr('href')
});
});
console.log(titleHref);
});
if(page <= totalPage) {
getTitleHref(url,++page);
}else {
console.log(page);
getLink(titleHref);
}
});
}
//获取种子链接
function getLink(titleHref) {
console.log('进入getLink');
titleHref.forEach(function(v,k) {
console.log('~~~~~~~~~~~~~~~~~~~~');
let infoUrl = Host + v.href;
console.log(infoUrl);
// try {
http.get(infoUrl,function(res) {
console.log('进入getlink http');
let chunks = [];
res.on('data',function(chunk) {
chunks.push(chunk);
});
res.on('end', function(){
let html = iconv.decode(Buffer.concat(chunks),'gb2312');
let $ = cheerio.load(html, {decodeEntities: false});
let reg = /.*译 名/;
let info = '';
let bt = '';
let textInfo = $('.co_content8 #Zoom p').eq(0).text();
info = textInfo.match(reg)[0];
bt = $('#Zoom td').children('a').attr('href');
res.push({
Info:info,
Bt:bt
});
console.log(res);
})
//怎么捕获错误!!!
//res.on('error',function(){
// console.log('error');
//})
})
// }catch(e) {
// console.log(e);
// }
});
};
getTitleHref(baseUrl,1)
所以写node代码切记大多数都是异步的,上面代码就出了一个问题:
当前代码就不能保证下面的代码, 在 res.end 后执行,因为res.end在异步队列里可能没执行完,就进入了下面的if,就算最后进入getLink后就会出现titleHref.forEach进不去的情况的,因为titleHref是空的。
当时遇到这个问题如果不考虑到异步流程控制的解决流程的话,一个解决方案是在each函数里,获取到一个titleHref就getLink下,titileHref定义成局部函数,getLink函数放在each里面,这样就保证titleHref不会是空的了。然后代码如下:
const cheerio = require('cheerio');
const http = require('http');
const iconv = require('iconv-lite');
let baseUrl = "http://www.ygdy8.net/html/gndy/dyzz/list_23_";
let Host = "http://www.ygdy8.net/";
const totalPage = 2; //指定爬多少页数据
let ans = [];
//获取页面电影数据
function getTitleHref(url,page) {
let startUrl = url+page+".html";
http.get(startUrl,function(res) {
const { statusCode } = res;
let chunks = [];
res.on('data',function(chunk){
chunks.push(chunk);
});
res.on('end',function(){
let title = [];
let html = iconv.decode(Buffer.concat(chunks),'gb2312');
let $ = cheerio.load(html, {decodeEntities: false});
// console.log($);
$('.co_content8 .ulink').each(function(i,d) {
let $d = $(d);
let titleHref = [];
titleHref.push({
href: $d.attr('href')
});
getLink(titleHref);
});
// console.log(ans);
});
});
}
// /*
//获取种子链接
function getLink(titleHref) {
console.log('进入getLink');
console.log(titleHref);
if(titleHref) {
titleHref.forEach(function(v,k) {
console.log('~~~~~~~~~~~~~~~~~~~~');
let infoUrl = Host + v.href;
// console.log(infoUrl);
http.get(infoUrl,function(res) {
const { statusCode } = res;
const contentType = res.headers['content-type'];
let error;
if (statusCode !== 200) {
error = new Error('请求失败。\n' +
`状态码: ${statusCode}`);
}
if (error) {
console.error(error.message);
// 消耗响应数据以释放内存
res.resume();
return;
}
console.log('进入getlink http');
let chunks = [];
res.on('data',function(chunk) {
chunks.push(chunk);
});
res.on('end', function(){
try {
let html = iconv.decode(Buffer.concat(chunks),'gb2312');
let $ = cheerio.load(html, {decodeEntities: false});
let bt = '';
bt = $('#Zoom td').children('a').attr('href');
// console.log(bt);
// console.log(typeof bt)
ans.push(bt);
// cb(ans);
}catch (e) {
console.error('bt',e.message);
}
})
}).on('error', (e) => {
console.error(`错误: ${e.message}`);
});
});
}
};
// */
for(let i = 1; i <= totalPage; i++) {
getTitleHref(baseUrl,i);
console.log(ans);
};
但是这样的代码你还会发现一个问题,我们最后保存的bt链接的ans结果,打印的还是空的,同样是异步的问题,我们如果要存入数据库或者需要ans数据的话,我们不知道何时返回了这个数据。
所以最终我们还是要用到ES6/7提出的方案Promise和async/await。
修改之后代码如下:
const cheerio = require('cheerio')
const http = require('http')
const iconv = require('iconv-lite')
const baseUrl = 'http://www.ygdy8.net/html/gndy/dyzz/list_23_'
const Host = 'http://www.ygdy8.net/'
const totalPage = 2 //指定爬多少页数据
let ans = []
//获取页面电影数据
function getTitleHref(url, page) {
return new Promise((resolve, reject) => {
let startUrl = url + page + '.html'
http.get(startUrl, function(res) {
const { statusCode } = res
let chunks = []
res.on('data', function(chunk) {
chunks.push(chunk)
})
res.on('end', function() {
let title = []
let html = iconv.decode(Buffer.concat(chunks), 'gb2312')
let $ = cheerio.load(html, { decodeEntities: false })
let titleHref = []
$('.co_content8 .ulink').each(function(i, d) {
let $d = $(d)
titleHref.push({
href: $d.attr('href')
})
})
resolve(getLink(titleHref))
})
})
})
}
// /*
//获取种子链接
function getLink(titleHref, cb) {
console.log('进入getLink')
console.log(titleHref)
if (titleHref) {
return Promise.all(
titleHref.map(function(v, k) {
return new Promise((resolve, reject) => {
console.log('~~~~~~~~~~~~~~~~~~~~')
let infoUrl = Host + v.href
http
.get(infoUrl, function(res) {
const { statusCode } = res
const contentType = res.headers['content-type']
let error
if (statusCode !== 200) {
error = new Error('请求失败。\n' + `状态码: ${statusCode}`)
}
if (error) {
console.error(error.message)
// 消耗响应数据以释放内存
res.resume()
return
}
let chunks = []
res.on('data', function(chunk) {
chunks.push(chunk)
})
res.on('end', function() {
try {
let html = iconv.decode(Buffer.concat(chunks), 'gb2312')
let $ = cheerio.load(html, { decodeEntities: false })
let bt = ''
bt = $('#Zoom td')
.children('a')
.attr('href')
resolve(bt)
} catch (e) {
reject(e)
}
})
})
.on('error', e => {
reject(e)
})
})
})
)
} else {
return Promise.resolve()
}
}
async function main() {
// */
let results = await Promise.all(
new Array(totalPage).fill().map((_, i) => getTitleHref(baseUrl, i + 1))
)
ans = ans.concat(...results)
console.log('get data:', ans)
}
main()
每个函数都封装成Promise,最后在主函数中用await强制同步得到最后的结果results。(注意:1. new Array出来的是稀疏数组empty,最后fill()一下填充成undefine,2. 47行传递的已经不是一个只有一条数据的数组了,而是将一个页面each执行完成后的汇总,所以在函数内部会有Promise.all
3. 93行则聊胜于无,即使return null也会正确的触发resolve的,这么写只是提高一些可读性罢了。).
Promise和async/await整理可以看我的这篇博客Promise和async/await用法整理
代码:github传送
这次告诉我实践很重要!要把所学和书中所看运用到业务和代码逻辑中!