利用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 相关文章推荐
Prototype使用指南之string.js
Jan 10 Javascript
解析js中获得父窗口链接getParent方法以及各种打开窗口的方法
Jun 19 Javascript
jQuery 获取/设置/删除DOM元素的属性以a元素为例
May 23 Javascript
原生js实现复制对象、扩展对象 类似jquery中的extend()方法
Aug 30 Javascript
jQuery中:text选择器用法实例
Jan 03 Javascript
jquery插件uploadify实现带进度条的文件批量上传
Dec 13 Javascript
js实现砖头在页面拖拉效果
Nov 20 Javascript
原生js实现吸顶效果
Mar 13 Javascript
详解vue2 $watch要注意的问题
Sep 08 Javascript
深入理解Vue Computed计算属性原理
May 29 Javascript
Vue+Element实现网页版个人简历系统(推荐)
Dec 31 Javascript
JS可断点续传文件上传实现代码解析
Jul 30 Javascript
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中文件上传的一个问题
2010/09/04 PHP
php下载文件源代码(强制任意文件格式下载)
2014/05/09 PHP
PHP使用strtotime获取上个月、下个月、本月的日期
2015/12/30 PHP
jquery text,radio,checkbox,select操作实现代码
2009/07/09 Javascript
浅说js变量
2011/05/25 Javascript
jQuery(1.6.3) 中css方法对浮动的实现缺陷分析
2011/09/09 Javascript
JavaScript 事件绑定及深入
2015/04/13 Javascript
纯JS实现本地图片预览的方法
2015/07/31 Javascript
javascript实现label标签跳出循环操作
2016/03/06 Javascript
微信小程序开发之toast提示插件使用示例
2017/06/08 Javascript
妙用Angularjs实现表格按指定列排序
2017/06/23 Javascript
Vue.js实现一个todo-list的上移下移删除功能
2017/06/26 Javascript
javascript 作用于作用域链的详解
2017/09/27 Javascript
详解plotly.js 绘图库入门使用教程
2018/02/23 Javascript
JavaScript生成指定范围的时间列表
2018/03/19 Javascript
JS实现图片切换效果
2018/11/17 Javascript
vue 检测用户上传图片宽高的方法
2020/02/06 Javascript
微信小程序实现限制用户转发功能的实例代码
2020/02/22 Javascript
JavaScript实现点击出现子菜单效果
2021/02/08 Javascript
Python实现数据库编程方法详解
2015/06/09 Python
TensorFlow神经网络优化策略学习
2018/03/09 Python
Python符号计算之实现函数极限的方法
2019/07/15 Python
python 动态迁移solr数据过程解析
2019/09/04 Python
python随机生成库faker库api实例详解
2019/11/28 Python
python+Django+pycharm+mysql 搭建首个web项目详解
2019/11/29 Python
解决Pycharm中恢复被exclude的项目问题(pycharm source root)
2020/02/14 Python
Python编写单元测试代码实例
2020/09/10 Python
使用canvas实现黑客帝国数字雨效果
2020/01/02 HTML / CSS
蔻驰意大利官网:COACH意大利
2019/01/16 全球购物
早会主持词
2014/03/17 职场文书
新年联欢会主持词
2014/03/27 职场文书
心理健康日活动总结
2014/05/08 职场文书
乡镇党委书记个人整改措施
2014/09/15 职场文书
财务负责人岗位职责
2015/02/03 职场文书
详解php中流行的rpc框架
2021/05/29 PHP
Nginx HTTP跳转至HTTPS
2022/05/15 Servers