async mapseries.maplimit怎么结束循环

利用Node.js制作爬取大众点评的爬虫
投稿:daisy
字体:[ ] 类型:转载 时间:
相信每位用过大众点评的人都知道,大众点评上有很多美食餐馆的信息,所以这篇文章给大家分享利用Node.js实现爬取大众点评的爬虫,正好可以拿来练练手Node.js。感兴趣的可以参考借鉴。
Node.js天生支持并发,但是对于习惯了顺序编程的人,一开始会对Node.js不适应,比如,变量作用域是函数块式的(与C、Java不一样);for循环体({})内引用i的值实际上是循环结束之后的值,因而引起各种undefined的问题;嵌套函数时,内层函数的变量并不能及时传导到外层(因为是异步)等等。
一、&API分析
大众点评开放了查询餐馆信息的API,给出了城市与cityid之间的对应关系,
链接:http://m./searchshop.json?&regionid=0&start=0&categoryid=10&sortid=0&cityid=110
以GET方式给出了餐馆的信息(JSON格式)。
首先解释下GET参数的含义:
&&&& 1、start为步进数,表示分步获取信息的index,与nextStartIndex字段相对应;
&&&& 2、cityid表示城市id,比如,合肥对应于110;
&&&& 3、regionid表示区域id,每一个id代表含义在start=0时rangeNavs字段中有解释;
&&&& 4、categoryid表示搜索商家的分类id,比如,美食对应的id为10,具体每一个id的含义参见在start=0时categoryNavs字段;
&&&& 5、sortid表示商家结果的排序方式,比如,0对应智能排序,2对应评价最好,具体每一个id的含义参见在start=0时sortNavs字段。
在GET返回的JSON串中list字段为商家列表,id表示商家的id,作为商家的唯一标识。在返回的JSON串中是没有商家的口味、环境、服务的评分信息以及经纬度的;
&&&&& 因而我们还需要爬取两个商家页面:/shop/&id&、/shop/&id&/map。
通过以上分析,确定爬取策略如下(与dianping_crawler的思路相类似):
&&&&& 1、逐步爬取searchshop API的取商家基本信息列表;
&&&&& 2、通过爬取的所有商家的id,异步并发爬取评分信息、经纬度;
&&&&& 3、最后将三份数据通过id做聚合,输出成json文件。
二、爬虫实现
Node.js爬虫代码用到如下的第三方模块:
&&&&& 1、superagent,轻量级http请求库,模仿了浏览器登录;
&&&&& 2、cheerio,采用jQuery语法解析HTML元素,跟Python的PyQuery相类似;
&&&&& 3、async,牛逼闪闪的异步流程控制库,Node.js的必学库。
导入依赖库:
var util = require("util"); var superagent = require("superagent"); var cheerio = require("cheerio"); var async = require("async"); var fs = require('fs');
声明全局变量,用于存放配置项及中间结果:
var cityOptions = { "cityId": 110, // 合肥 // 全部商区, 蜀山区, 庐阳区, 包河区, 政务区, 瑶海区, 高新区, 经开区, 滨湖新区, 其他地区, 肥西县 "regionIds": [0, 356, 355, 357, , , , -922], "categoryId": 10, // 美食 "sortId": 2, // 人气最高 "threshHold": 5000 // 最多餐馆数 }; var idVisited = {}; // used to distinct shop var ratingDict = {}; // id -& ratings var posDict = {}; // id -& pos
判断一个id是否在前面出现过,若object没有该id,则为undefined(注意不是null):
function isVisited(id) { if (idVisited[id] != undefined) { } else { idVisited[id] = } }
采取回调函数的方式,实现顺序逐步地递归调用爬虫函数:
function DianpingSpider(regionId, start, callback) { console.log('crawling region=', regionId, ', start =', start); var searchBase = 'http://m./searchshop.json?&regionid=%s&start=%s&categoryid=%s&sortid=%s&cityid=%s'; var url = util.format(searchBase, regionId, start, cityOptions.categoryId, cityOptions.sortId, cityOptions.cityId); superagent.get(url) .end(function (err, res) { if (err) return console.err(err.stack); var restaurants = []; var data = JSON.parse(res.text); var shops = data['list']; shops.forEach(function (shop) { var restaurant = {}; if (!isVisited(shop['id'])) { restaurant.id = shop['id']; restaurant.name = shop['name']; restaurant.branchName = shop['branchName']; var regex = /(.*?)(\d+)(.*)/g; if (shop['priceText'].match(regex)) { restaurant.price = parseInt(regex.exec(shop['priceText'])[2]); } else { restaurant.price = shop['priceText']; } restaurant.star = shop['shopPower'] / 10; restaurant.category = shop['categoryName']; restaurant.region = shop['regionName']; restaurants.push(restaurant); } }); var nextStart = data['nextStartIndex']; if (nextStart & start && nextStart & cityOptions.threshHold) { DianpingSpider(regionId, nextStart, function (err, restaurants2) { if (err) return callback(err); callback(null, restaurants.concat(restaurants2)) }); } else { callback(null, restaurants); } }); }
在调用爬虫函数时,采用async的mapLimit函数实现对并发的控制;采用async的until对并发的协同处理,保证三份数据结果的id一致性(不会因为并发完成时间不一致而丢数据):
DianpingSpider(0, 0, function (err, restaurants) { if (err) return console.err(err.stack); var concurrency = 0; var crawlMove = function (id, callback) { var delay = parseInt((Math.random() * ) % 1000, 10); concurrency++; console.log('current concurrency:', concurrency, ', now crawling id=', id, ', costs(ms):', delay); parseShop(id); parseMap(id); setTimeout(function () { concurrency--; callback(null, id); }, delay); }; async.mapLimit(restaurants, 5, function (restaurant, callback) { crawlMove(restaurant.id, callback) }, function (err, ids) { console.log('crawled ids:', ids); var resultArray = []; async.until( function () { return restaurants.length === Object.keys(ratingDict).length && restaurants.length === Object.keys(posDict).length }, function (callback) { setTimeout(function () { callback(null) }, 1000) }, function (err) { restaurants.forEach(function (restaurant) { var rating = ratingDict[restaurant.id]; var pos = posDict[restaurant.id]; var result = Object.assign(restaurant, rating, pos); resultArray.push(result); }); writeAsJson(resultArray); } ); }); });
其中,parseShop与parseMap分别为解析商家详情页、商家地图页:
function parseShop(id) { var shopBase = '/shop/%s'; var shopUrl = util.format(shopBase, id); superagent.get(shopUrl) .end(function (err, res) { if (err) return console.err(err.stack); console.log('crawling shop:', shopUrl); var restaurant = {}; var $ = cheerio.load(res.text); var desc = $("div.shopInfoPagelet & div.desc & span"); restaurant.taste = desc.eq(0).text().split(":")[1]; restaurant.surrounding = desc.eq(1).text().split(":")[1]; restaurant.service = desc.eq(2).text().split(":")[1]; ratingDict[id] = }); } function parseMap(id) { var mapBase = '/shop/%s/map'; var mapUrl = util.format(mapBase, id); superagent.get(mapUrl) .end(function (err, res) { if (err) return console.err(err.stack); console.log('crawling map:', mapUrl); var restaurant = {}; var $ = cheerio.load(res.text); var data = $("body & script").text(); var latRegex = /(.*lat:)(\d+.\d+)(.*)/; var lngRegex = /(.*lng:)(\d+.\d+)(.*)/; if(data.match(latRegex) && data.match(lngRegex)) { restaurant.latitude = latRegex.exec(data)[2]; restaurant.longitude = lngRegex.exec(data)[2]; }else { restaurant.latitude = ''; restaurant.longitude = ''; } posDict[id] = }); }
将array的每一个商家信息,逐行写入到json文件中:
function writeAsJson(arr) { fs.writeFile( 'data.json', arr.map(function (data) { return JSON.stringify(data); }).join('\n'), function (err) { if (err) return err. }) }
以上就是这篇文章的全部内容,希望本文能给学习或者使用node.js的朋友们带来一定的帮助,如果有疑问大家可以留言交流。
您可能感兴趣的文章:
大家感兴趣的内容
12345678910
最近更新的内容
常用在线小工具使用Node.js制作爬虫教程(续:爬图) - 简书
下载简书移动应用
写了80010字,被608人关注,获得了523个喜欢
使用Node.js制作爬虫教程(续:爬图)
使用Node.js制作爬虫教程(续:爬图)
前几天发了《》之后,有朋友问如果要爬文件怎么办,正好之前也写过类似的,那就直接拿过来写个续篇吧,有需要的可以借鉴,觉得不好的可以留言交流。
上一篇中,主要利用nodejs发起一个getData请求来得到4星角色的id列表。通过chrome开发者工具来查看页面结构,分析得出角色详细页面的URL规则和详细页面中想要抓取内容的位置。再循环遍历4星角色id列表去发起角色详细页面的请求并解析出想要收集的内容。
具体内容可再参考原文:
案例回顾中提到的角色详细页面(),有不少图片内容,本文就以抓取“主动技能”的GIF图片为例,来改造一下前文的代码以完成定向抓取图片的效果。
通过Chrome查看图片对象的URL规则为:/img/as2/角色id.gif
构建工程和引入框架
$npm install --save superagent
$npm install --save cheerio
$npm install --save async
上篇代码逻辑
发起getData.php请求,获得所有4星角色的ID
依次循环根据char/角色id规则访问各个角色的详细页面,并解析其中需要的数据并按我们想要的方式存储起来
本篇代码逻辑:
发起getData.php请求,获得所有4星角色的ID
依次循环根据/img/as2/角色id.gif规则下载gif文件到本地
所以,只要修改上篇代码中对每个角色对象的处理逻辑部分的内容为下载文件即可。
具体代码如下:
var superagent = require('superagent');
var cheerio = require('cheerio');
var async = require('async');
var fs = require('fs');
var request = require("request");
console.log('爬虫程序开始运行......');
// 第一步,发起getData请求,获取所有4星角色的列表
superagent
.post('/charSearch/function/getData.php')
// 请求的表单信息Form data
info: 'isempty',
star : [0,0,0,1,0],
job : [0,0,0,0,0,0,0,0],
type : [0,0,0,0,0,0,0],
phase : [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
cate : [0,0,0,0,0,0,0,0,0,0],
phases : ['初代', '第一期','第二期','第三期','第四期','第五期','第六期', '第七期','第八期','第九期','第十期','第十一期','第十二期','第十三期','第十四期', '第十五期', '第十六期'],
cates : ['活動限定','限定角色','聖誕限定','正月限定','黑貓限定','中川限定','茶熊限定','夏日限定'] })
// Http请求的Header信息
.set('Accept', 'application/json, text/javascript, */*; q=0.01')
.set('Content-Type','application/x-www-form- charset=UTF-8')
.end(function(err, res){
// 请求返回后的处理
// 将response中返回的结果转换成JSON对象
var heroes = JSON.parse(res.text);
// 并发遍历heroes对象
async.mapLimit(heroes, 5,
function (hero, callback) {
// 对每个角色对象的处理逻辑
var heroId = hero[0];
// 获取角色数据第一位的数据,即:角色id
fetchInfo(heroId, callback);
function (err, result) {
console.log('抓取的角色数:' + heroes.length);
// 获取角色信息
var concurrencyCount = 0; // 当前并发数记录
var as2Url = '/img/as2/';
var fetchInfo = function(heroId, callback){
// 下载链接
var url = as2Url + heroId + '.gif';
// 本地保存路径
var filepath = 'img/' + heroId + '.gif';
// 判断文件是否存在
fs.exists(filepath, function(exists) {
if (exists) {
// 文件已经存在不下载
console.log(filepath + ' is exists');
callback(null, 'exists');
// 文件不存在,开始下载文件
concurrencyCount++;
console.log('并发数:', concurrencyCount, ',正在抓取的是', url);
request.head(url, function(err, res, body){
if (err) {
console.log('err: '+ err);
callback(null, err);
request(url)
.pipe(fs.createWriteStream(filepath))
.on('close', function(){
console.log('Done : ', url);
concurrencyCount --;
callback(null, url);
主要修改内容:
对fs模块和request模块的引用,前者用来读写文件,后者用来通过http请求获取文件。
var fs = require('fs');
var request = require("request");
fetchInfo函数修改成拼接url和本地保存路径,并通过request进行下载。
由于图片下载较慢修改并发数,async.mapLimit(heroes, 5, function (hero, callback)
执行情况如下,根据配置的并发数5,可以看到如下输出
$ node index.js
爬虫程序开始运行......
并发数: 1 ,正在抓取的是 /img/as2/SS0441.gif
并发数: 2 ,正在抓取的是 /img/as2/NS1641.gif
并发数: 3 ,正在抓取的是 /img/as2/SS1141.gif
并发数: 4 ,正在抓取的是 /img/as2/SS1041.gif
并发数: 5 ,正在抓取的是 /img/as2/SS0941.gif
/img/as2/SS1141.gif
并发数: 5 ,正在抓取的是 /img/as2/SS0841.gif
/img/as2/SS0841.gif
并发数: 5 ,正在抓取的是 /img/as2/LS0941.gif
/img/as2/SS1041.gif
并发数: 5 ,正在抓取的是 /img/as2/LS1441.gif
/img/as2/NS1641.gif
并发数: 5 ,正在抓取的是 /img/as2/SS0741.gif
/img/as2/LS0941.gif
并发数: 5 ,正在抓取的是 /img/as2/SS0641.gif
示例到此结束,有需要的去爬爬爬,至于爬什么我就不负责啦,^_^
代码参考:
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮
请教作者抓图时为什么选择用request而没用superagent呢?另外request本身好像是个第三方模块吧?node文档没找到这个模块
被以下专题收入,发现更多相似内容:
如果你是程序员,或者有一颗喜欢写程序的心,喜欢分享技术干货、项目经验、程序员日常囧事等等,欢迎投稿《程序员》专题。
专题主编:小...
· 180471人关注
玩转简书的第一步,从这个专题开始。
想上首页热门榜么?好内容想被更多人看到么?来投稿吧!如果被拒也不要灰心哦~入选文章会进一个队...
· 131781人关注
关注互联网、科技。
**诚邀科技爱好者一起管理这个专题,私信我。
不定期会精选一些好的文章发在公众号(joojencom)...
· 29135人关注
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
选择支付方式:有没优雅的写法,让nodejs的回调+循环不那么操蛋?
nodejs回调和循环一起用,是非常痛苦的。 由于无法使用通常的for, while等控制流程,于是用类似递归的方式来“模拟” 循环控制。 实现起来就很操蛋,让编程少了很多乐趣。 async.js库的出现,让程序员在面对此类问题时,稍微舒适了一点, 但总没有原生的for, while好用和直接。 各位看官安有良策?:to anthonyive,想修改标题,题主不反对,麻烦别把意思改成正好相反好吗?原标题: 有没优雅的写法,让nodejs的回调+循环不那么操蛋?anthonyive改的标题: 有没优雅的写法,让nodejs的回调+循环不易使用?
1. Promise标准 可以解决回调地狱的问题2. js本身支持forEach,map和reduce,这三个都是历遍的意思,但返回结果不一样,forEach是什么都不返回,map是把历遍返回值形成一个数组,reduce是把前一个返回值导入和现在这个值放在一起处理,整个历遍后形成一个值。3.filter,历遍所有值把返回值(符合值)给保留下来4.看看underscore的库,里面有很多有用的工具,比如compose,
有f1(x),f2(x)和f3(x))三个函数,_.compose(f1,f2,f3)返回一个f1(f2(f3))的函数组合函数。5.按需求,使用特殊数据结构类型的数据,Hash,Map,Set,List,Array,Btree等等。6.找符合需求,可以简化代码的第三方库,7.多学学和使用js的设计模式
async/await+babel (注意不是async.js库)
有generator来优化异步流程的方法: 还有现成的库: 不过generator在nodejs中现在还处于测试版本中,还没有正式发布现有的解决方案,除了async,还可以用promise模式来优化异步流程,比如
coffeescript
前面一些回答很好了。解决类似问题的库还有:- - Promise 的API不错的。
async就够用了吧, 有很复杂的循环逻辑么?
这种问题不是应该去stack overflow上问更好吗?上面看到Async, Promise,这些都是当年常用的工具,不然callback真的会死人的!Async用起来也还是相对比较麻烦的,当然里面的waterfull(逐个任务依次执行),forEach(对数组里每一个对象执行),foreachSeries(依次对每一个对象执行),ofLimit(依次执行x个对象,逐批处理,很适合数据库任务批量读写)。Promise也是不错的选择。但相对来说,co更加直观,可以把他视为异步命令逐行执行,非常符合原来写Java,Python,C++的这些朋友, 如果你使用在新的项目,果断切到最新stable 版本引入 co,之后基本上可以按照Java,Python这样的感觉走了:const a = 'htps://';
const b = '';
let foo = (a, b) =& { // ES6 语法,函数声明:function(a, b) { } 等同于 (a, b) =& { }
return co(function*() {
let ra = yield asyncRequest(a); // yield 会等待这个异步任务执行完,然后return结果
let rb = yield asyncRequest(b);
return 'OK';
foo(a, b);
说实话,使用es6会让从java, C++ 等逐行执行语言转过来的伙伴感觉好很多,但是依然还是有些不便,比如有时候你真的不知道什么时候for循环就需要等待,有些就刚好不用,确实Egg Pain。经验上来说,Request,File IO,数据库操作,比较复杂的大型数据逻辑等都可以保险起见视为异步操作,需要专门做callback,promise或者yield之类的等待处理。希望之后JavaScript能够有更人性化的改进。既然你提到了逐个循环执行,那我就安利一下我的
这个包,原理就是,使用 co语法的 foreach,并且里面的任务是逐个执行,要知道 foreach 是有可能平行执行的,如果你需要逐个执行的话就选他了。这个包主要是支持 co语法,函数里面果断使用 yield 爽得很!npm install
--save 就ok了!怎么用? 谷歌
不出意外第一个,详见或者上有教程!
coES2016 typescript
async/await + Typescript/Babel
已有帐号?
无法登录?
社交帐号登录

我要回帖

更多关于 async.each 结束循环 的文章

 

随机推荐