Node.jsを利用して大衆の評価を取る爬虫類を作る

7361 ワード

前言
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=0時のrangeNavsフィールドに解釈される意味を表す.
     4、categoryidは検索業者の分類idを表し、例えば、美食に対応するidは10であり、具体的なidの意味はstart=0categoryNavsフィールドを参照する.
     5、sortidは事業者の結果のソート方式を表し、例えば、0はスマートソートに対応し、2は評価が最も良く、具体的なidの意味はstart=0でsortNavsフィールドを参照する.
GETで返されるJSON列ではlistフィールドが商家リストであり、idは商家のidを表し、商家の唯一の標識となる.戻ってきたJSON列には、業者の味、環境、サービスの採点情報、経緯度はありません.
      2つの企業のページを登る必要がありますhttp://m.dianping.com/shop/、http://m.dianping.com/shop//map.
以上の分析により、這い出し戦略は以下のように決定された(dianping_crawlerの考え方と類似している).
      1、searchshop APIの取得業者の基本情報リストを徐々に登る.
      2、這い出したすべての業者のidを通じて、非同期で採点情報、経緯度を同時に這い出す.
      3、最後に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

1つの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); } }); }

爬虫類関数を呼び出す際、asyncmapLimit関数を用いて同時制御を実現する.asyncuntil対の同時の協同処理を採用し、3部のデータ結果の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('
'), function (err) { if (err) return err.stack; }) }

まとめ
以上がこの文章のすべての内容で、本文がnode.jsを勉強したり使ったりする友达に一定の助けをもたらすことを望んで、もし疑問があればみんなは伝言を残して交流することができます.