码迷,mamicode.com
首页 > Web开发 > 详细

基于nightmare的美团美食商家爬虫实践

时间:2017-09-03 22:05:12      阅读:1645      评论:0      收藏:0      [点我收藏+]

标签:innertext   name   span   并且   深圳   min   web自动化   正则表达式   child   

前言

上学的时候自己写过一些爬虫代码,比较简陋,基于HttpRequest请求获取地址返回的信息,再根据正则表达式抓取想要的内容。那时候爬的网站大多都是静态的,直接获取直接爬即可,而且也没有什么限制。但是现在网站的安全越来越完善,各种机器识别,打码,爬虫也要越来越只能才行了。

前段时间有需求要简单爬取美团商家的数据,做了一些分析,实践,在这里总结分享。

美团商家页分析

技术分享

1、城市大全可以很容易的在这个页面爬出来 http://www.meituan.com/index/changecity/initiative
2、每个城市一个地址,例如深圳:http://sz.meituan.com/category/meishi
3、可以按照分类、区域、人数来分类
4、商家列表是动态JS加载的,并且会有很多页数
5、根据商家列表再进入商家详情获取数据

这样爬取流程即为
1、进去城市美食页
2、抓取分类,循环选择分类
3、抓取区域,循环选择区域
4、抓取人数,循环选择人数
5、判断是否有下一页按钮,循环进入下一页
6、进入详情页抓取,提交之后continue

需要爬取的数据有(这里没有按人数爬)

  1. CREATE TABLE `test_mt` (
  2. `Id` int(11) NOT NULL AUTO_INCREMENT,
  3. `city` varchar(10) NOT NULL DEFAULT ‘‘ COMMENT ‘城市‘,
  4. `cate` varchar(15) NOT NULL DEFAULT ‘‘ COMMENT ‘分类‘,
  5. `area` varchar(15) NOT NULL DEFAULT ‘‘ COMMENT ‘区域‘,
  6. `poi` varchar(15) NOT NULL DEFAULT ‘‘ COMMENT ‘商圈‘,
  7. `name` varchar(30) NOT NULL DEFAULT ‘‘ COMMENT ‘店名‘,
  8. `addr` varchar(50) NOT NULL DEFAULT ‘‘ COMMENT ‘地址‘,
  9. `tel` varchar(30) NOT NULL DEFAULT ‘‘ COMMENT ‘联系方式‘,
  10. `rj` int(11) NOT NULL DEFAULT ‘0‘ COMMENT ‘人均‘,
  11. `rate` float(2,1) NOT NULL DEFAULT ‘0.0‘ COMMENT ‘评价‘,
  12. `rate_count` int(11) NOT NULL DEFAULT ‘0‘ COMMENT ‘评价数‘,
  13. `recom_food` varchar(512) NOT NULL DEFAULT ‘‘ COMMENT ‘特色菜‘,
  14. `desc` varchar(512) NOT NULL DEFAULT ‘‘ COMMENT ‘门店介绍‘,
  15. PRIMARY KEY (`Id`)
  16. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

爬虫工具选取

为了快速实现功能,直接找现有的开源工具,说到爬虫,python首屈一指,所以先以此尝试

pysipder

https://github.com/binux/pyspider
最开始尝试了pysipder,主要是因为国人写的,先支持国产,准确的说pyspider已经是一个非常强大的爬虫框架了,具体内容官网查看,经过试用之后,感觉有一些杀鸡用牛刀的感觉,再来pyspider默认只支持抓取静态页,对于js加载的美团列表,难度就大了很多,中间尝试考虑通过获取cookie,模拟接口操作,但是协议解析起麻烦又耗时,坑肯定又多,就放弃了。

pyspider的js加载是通过配合phantomjs实现的,但是据说有内存泄露的问题,要不定期的重启phantomjs,试用之后发现并不很顺手(也许是python不熟),特别是模拟点击操作很麻烦导致回调地狱的出现,所以考虑再多试用几款工具选型。

scrapy

https://scrapy.org/
20K的star代表了它的强大,试用过程中发现和pysipder大同小异,遇到的问题也大同小异,也跳过了。

nightmare

https://github.com/segmentio/nightmare
之前用的时候git上star貌似还不多,目前已经13K了,准确的说nightmare一款基于electron(曾经使用PhantomJS,后面改用Electron)的高度封装web自动化测试工具,当然也可以用来做一些简易的爬虫,api及其简单,缺点就是模拟真人操作,这样的爬虫效率非常低,但是对于高安全性的网站来说,这样的操作也最为安全,防止被封。

经过试用之后,最后决定采用nightmare进行爬虫。

同步任务

需要注意的是evaluate函数返回的是promise,即为异步回调函数,可以配合co库,进行yield操作,即可用同步模式进行异步操作。

  1. var Nightmare = require(‘nightmare‘),
  2. nightmare = Nightmare(),
  3. co = require(‘co‘);
  4. var run = function*(){
  5. var results = [];
  6. for(var i=0; i<5; i++){
  7. var result = yield nightmare.goto(‘http://example.org‘).evaluate(function(){ return 123;});
  8. results.push(result);
  9. }
  10. return results;
  11. }
  12. co(run).then(function(results){
  13. console.dir(results);
  14. console.log(‘done‘);
  15. });

js动态加载

nightmare是模拟操作,相当于开了个浏览器,所以这都不是什么问题,但需要注意的是,列表如果数量太多,其实是分页加载的,第一次只加载十个,下滑再加载10个,加载完整页之后,还有下一页按钮,至于当前页的分页,需要滑动到低,才会加载完,所以还需要模拟滑动操作scrollTo,滑动过程中动态加载数据,有时候会有不成功的情况(可能是由于官方限制?),所以偶尔会漏过一些商家。

中断继续

为了避免异常导致从头开始爬,可以每次在catch的时候保存当前的状态值,下次启动读取,然后接着爬即可。

  1. //记录执行步骤,中断后下次继续
  2. //分类
  3. let stepLog = {
  4. cate_index: 0,
  5. //区域
  6. location_index: 8,
  7. //商圈
  8. area_index: 0,
  9. }
  10. function writeLog (log) {
  11. fs.writeFileSync(‘./steplog.log‘, JSON.stringify(log));
  12. }
  13. function readLog () {
  14. let json = fs.readFileSync(‘./steplog.log‘);
  15. return JSON.parse(json);
  16. }

爬坑总结

1、爬虫检测限制非常严格,搞不好就403,隔天才恢复,间隔时间很重要,示例代码的参数已经比较稳定。
2、数据基本上都是动态加载,加载接口又要cookie,又要post首次加载的各种参数,这也是为什么难爬,之前考虑过用PhantomJS,但是API过于复杂,无界面,不方便调试,nightmare基于electron这方面简直是神器,模拟人操作,又防封,又便捷。
3、nightmare不会记录cookie,所以如果有时候爬久了,关闭再开会403,但是浏览器正常,是cookie导致的,可以访问一些美团其他页面,先加载cookie再跳转到需要爬的页面即可。
4、由于默认情况不会记录cookie,所以需要的话可以再结束的时候getcookie序列化成json保存成文件,下次开启的时候再进行初始化。
5、中断继续,也可以把各种状态参数序列化成json保存,下次启动初始化,即可从中断的地方继续开始。
6、可以不需要正则,直接用dom选择器进行html元素查询。
7、效率确实不高,但也没啥好办法,爬一个城市大概花4-5天。

示例代码

注意post提交服务器地址改为自己的接口,如果需要保存本地,需自行处理。
代码保存直接运行即可 node **.js
PS:中间变量命名有些随意,请见谅。

  1. var Nightmare = require(‘nightmare‘);
  2. var fs = require(‘fs‘);
  3. //nightmare = Nightmare({ show: true }),
  4. var co = require(‘co‘);
  5. var http = require(‘http‘);
  6. var city = ‘南昌‘;
  7. var run = function* () {
  8. let nightmare = Nightmare({ show: true, waitTimeout: 60000 });
  9. //记录执行步骤,中断后下次继续
  10. //分类
  11. let stepLog = {
  12. cate_index: 0,
  13. //区域
  14. location_index: 8,
  15. //商圈
  16. area_index: 0,
  17. }
  18. if (fs.existsSync(‘./steplog.log‘)) {
  19. stepLog = readLog();
  20. }
  21. //writeLog(stepLog);
  22. //获取地区美食分类,先进主页是为了获取cookie防止被封
  23. let result1 = yield nightmare.goto(‘http://nc.meituan.com/‘).wait(5000)
  24. .goto(‘http://nc.meituan.com/category/meishi‘)
  25. .wait(‘div.filter-label-list.filter-section.category-filter-wrapper.first-filter ul.inline-block-list‘)
  26. .evaluate(function () {
  27. let arr_a = document.querySelectorAll(‘div.filter-label-list.filter-section.category-filter-wrapper.first-filter ul.inline-block-list li a‘);
  28. let str = ‘‘;
  29. //过滤全部,代金券
  30. for (var index = 2; index < arr_a.length; index++) {
  31. var element = arr_a[index];
  32. str += element.href + ‘,‘;
  33. }
  34. return str;
  35. })
  36. let arr_a1 = result1.split(‘,‘);
  37. console.log(arr_a1);
  38. var temp_index1 = stepLog.cate_index;
  39. for (var index1 = temp_index1; index1 < arr_a1.length; index1++) {
  40. stepLog.cate_index = index1;
  41. //获取美食分类之后,获取地区
  42. var element1 = arr_a1[index1];
  43. if (element1 != ‘‘) {
  44. try {
  45. let result2 = yield nightmare
  46. .wait(10000)
  47. .goto(element1)
  48. .wait(‘ul.inline-block-list.J-filter-list.filter-list--fold‘)
  49. .evaluate(function () {
  50. let arr_a = document.querySelectorAll(‘ul.inline-block-list.J-filter-list.filter-list--fold li a‘);
  51. let str = ‘‘;
  52. //过滤全部,地铁2
  53. for (var index = 2; index < arr_a.length; index++) {
  54. var element = arr_a[index];
  55. str += element.href + ‘,‘;
  56. }
  57. return str
  58. });
  59. let arr_a2 = result2.split(‘,‘);
  60. console.log(arr_a2);
  61. var temp_index2 = stepLog.location_index;
  62. for (var index2 = temp_index2; index2 < arr_a2.length; index2++) {
  63. stepLog.location_index = index2;
  64. //获取地区之后,获取商圈
  65. var element2 = arr_a2[index2];
  66. if (element2 != ‘‘) {
  67. try {
  68. let result3 = yield nightmare
  69. .wait(10000)
  70. .goto(element2)
  71. .wait(‘ul.inline-block-list.J-area-block‘)
  72. .evaluate(function () {
  73. let arr_a = document.querySelectorAll(‘ul.inline-block-list.J-area-block li a‘);
  74. let str = ‘‘;
  75. //商圈下标,过滤全部
  76. for (var index = 1; index < arr_a.length; index++) {
  77. var element = arr_a[index];
  78. str += element.href + ‘,‘;
  79. }
  80. return str
  81. });
  82. arr_a3 = result3.split(‘,‘);
  83. console.log(arr_a3);
  84. var temp_index3 = stepLog.area_index;
  85. for (var index3 = temp_index3; index3 < arr_a3.length; index3++) {
  86. stepLog.area_index = index3;
  87. //获取商圈店铺信息
  88. var element3 = arr_a3[index3];
  89. if (element3 != ‘‘) {
  90. let nextPage = ‘undefined‘;
  91. do {
  92. let url = ‘‘;
  93. if (nextPage == ‘undefined‘)
  94. url = element3;
  95. else
  96. url = nextPage;
  97. try {
  98. let result4 = yield nightmare
  99. .wait(10000)
  100. .goto(url)
  101. .wait(‘#content‘).wait(2000)
  102. .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)
  103. .evaluate(function () {
  104. let arr_a = document.querySelectorAll(‘div.poi-tile-nodeal‘);
  105. let str = ‘‘;
  106. for (var index = 0; index < arr_a.length; index++) {
  107. var element = arr_a[index];
  108. let sp_rj = element.querySelector(‘div.poi-tile__money span.avg span‘);
  109. //人均
  110. let rj = 0;
  111. if (sp_rj != null) {
  112. let str_rj = sp_rj.innerText;
  113. rj = parseInt(str_rj.substr(1, rj.length));
  114. }
  115. console.log(index);
  116. let url = ‘‘;
  117. let elelink = element.querySelector(‘a.poi-tile__head.J-mtad-link‘);
  118. if (elelink != null) {
  119. //链接地址
  120. url = elelink.href;
  121. console.log(url);
  122. }
  123. str += url + ‘|‘ + rj + ‘,‘;
  124. }
  125. let href = document.querySelector(‘li.next a‘) ? document.querySelector(‘li.next a‘).href : ‘undefined‘
  126. return str + ‘^‘ + href;
  127. });
  128. console.log(result4);
  129. temp4 = result4.split(‘^‘);
  130. nextPage = temp4[1];
  131. arr_a4 = temp4[0].split(‘,‘);
  132. for (var index4 = 0; index4 < arr_a4.length; index4++) {
  133. var element4 = arr_a4[index4];
  134. if (element4 != ‘‘) {
  135. try {
  136. let temp = element4.split(‘|‘);
  137. let url5 = ‘‘;
  138. if (temp[0] != ‘‘) {
  139. url5 = temp[0];
  140. } else {
  141. continue;
  142. }
  143. //获取店铺详细信息
  144. let result5 = yield nightmare
  145. .wait(5000)
  146. .goto(url5)
  147. .wait(‘div.poi-section.poi-section--shop‘)
  148. .evaluate(function () {
  149. let query = document.querySelectorAll(‘div.component-bread-nav a‘);
  150. let cate = query[2].innerText; console.log(cate);
  151. let area = query[3].innerText; console.log(area);
  152. let poi = ‘‘;
  153. if (query[4] != undefined) {
  154. poi = query[4].innerText; console.log(poi);
  155. }
  156. query = document.querySelector(‘div.summary‘);
  157. let name = query.querySelector(‘h2 span.title‘).innerText; console.log(name);
  158. let addr = query.querySelector(‘span.geo‘).innerText; console.log(addr);
  159. let tel = query.querySelector(‘div.fs-section__left p:nth-child(3)‘).innerText; console.log(tel);
  160. let rate = ‘‘;
  161. if (query.querySelector(‘span.biz-level strong‘) != undefined) {
  162. rate = query.querySelector(‘span.biz-level strong‘).innerText; console.log(rate);
  163. }
  164. let rate_count = query.querySelector(‘a.num.rate-count‘).innerText; console.log(rate_count);
  165. let recom_food = ‘‘;
  166. query = document.querySelectorAll(‘div.menu__items table tbody td‘);
  167. for (var index = 0; index < query.length; index++) {
  168. var element = query[index];
  169. recom_food += element.innerText + ‘,‘;
  170. }
  171. desc = document.querySelector(‘div.poi-section.poi-section--shop div div‘).innerText;
  172. return cate + ‘|‘ + area + ‘|‘ + poi + ‘|‘ + name + ‘|‘ + addr + ‘|‘ + tel + ‘|‘ + rate + ‘|‘ + rate_count + ‘|‘ + recom_food + ‘|‘ + desc
  173. });
  174. console.log(result5);
  175. postResult(result5 + ‘|‘ + city + ‘|‘ + temp[1]);
  176. } catch (e) {
  177. console.log(e);
  178. writeLog(stepLog);
  179. continue;
  180. }
  181. }
  182. }
  183. } catch (e) {
  184. console.log(e);
  185. writeLog(stepLog);
  186. continue;
  187. }
  188. } while (nextPage != ‘undefined‘)
  189. }
  190. }
  191. stepLog.area_index = 1;
  192. } catch (e) {
  193. console.log(e);
  194. writeLog(stepLog);
  195. continue;
  196. }
  197. }
  198. }
  199. stepLog.location_index = 2;
  200. } catch (e) {
  201. console.log(e);
  202. writeLog(stepLog);
  203. continue;
  204. }
  205. }
  206. }
  207. stepLog.cate_index = 2;
  208. }
  209. function postResult (postData) {
  210. options = {
  211. hostname: ‘你的提交域名‘,
  212. port: 80,
  213. path: ‘/admin/test/upload‘,
  214. method: ‘POST‘,
  215. headers: {
  216. ‘Content-Type‘: ‘raw‘,
  217. ‘Content-Length‘: Buffer.byteLength(postData)
  218. }
  219. };
  220. req = http.request(options, (res) => {
  221. //console.log(`STATUS: ${res.statusCode}`);
  222. //console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  223. res.setEncoding(‘utf8‘);
  224. res.on(‘data‘, (chunk) => {
  225. console.log(`BODY: ${chunk}`);
  226. });
  227. res.on(‘end‘, () => {
  228. console.log(‘No more data in response.‘);
  229. });
  230. });
  231. req.on(‘error‘, (e) => {
  232. console.error(`problem with request: ${e.message}`);
  233. });
  234. // write data to request body
  235. req.write(postData);
  236. req.end();
  237. }
  238. function writeLog (log) {
  239. fs.writeFileSync(‘./steplog.log‘, JSON.stringify(log));
  240. }
  241. function readLog () {
  242. let json = fs.readFileSync(‘./steplog.log‘);
  243. return JSON.parse(json);
  244. }
  245. function start () {
  246. co(run).then(function () {
  247. console.log(‘done‘);
  248. }).catch(function (err) {
  249. console.log(new Date().toUTCString());
  250. console.error(err);
  251. start();
  252. });
  253. }
  254. start();

爬去结果如下:
技术分享

如有更好的方案,欢迎交流。

附件列表

     

    基于nightmare的美团美食商家爬虫实践

    标签:innertext   name   span   并且   深圳   min   web自动化   正则表达式   child   

    原文地址:http://www.cnblogs.com/leestar54/p/7470850.html

    (0)
    (0)
       
    举报
    评论 一句话评论(0
    登录后才能评论!
    © 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
    迷上了代码!