利用Node.js制作爬取大众点评的爬虫


Posted in Javascript onSeptember 22, 2016

前言

Node.js天生支持并发,但是对于习惯了顺序编程的人,一开始会对Node.js不适应,比如,变量作用域是函数块式的(与C、Java不一样);for循环体({})内引用i的值实际上是循环结束之后的值,因而引起各种undefined的问题;嵌套函数时,内层函数的变量并不能及时传导到外层(因为是异步)等等。

一、 API分析

大众点评开放了查询餐馆信息的API,这里给出了城市与cityid之间的对应关系,

链接:http://m.api.dianping.com/searchshop.json?®ionid=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=0rangeNavs字段中有解释;

     4、categoryid表示搜索商家的分类id,比如,美食对应的id为10,具体每一个id的含义参见在start=0categoryNavs字段;

     5、sortid表示商家结果的排序方式,比如,0对应智能排序,2对应评价最好,具体每一个id的含义参见在start=0时sortNavs字段。

在GET返回的JSON串中list字段为商家列表,id表示商家的id,作为商家的唯一标识。在返回的JSON串中是没有商家的口味、环境、服务的评分信息以及经纬度的;

      因而我们还需要爬取两个商家页面:http://m.dianping.com/shop/<id>、http://m.dianping.com/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, 8840, 354, 8839, 8841, 8843, 358, -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) { return true; } else { idVisited[id] = true; return false; } }

采取回调函数的方式,实现顺序逐步地递归调用爬虫函数:

function DianpingSpider(regionId, start, callback) { console.log('crawling region=', regionId, ', start =', start); var searchBase = 'http://m.api.dianping.com/searchshop.json?®ionid=%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); } }); }

在调用爬虫函数时,采用asyncmapLimit函数实现对并发的控制;采用asyncuntil对并发的协同处理,保证三份数据结果的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() * 30000000) % 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); } ); }); });

其中,parseShopparseMap分别为解析商家详情页、商家地图页:

function parseShop(id) { var shopBase = 'http://m.dianping.com/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] = restaurant; }); } function parseMap(id) { var mapBase = 'http://m.dianping.com/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] = restaurant; }); }

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.stack; }) }

总结

以上就是这篇文章的全部内容,希望本文能给学习或者使用node.js的朋友们带来一定的帮助,如果有疑问大家可以留言交流。

Javascript 相关文章推荐
javascript 学习之旅 (3)
Feb 05 Javascript
Javascript笔记一 js以及json基础使用说明
May 22 Javascript
js中关于String对象的replace使用详解
May 24 Javascript
《JavaScript高级程序设计》阅读笔记(一) ECMAScript基础
Feb 27 Javascript
JavaScript实现给定时间相加天数的方法
Jan 25 Javascript
两行代码轻松搞定JavaScript日期验证
Aug 03 Javascript
AngularJs html compiler详解及示例代码
Sep 01 Javascript
AngularJS中如何使用echart插件示例详解
Oct 26 Javascript
详解jQuery选择器
Dec 21 Javascript
bootstrap轮播图示例代码分享
May 17 Javascript
javascript 判断用户有没有操作页面
Oct 17 Javascript
VUE项目实现主题切换的多种方法
Nov 26 Vue.js
JavaScript与java语言有什么不同
Sep 22 #Javascript
JavaScript中数组slice和splice的对比小结
Sep 22 #Javascript
深入理解JavaScript中的并行处理
Sep 22 #Javascript
Actionscript与javascript交互实例程序(修改)
Sep 22 #Javascript
Javascript 调用 ActionScript 的简单方法
Sep 22 #Javascript
JavaScript与ActionScript3两者的同性与差异性
Sep 22 #Javascript
ionic由于使用了header和subheader导致被遮挡的问题的两种解决方法
Sep 22 #Javascript
You might like
关于PHP中的Class的几点个人看法
2006/10/09 PHP
php析构函数的简单使用说明
2015/08/24 PHP
PHP判断字符串长度的两种方法很实用
2015/09/22 PHP
jQuery.Validate 使用笔记(jQuery Validation范例 )
2010/06/25 Javascript
jQuery新闻滚动插件 jquery.roller.js
2011/06/27 Javascript
关于Jquery操作Cookie取值错误的解决方法
2013/08/26 Javascript
JS操作Cookie写入和读取实例代码
2013/10/20 Javascript
JS对象与json字符串格式转换实例
2014/10/28 Javascript
node.js中的fs.openSync方法使用说明
2014/12/17 Javascript
javascript实现博客园页面右下角返回顶部按钮
2015/02/22 Javascript
JavaScript使用shift方法移除素组第一个元素实例分析
2015/04/06 Javascript
高性能JavaScript 重排与重绘(2)
2015/08/11 Javascript
简单介绍jsonp 使用小结
2016/01/27 Javascript
jQuery取消特定的click事件
2016/02/29 Javascript
原生js代码实现图片放大境效果
2016/10/30 Javascript
使用canvas进行图像编辑的实例
2017/08/29 Javascript
初探JavaScript 面向对象(推荐)
2017/09/03 Javascript
浅谈webpack 构建性能优化策略小结
2018/06/13 Javascript
vue组件横向树实现代码
2018/08/02 Javascript
搭建Vue从Vue-cli到router路由护卫的实现
2019/11/14 Javascript
[50:12]EG vs Fnatic 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
[04:09]2018年度DOTA2社区贡献奖-完美盛典
2018/12/16 DOTA
Python2.7简单连接与操作MySQL的方法
2016/04/27 Python
Python简单删除列表中相同元素的方法示例
2017/06/12 Python
Python2.7 实现引入自己写的类方法
2018/04/29 Python
python实现朴素贝叶斯算法
2018/11/19 Python
Python BeautifulSoup [解决方法] TypeError: list indices must be integers or slices, not str
2019/08/07 Python
python字符串替换re.sub()方法解析
2019/09/18 Python
Python3.9又更新了:dict内置新功能
2020/02/28 Python
英国在线自行车店:Merlin Cycles
2018/08/20 全球购物
大四自我鉴定范文
2013/10/06 职场文书
师范毕业生个人求职信
2013/12/09 职场文书
个人实习生的自我评价
2014/02/16 职场文书
2014年清明节寄语
2014/04/03 职场文书
三好学生事迹材料
2014/12/24 职场文书
2015年高三年级组工作总结
2015/07/21 职场文书