利用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 相关文章推荐
用js+xml自动生成表格的东西
Dec 21 Javascript
js前台判断开始时间是否小于结束时间
Feb 23 Javascript
jquery mobile实现拨打电话功能的几种方法
Aug 05 Javascript
jquery判断复选框选中状态以及区分attr和prop
Dec 18 Javascript
JS实现的几个常用算法
Nov 12 Javascript
探讨跨域请求资源的几种方式(总结)
Dec 02 Javascript
使用jsonp实现跨域获取数据实例讲解
Dec 25 Javascript
如何写好你的JavaScript【推荐】
Mar 02 Javascript
利用n工具轻松管理Node.js的版本
Apr 21 Javascript
jquery应用实例分享_实现手风琴特效
Feb 01 jQuery
vue+elementUI动态生成面包屑导航教程
Nov 04 Javascript
Angular封装表单控件及思想总结
Dec 11 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中trim()函数简单使用指南
2015/04/16 PHP
php生成复杂验证码(倾斜,正弦干扰线,黏贴,旋转)
2018/03/12 PHP
Laravel 修改默认日志文件名称和位置的例子
2019/10/17 PHP
PHP获取当前时间不准确问题解决方案
2020/08/14 PHP
jQuery Mobile的loading对话框显示/隐藏方法分享
2013/11/26 Javascript
js 验证身份证信息有效性
2014/03/28 Javascript
javascript实现查找数组中最大值方法汇总
2016/02/13 Javascript
基于jQuery日历插件制作日历
2016/03/11 Javascript
轻松掌握JavaScript单例模式
2016/08/25 Javascript
10分钟掌握XML、JSON及其解析
2020/12/06 Javascript
原生js实现对Ajax的封装(仿jquery)
2017/01/22 Javascript
移动端触屏幻灯片图片切换插件idangerous swiper.js
2017/04/10 Javascript
jquery实现图片轮播器
2017/05/23 jQuery
JavaScript实现学生在线做题计时器功能
2018/12/05 Javascript
vue项目打包后怎样优雅的解决跨域
2019/05/26 Javascript
Python 字符串操作实现代码(截取/替换/查找/分割)
2013/06/08 Python
python中的lambda表达式用法详解
2016/06/22 Python
浅谈python中的正则表达式(re模块)
2017/10/17 Python
python3.6 +tkinter GUI编程 实现界面化的文本处理工具(推荐)
2017/12/20 Python
Python模块的制作方法实例分析
2019/12/21 Python
Pytorch实现各种2d卷积示例
2019/12/30 Python
大数据分析用java还是Python
2020/07/06 Python
美国杰西潘尼官网:JCPenney
2019/06/12 全球购物
美国孕妇装购物网站:Motherhood Maternity
2019/09/22 全球购物
mysql有关权限的表都有哪几个
2015/04/22 面试题
幼儿园五一活动方案
2014/02/07 职场文书
环境工程专业自荐信
2014/03/03 职场文书
教师竞聘演讲稿
2014/05/16 职场文书
社团活动总结怎么写
2014/06/30 职场文书
小学数学教研活动总结
2014/07/01 职场文书
2014年教学工作总结
2014/11/13 职场文书
车间班组长竞聘书
2015/09/15 职场文书
2016年春节慰问信息大全
2015/11/30 职场文书
HTML基础-标签分类(闭合标签,空标签,块级元素,行内元素,行级块元素,可替换元素)
2021/03/31 HTML / CSS
php 解析非标准json、非规范json
2021/04/01 PHP
Java版 简易五子棋小游戏
2022/05/04 Java/Android