上學的時候本身寫過一些爬蟲代碼,比較簡陋,基於HttpRequest請求獲取地址返回的信息,再根據正則表達式抓取想要的內容。那時候爬的網站大多都是靜態的,直接獲取直接爬便可,並且也沒有什麼限制。可是如今網站的安全愈來愈完善,各類機器識別,打碼,爬蟲也要愈來愈只能才行了。html
前段時間有需求要簡單爬取美團商家的數據,作了一些分析,實踐,在這裏總結分享。node
一、城市大全能夠很容易的在這個頁面爬出來 http://www.meituan.com/index/changecity/initiative
二、每一個城市一個地址,例如深圳:http://sz.meituan.com/category/meishi
三、能夠按照分類、區域、人數來分類
四、商家列表是動態JS加載的,而且會有不少頁數
五、根據商家列表再進入商家詳情獲取數據python
這樣爬取流程即爲
一、進去城市美食頁
二、抓取分類,循環選擇分類
三、抓取區域,循環選擇區域
四、抓取人數,循環選擇人數
五、判斷是否有下一頁按鈕,循環進入下一頁
六、進入詳情頁抓取,提交以後continuegit
CREATE TABLE `test_mt` ( `Id` int(11) NOT NULL AUTO_INCREMENT, `city` varchar(10) NOT NULL DEFAULT '' COMMENT '城市', `cate` varchar(15) NOT NULL DEFAULT '' COMMENT '分類', `area` varchar(15) NOT NULL DEFAULT '' COMMENT '區域', `poi` varchar(15) NOT NULL DEFAULT '' COMMENT '商圈', `name` varchar(30) NOT NULL DEFAULT '' COMMENT '店名', `addr` varchar(50) NOT NULL DEFAULT '' COMMENT '地址', `tel` varchar(30) NOT NULL DEFAULT '' COMMENT '聯繫方式', `rj` int(11) NOT NULL DEFAULT '0' COMMENT '人均', `rate` float(2,1) NOT NULL DEFAULT '0.0' COMMENT '評價', `rate_count` int(11) NOT NULL DEFAULT '0' COMMENT '評價數', `recom_food` varchar(512) NOT NULL DEFAULT '' COMMENT '特點菜', `desc` varchar(512) NOT NULL DEFAULT '' COMMENT '門店介紹', PRIMARY KEY (`Id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
爲了快速實現功能,直接找現有的開源工具,說到爬蟲,python數一數二,因此先以此嘗試github
https://github.com/binux/pyspider
最開始嘗試了pysipder,主要是由於國人寫的,先支持國產,準確的說pyspider已是一個很是強大的爬蟲框架了,具體內容官網查看,通過試用以後,感受有一些殺雞用牛刀的感受,再來pyspider默認只支持抓取靜態頁,對於js加載的美團列表,難度就大了不少,中間嘗試考慮經過獲取cookie,模擬接口操做,可是協議解析起麻煩又耗時,坑確定又多,就放棄了。web
pyspider的js加載是經過配合phantomjs實現的,可是聽說有內存泄露的問題,要不按期的重啓phantomjs,試用以後發現並不很順手(也許是python不熟),特別是模擬點擊操做很麻煩致使回調地獄的出現,因此考慮再多試用幾款工具選型。正則表達式
https://scrapy.org/
20K的star表明了它的強大,試用過程當中發現和pysipder大同小異,遇到的問題也大同小異,也跳過了。sql
https://github.com/segmentio/nightmare
以前用的時候git上star貌似還很少,目前已經13K了,準確的說nightmare一款基於electron(曾經使用PhantomJS,後面改用Electron)的高度封裝web自動化測試工具,固然也能夠用來作一些簡易的爬蟲,api及其簡單,缺點就是模擬真人操做,這樣的爬蟲效率很是低,可是對於高安全性的網站來講,這樣的操做也最爲安全,防止被封。json
通過試用以後,最後決定採用nightmare進行爬蟲。api
須要注意的是evaluate函數返回的是promise,即爲異步回調函數,能夠配合co庫,進行yield操做,便可用同步模式進行異步操做。
var Nightmare = require('nightmare'), nightmare = Nightmare(), co = require('co'); var run = function*(){ var results = []; for(var i=0; i<5; i++){ var result = yield nightmare.goto('http://example.org').evaluate(function(){ return 123;}); results.push(result); } return results; } co(run).then(function(results){ console.dir(results); console.log('done'); });
nightmare是模擬操做,至關於開了個瀏覽器,因此這都不是什麼問題,但須要注意的是,列表若是數量太多,實際上是分頁加載的,第一次只加載十個,下滑再加載10個,加載完整頁以後,還有下一頁按鈕,至於當前頁的分頁,須要滑動到低,纔會加載完,因此還須要模擬滑動操做scrollTo,滑動過程當中動態加載數據,有時候會有不成功的狀況(多是因爲官方限制?),因此偶爾會漏過一些商家。
爲了不異常致使從頭開始爬,能夠每次在catch的時候保存當前的狀態值,下次啓動讀取,而後接着爬便可。
//記錄執行步驟,中斷後下次繼續 //分類 let stepLog = { cate_index: 0, //區域 location_index: 8, //商圈 area_index: 0, } function writeLog (log) { fs.writeFileSync('./steplog.log', JSON.stringify(log)); } function readLog () { let json = fs.readFileSync('./steplog.log'); return JSON.parse(json); }
一、爬蟲檢測限制很是嚴格,搞很差就403,隔天才恢復,間隔時間很重要,示例代碼的參數已經比較穩定。
二、數據基本上都是動態加載,加載接口又要cookie,又要post首次加載的各類參數,這也是爲何難爬,以前考慮過用PhantomJS,可是API過於複雜,無界面,不方便調試,nightmare基於electron這方面簡直是神器,模擬人操做,又防封,又便捷。
三、nightmare不會記錄cookie,因此若是有時候爬久了,關閉再開會403,可是瀏覽器正常,是cookie致使的,能夠訪問一些美團其餘頁面,先加載cookie再跳轉到須要爬的頁面便可。
四、因爲默認狀況不會記錄cookie,因此須要的話能夠再結束的時候getcookie序列化成json保存成文件,下次開啓的時候再進行初始化。
五、中斷繼續,也能夠把各類狀態參數序列化成json保存,下次啓動初始化,便可從中斷的地方繼續開始。
六、能夠不須要正則,直接用dom選擇器進行html元素查詢。
七、效率確實不高,但也沒啥好辦法,爬一個城市大概花4-5天。
注意post提交服務器地址改成本身的接口,若是須要保存本地,需自行處理。
代碼保存直接運行便可 node **.js
。
PS:中間變量命名有些隨意,請見諒。
var Nightmare = require('nightmare'); var fs = require('fs'); //nightmare = Nightmare({ show: true }), var co = require('co'); var http = require('http'); var city = '南昌'; var run = function* () { let nightmare = Nightmare({ show: true, waitTimeout: 60000 }); //記錄執行步驟,中斷後下次繼續 //分類 let stepLog = { cate_index: 0, //區域 location_index: 8, //商圈 area_index: 0, } if (fs.existsSync('./steplog.log')) { stepLog = readLog(); } //writeLog(stepLog); //獲取地區美食分類,先進主頁是爲了獲取cookie防止被封 let result1 = yield nightmare.goto('http://nc.meituan.com/').wait(5000) .goto('http://nc.meituan.com/category/meishi') .wait('div.filter-label-list.filter-section.category-filter-wrapper.first-filter ul.inline-block-list') .evaluate(function () { let arr_a = document.querySelectorAll('div.filter-label-list.filter-section.category-filter-wrapper.first-filter ul.inline-block-list li a'); let str = ''; //過濾所有,代金券 for (var index = 2; index < arr_a.length; index++) { var element = arr_a[index]; str += element.href + ','; } return str; }) let arr_a1 = result1.split(','); console.log(arr_a1); var temp_index1 = stepLog.cate_index; for (var index1 = temp_index1; index1 < arr_a1.length; index1++) { stepLog.cate_index = index1; //獲取美食分類以後,獲取地區 var element1 = arr_a1[index1]; if (element1 != '') { try { let result2 = yield nightmare .wait(10000) .goto(element1) .wait('ul.inline-block-list.J-filter-list.filter-list--fold') .evaluate(function () { let arr_a = document.querySelectorAll('ul.inline-block-list.J-filter-list.filter-list--fold li a'); let str = ''; //過濾所有,地鐵2 for (var index = 2; index < arr_a.length; index++) { var element = arr_a[index]; str += element.href + ','; } return str }); let arr_a2 = result2.split(','); console.log(arr_a2); var temp_index2 = stepLog.location_index; for (var index2 = temp_index2; index2 < arr_a2.length; index2++) { stepLog.location_index = index2; //獲取地區以後,獲取商圈 var element2 = arr_a2[index2]; if (element2 != '') { try { let result3 = yield nightmare .wait(10000) .goto(element2) .wait('ul.inline-block-list.J-area-block') .evaluate(function () { let arr_a = document.querySelectorAll('ul.inline-block-list.J-area-block li a'); let str = ''; //商圈下標,過濾所有 for (var index = 1; index < arr_a.length; index++) { var element = arr_a[index]; str += element.href + ','; } return str }); arr_a3 = result3.split(','); console.log(arr_a3); var temp_index3 = stepLog.area_index; for (var index3 = temp_index3; index3 < arr_a3.length; index3++) { stepLog.area_index = index3; //獲取商圈店鋪信息 var element3 = arr_a3[index3]; if (element3 != '') { let nextPage = 'undefined'; do { let url = ''; if (nextPage == 'undefined') url = element3; else url = nextPage; try { let result4 = yield nightmare .wait(10000) .goto(url) .wait('#content').wait(2000) .scrollTo(716, 0).wait(5000).scrollTo(716 * 2, 0).wait(5000).scrollTo(716 * 3, 0).wait(5000).scrollTo(716 * 4, 0).wait(5000).scrollTo(716 * 5, 0).wait(5000).scrollTo(716 * 6, 0).wait(5000).scrollTo(716 * 7, 0).wait(5000).scrollTo(716 * 8, 0).wait(5000).scrollTo(716 * 9, 0).wait(5000).scrollTo(716 * 10, 0).wait(5000).scrollTo(716 * 11, 0).wait(5000).scrollTo(716 * 12, 0).wait(5000).scrollTo(716 * 13, 0).wait(5000).scrollTo(716 * 14, 0).wait(5000).scrollTo(716 * 15, 0).wait(5000).scrollTo(716 * 16, 0).wait(5000).scrollTo(716 * 17, 0).wait(5000).scrollTo(716 * 18, 0).wait(5000).scrollTo(716 * 19, 0).wait(5000).scrollTo(716 * 20, 0).wait(5000) .evaluate(function () { let arr_a = document.querySelectorAll('div.poi-tile-nodeal'); let str = ''; for (var index = 0; index < arr_a.length; index++) { var element = arr_a[index]; let sp_rj = element.querySelector('div.poi-tile__money span.avg span'); //人均 let rj = 0; if (sp_rj != null) { let str_rj = sp_rj.innerText; rj = parseInt(str_rj.substr(1, rj.length)); } console.log(index); let url = ''; let elelink = element.querySelector('a.poi-tile__head.J-mtad-link'); if (elelink != null) { //連接地址 url = elelink.href; console.log(url); } str += url + '|' + rj + ','; } let href = document.querySelector('li.next a') ? document.querySelector('li.next a').href : 'undefined' return str + '^' + href; }); console.log(result4); temp4 = result4.split('^'); nextPage = temp4[1]; arr_a4 = temp4[0].split(','); for (var index4 = 0; index4 < arr_a4.length; index4++) { var element4 = arr_a4[index4]; if (element4 != '') { try { let temp = element4.split('|'); let url5 = ''; if (temp[0] != '') { url5 = temp[0]; } else { continue; } //獲取店鋪詳細信息 let result5 = yield nightmare .wait(5000) .goto(url5) .wait('div.poi-section.poi-section--shop') .evaluate(function () { let query = document.querySelectorAll('div.component-bread-nav a'); let cate = query[2].innerText; console.log(cate); let area = query[3].innerText; console.log(area); let poi = ''; if (query[4] != undefined) { poi = query[4].innerText; console.log(poi); } query = document.querySelector('div.summary'); let name = query.querySelector('h2 span.title').innerText; console.log(name); let addr = query.querySelector('span.geo').innerText; console.log(addr); let tel = query.querySelector('div.fs-section__left p:nth-child(3)').innerText; console.log(tel); let rate = ''; if (query.querySelector('span.biz-level strong') != undefined) { rate = query.querySelector('span.biz-level strong').innerText; console.log(rate); } let rate_count = query.querySelector('a.num.rate-count').innerText; console.log(rate_count); let recom_food = ''; query = document.querySelectorAll('div.menu__items table tbody td'); for (var index = 0; index < query.length; index++) { var element = query[index]; recom_food += element.innerText + ','; } desc = document.querySelector('div.poi-section.poi-section--shop div div').innerText; return cate + '|' + area + '|' + poi + '|' + name + '|' + addr + '|' + tel + '|' + rate + '|' + rate_count + '|' + recom_food + '|' + desc }); console.log(result5); postResult(result5 + '|' + city + '|' + temp[1]); } catch (e) { console.log(e); writeLog(stepLog); continue; } } } } catch (e) { console.log(e); writeLog(stepLog); continue; } } while (nextPage != 'undefined') } } stepLog.area_index = 1; } catch (e) { console.log(e); writeLog(stepLog); continue; } } } stepLog.location_index = 2; } catch (e) { console.log(e); writeLog(stepLog); continue; } } } stepLog.cate_index = 2; } function postResult (postData) { options = { hostname: '你的提交域名', port: 80, path: '/admin/test/upload', method: 'POST', headers: { 'Content-Type': 'raw', 'Content-Length': Buffer.byteLength(postData) } }; req = http.request(options, (res) => { //console.log(`STATUS: ${res.statusCode}`); //console.log(`HEADERS: ${JSON.stringify(res.headers)}`); res.setEncoding('utf8'); res.on('data', (chunk) => { console.log(`BODY: ${chunk}`); }); res.on('end', () => { console.log('No more data in response.'); }); }); req.on('error', (e) => { console.error(`problem with request: ${e.message}`); }); // write data to request body req.write(postData); req.end(); } function writeLog (log) { fs.writeFileSync('./steplog.log', JSON.stringify(log)); } function readLog () { let json = fs.readFileSync('./steplog.log'); return JSON.parse(json); } function start () { co(run).then(function () { console.log('done'); }).catch(function (err) { console.log(new Date().toUTCString()); console.error(err); start(); }); } start();
爬取結果以下:
若有更好的方案,歡迎交流。