利用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 精粹读书笔记(1,2)
Feb 07 Javascript
Jquery中使用setInterval和setTimeout的方法
Apr 08 Javascript
jquery $.trim()方法使用介绍
May 21 Javascript
js如何判断访问是来自搜索引擎(蜘蛛人)还是直接访问
Sep 14 Javascript
通用javascript代码判断版本号是否在版本范围之间
Nov 29 Javascript
jquery实现倒计时效果
Dec 14 Javascript
详解MVC如何使用开源分页插件(shenniu.pager.js)
Dec 16 Javascript
bootstrap datetimepicker日期插件超详细使用方法介绍
Feb 23 Javascript
JavaScript编写一个贪吃蛇游戏
Mar 09 Javascript
从零开始最小实现react服务器渲染详解
Jan 26 Javascript
在HTML文档中嵌入JavaScript的四种方法
May 07 Javascript
Axios取消重复请求的方法实例详解
Jun 15 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
利用Ffmpeg获得flv视频缩略图和视频时间的代码
2011/09/15 PHP
Apache无法自动跳转却显示目录的解决方法
2020/11/30 PHP
js 小数取整的函数
2010/05/10 Javascript
使用滤镜设置透明导致 IE 6/7/8/9 解析异常的解决方法
2011/04/07 Javascript
TextArea设置MaxLength属性最大输入值的js代码
2012/12/21 Javascript
使用jQueryMobile实现滑动翻页效果的方法
2015/02/04 Javascript
JavaScript 随机验证码的生成实例代码
2016/09/22 Javascript
JavaScript 实现的checkbox经典实例分享
2016/10/16 Javascript
谈谈Vue.js——vue-resource全攻略
2017/01/16 Javascript
jQuery插件FusionCharts实现的MSBar2D图效果示例【附demo源码】
2017/03/24 jQuery
JS FormData上传文件的设置方法
2017/07/05 Javascript
JavaScript生成简单等差数列
2017/11/28 Javascript
Vue 源码分析之 Observer实现过程
2018/03/29 Javascript
Vue 实现拖动滑块验证功能(只有css+js没有后台验证步骤)
2018/08/24 Javascript
微信小程序实用代码段(收藏版)
2019/12/17 Javascript
[34:56]Ti4冒泡赛LGD vs Liquid 1
2014/07/14 DOTA
[54:33]2018DOTA2亚洲邀请赛小组赛 A组加赛 Liquid vs Optic
2018/04/03 DOTA
python通过pil模块将raw图片转换成png图片的方法
2015/03/16 Python
python以环状形式组合排列图片并输出的方法
2015/03/17 Python
实例讲解Python中函数的调用与定义
2016/03/14 Python
python对象及面向对象技术详解
2016/07/19 Python
TensorFlow入门使用 tf.train.Saver()保存模型
2018/04/24 Python
Python迭代器与生成器用法实例分析
2018/07/09 Python
Python如何处理大数据?3个技巧效率提升攻略(推荐)
2019/04/15 Python
jupyter notebook指定启动目录的方法
2021/03/02 Python
社团活动总结范文
2014/04/26 职场文书
计算机专业自荐信
2014/05/24 职场文书
教师个人培训总结
2015/02/11 职场文书
2015年后勤工作总结范文
2015/04/08 职场文书
2015年文秘个人工作总结
2015/10/14 职场文书
2015年评职称个人工作总结
2015/10/15 职场文书
实验心得体会范文
2016/01/25 职场文书
PyCharm 安装与使用配置教程(windows,mac通用)
2021/05/12 Python
Linux7.6二进制安装Mysql8.0.27详细操作步骤
2021/11/27 MySQL
《黑岩★★射手 DAWN FALL》BD发售宣传CM公开
2022/04/04 日漫
Tomcat弱口令复现及利用
2022/05/06 Servers