利用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 相关文章推荐
jQuery的链式调用浅析
Dec 03 Javascript
brook javascript框架介绍
Oct 10 Javascript
jquery按回车提交数据的代码示例
Nov 05 Javascript
常见的jQuery选择器汇总
Nov 24 Javascript
JQuery中使用on方法绑定hover事件实例
Dec 09 Javascript
黑帽seo劫持程序,js劫持搜索引擎代码
Sep 15 Javascript
JQuery移动页面开发之屏幕方向改变与滚屏的实现
Dec 03 Javascript
Bootstrap项目实战之子栏目资讯内容
Apr 25 Javascript
详解vue渲染从后台获取的json数据
Jul 06 Javascript
vue-resource:jsonp请求百度搜索的接口示例
Nov 09 Javascript
taro小程序添加骨架屏的实现代码
Nov 15 Javascript
小程序中的箭头函数的具体使用
Jun 19 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下正则来匹配dede模板标签的代码
2010/08/21 PHP
PHP开发者常犯的10个MySQL错误更正剖析
2012/01/30 PHP
yii2多图上传组件的使用教程
2018/05/10 PHP
编写Js代码要注意的几条规则
2010/09/10 Javascript
js控制输入框获得和失去焦点时状态显示的方法
2015/01/30 Javascript
推荐10 款 SVG 动画的 JavaScript 库
2015/03/24 Javascript
原生js实现类似弹窗抖动效果
2015/04/02 Javascript
JQuery插件Quicksand实现超炫的动画洗牌效果
2015/05/03 Javascript
jquery获取select选中值的方法分析
2015/12/22 Javascript
第二篇Bootstrap起步
2016/06/21 Javascript
nodejs Assert中equal(),strictEqual(),deepEqual(),strictDeepEqual()比较
2017/09/18 NodeJs
es6中的解构赋值、扩展运算符和rest参数使用详解
2017/09/28 Javascript
详解react-native WebView 返回处理(非回调方法可解决)
2018/02/27 Javascript
vue脚手架搭建项目的兼容性配置详解
2018/07/17 Javascript
Mint UI组件库CheckList使用及踩坑总结
2018/12/20 Javascript
webpack4之如何编写loader的方法步骤
2019/06/06 Javascript
浅谈TypeScript 用 Webpack/ts-node 运行的配置记录
2019/10/11 Javascript
JavaScript中的各种宽高属性的实现
2020/05/08 Javascript
利用Vue的v-for和v-bind实现列表颜色切换
2020/07/17 Javascript
如何在vue中使用HTML 5 拖放API
2021/01/14 Vue.js
[58:37]Serenity vs Fnatic 2018国际邀请赛淘汰赛BO1 8.21
2018/08/22 DOTA
python遍历序列enumerate函数浅析
2017/10/17 Python
python中文乱码不着急,先看懂字节和字符
2017/12/20 Python
对python中使用requests模块参数编码的不同处理方法
2018/05/18 Python
django框架使用方法详解
2019/07/18 Python
Python importlib动态导入模块实现代码
2020/04/16 Python
Python实现将元组中的元素作为参数传入函数的操作
2020/06/05 Python
Python编写万花尺图案实例
2021/01/03 Python
教师学习培训邀请函
2014/02/04 职场文书
大学校务公开实施方案
2014/03/31 职场文书
班主任班级寄语大全
2014/04/04 职场文书
大班上学期幼儿评语
2014/04/30 职场文书
焦点访谈观后感
2015/06/11 职场文书
比赛口号霸气押韵
2015/12/24 职场文书
Spring中bean的生命周期之getSingleton方法
2021/06/30 Java/Android
java多态注意项小结
2021/10/16 Java/Android