博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Node.js大众点评爬虫
阅读量:5858 次
发布时间:2019-06-19

本文共 6532 字,大约阅读时间需要 21 分钟。

大众点评上有很多美食餐馆的信息,正好可以拿来练练手Node.js。

1. API分析

大众点评开放了查询商家信息的API,给出了城市与cityid之间的对应关系,链接http://m.api.dianping.com/searchshop.json?&regionid=0&start=0&categoryid=10&sortid=0&cityid=110以GET方式给出了餐馆的信息(JSON格式)。首先解释下GET参数的含义:

  • start为步进数,表示分步获取信息的index,与nextStartIndex字段相对应;
  • cityid表示城市id,比如,合肥对应于110;
  • regionid表示区域id,每一个id代表含义在start=0时rangeNavs字段中有解释;
  • categoryid表示搜索商家的分类id,比如,美食对应的id为10,具体每一个id的含义参见在start=0时categoryNavs字段;
  • 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

通过以上分析,确定爬取策略如下(与的思路相类似):

  1. 逐步爬取searchshop API的取商家基本信息列表;
  2. 通过爬取的所有商家的id,异步并发爬取评分信息、经纬度;
  3. 最后将三份数据通过id做聚合,输出成json文件。

2. 爬虫实现

Node.js爬虫代码用到如下的第三方模块:

  • ,轻量级http请求库,模仿了浏览器登录;
  • ,采用jQuery语法解析HTML元素,跟Python的PyQuery相类似;
  • ,牛逼闪闪的异步流程控制库,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 shopvar ratingDict = {}; // id -> ratingsvar 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?&regionid=%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);        }      });}

在调用爬虫函数时,采用async的mapLimit函数实现对并发的控制(代码参考);采用async的until对并发的协同处理,保证三份数据结果的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);        }    );  });});

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

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

转载地址:http://vrojx.baihongyu.com/

你可能感兴趣的文章
Miller-Rabin素数测试
查看>>
bzoj1226学校食堂[SDOI2009]
查看>>
提交Windows8应用之注意事项
查看>>
头像文件上传 方法一:from表单 方法二:ajax
查看>>
Ajax提交数据的data序列化数据提交即可
查看>>
WPF——TargetNullValue(如何在绑定空值显示默认字符)
查看>>
PHP限制IP访问 只允许指定IP访问 允许*号通配符过滤IP
查看>>
[转] Optimizely:在线网站A/B测试平台
查看>>
软件设计基础-C/S系统
查看>>
Nginx反向代理以及负载均衡配置
查看>>
常用网络设备
查看>>
ServletContext
查看>>
cookie与session
查看>>
1041 Be Unique
查看>>
第三次实训作业
查看>>
jeecg中excel导出字段判空处理
查看>>
浅谈移动端混合开发
查看>>
虚拟 DOM 到底是什么?
查看>>
PLSQL DEVELOPER 连接远程数据库 OCI客户端安装方法
查看>>
算法模板——线段树3(区间覆盖值+区间求和)
查看>>