Node.js实战:构建大众点评数据爬虫
本文详解如何利用Node.js并发特性爬取大众点评数据。通过解析searchshop API获取基础列表,结合异步库async控制并发,分别抓取详情页与地图页以补全评分及经纬度信息,最终聚合输出JSON文件。
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代表步进数,用于分步获取信息的索引,与nextStartIndex字段对应;
2、cityid表示城市ID,例如合肥对应的ID为110;
3、regionid表示区域ID,每个ID的具体含义在start=0时的rangeNavs字段中有详细说明;
4、categoryid表示搜索商家的分类ID,例如美食对应的ID为10,各类别ID的具体含义可参考start=0时的categoryNavs字段;
5、sortid表示商家结果的排序方式,例如0代表智能排序,2代表评价最好,各类别ID的具体含义可参考start=0时的sortNavs字段。
在GET返回的JSON字符串中,list字段包含商家列表,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); } }); }
在调用爬虫函数时,采用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的朋友们带来一定的帮助,如果有疑问大家可以留言交流。
Java和JavaScript应用场景对比:企业级后端 vs Web前端与Node.js
2026-11-04 16:42:01
JavaScript模块化开发入门:CommonJS与ES Module的区别
2026-11-03 23:31:34
怎么创建并运行第一个js文件
2026-11-03 21:49:02
JavaScript运行环境怎么搭建?Node.js官方下载与安装教程
2026-11-03 20:03:04
NestJS 集成 TypeORM 或 Prisma 实现数据库持久化操作
2026-11-03 18:56:13
