Node.js 2小时爬取驴妈妈8W+条旅游数据

节前在CSDN博客看了《用python爬虫爬取去哪儿4500个热门景点,看看国庆不能去哪儿》
后来我自己用Node.js去爬取一下,发现有问题。
去哪儿接口:http://piao.qunar.com/ticket/list.htm?keyword=北京&region=&from=mpl_search_suggest&page=1。

问题

这个接口一个IP短时间访问次数多了是需要验证的,之后就是不能再访问了。
1.按照作者的逻辑是每个地点,每个页面都需要自己一个一个的取获取,这个工作量是比较大的,而且接口访问多了就不能访问了。
2.百度地图获取经纬度。其实这个是不需要的。去哪儿的页面数据里面已经携带的有。
这里写图片描述

我的爬虫

所以我就还成了爬取驴妈妈的,使用的是Node.js去爬取的。驴妈妈不存在一个ip短时间访问多了就不能访问的问题。同时我也做到了只需要开启服务,触发爬虫就可以自动爬取,数据是保存在本地数据库的。
爬取接口如下:
全部旅游:http://s.lvmama.com/route/H8P1?keyword=北京&tabType=ticket#list
景点门票:http://s.lvmama.com/ticket/H8P1?keyword=北京&tabType=ticket#list
酒店+景点:http://s.lvmama.com/scenictour/H8P1?keyword=北京&tabType=ticket#list
触发地参团:http://s.lvmama.com/group/H8P1?keyword=北京&tabType=ticket#list
自由行:http://s.lvmama.com/freetour/H8P1?keyword=北京&tabType=ticket#list
自由行:http://s.lvmama.com/freetour/H8P1?keyword=北京&tabType=ticket#list
暂时只爬取了这6个接口,共计80192条数据。总共爬取的时间大概是1天时间,我做的每8秒钟爬取一页数据,一次只爬取一个接口,没有6个接口同时爬取,每页爬取间隔也可以在缩短。粗略估算2个小时内可以全部爬取完成,但是我没这么做,实际在操作过程中,驴妈妈的页面存在不一致的问题,需要时刻关注异常并处理异常。

实际操作

/*
 * 获取景点门票列表
 * page: 页码
 * keyword: 关键字
 */
router.all('/api/lvmama/ticket', function(req, res, next) {
    // 接口参数
    var params = req.query || req.params; 
    // 页码
    var page = params.page || 1;
    var keyword = params.keyword;
    var urlStr = 'http://s.lvmama.com/ticket/H8P' + page + '?keyword=' + encodeURI(keyword) + '&tabType=ticket#list';
    var arrayList = [];
    request(urlStr, function (error, response, body) {
       if (!error && response.statusCode == 200) {
          // 获取html文档
          var $ = cheerio.load(body);
          // 获取总条数
          var allCount = $('.search-filter  .search-nav .active b').text();
          // 获取总页面
          var tempPage =  ($('.search-page-num').text()  || ' / ').split('/')[1];
          tempPage = parseInt(tempPage);
          var scenicSql = '';
          var ticketSql = '';
          // 循环遍历景点列表
          $('.product-list  .product-item').each(function(i,elem){
              var dataModel = {};
              dataModel.itemId = ($(this).find('.product-regular .product-picture').attr('href')||'0-0').split('-')[1];
              dataModel.itemHref = $(this).find('.product-regular .product-picture').attr('href');
              dataModel.itemImg = ($(this).find('.product-regular .product-picture img').attr('src')||'0\r0').split('\r')[0];
              dataModel.itemTitle = $(this).find('.product-regular .product-section .product-ticket-title a').text();
              dataModel.itemCity = $(this).find('.product-regular .product-section .product-ticket-title .city').text();
              dataModel.itemProvice = keyword;
              dataModel.itemLocal = ($(this).find('.product-regular .product-section .product-details dd').first().text() || '').replace(/[\'\"\\\/\b\f\n\r\t]/g, '');
              dataModel.itemPrice = $(this).find('.product-regular .product-info .product-price em').text() + '起';
              dataModel.itemCommentLevel = $(this).find('.product-regular .product-info .product-number b').text();
              var values = "'" + dataModel.itemId + "'" + ',' 
              + "'" + dataModel.itemHref + "'" + ',' 
              + "'" + dataModel.itemImg + "'" + ',' 
              + "'" + dataModel.itemTitle + "'" + ','
              + "'" + dataModel.itemProvice + "'" + ','
              + "'" + dataModel.itemCity + "'" + ','  
              + "'" + dataModel.itemLocal + "'" + ',' 
              + "'" + dataModel.itemPrice + "'" + ',' 
              + "'" +dataModel.itemCommentLevel +"'";
              scenicSql = scenicSql + 'insert ignore into scenic(itemId,itemHref,itemImg,itemTitle,itemProvice,itemCity,itemLocal,itemPrice,itemCommentLevel) VALUES(' + values + ');';
              dataModel.itemTickets = [];
              // 循环遍历当前景点下的门票信息
              $(this).find('.product-special .product-special-list').each(function(i,elem){
                 var ticket = {};
                 ticket.scenicId = dataModel.itemId;
                 ticket.goodsId = ($(this).find('.pay .btn').attr('href') || '0=0').split('=')[1];
                 ticket.title = $(this).find('.name .title').attr('title');
                 ticket.price = $(this).find('.price').text();
                 if (ticket.price.indexOf('¥') < 0) { // 不包含
                    ticket.price = '0¥0';
                 }
                 ticket.price = ticket.price.split('¥')[1].replace(/[\'\"\\\/\b\f\n\r\t]/g, '');
                 ticket.lvprice = $(this).find('.price').text();
                 if (ticket.lvprice.indexOf('¥') < 0) { // 不包含
                    ticket.lvprice = '0¥0';
                 }

                 ticket.lvprice = ticket.lvprice.split('¥')[1].replace(/[\'\"\\\/\b\f\n\r\t]/g, '');
                 dataModel.itemTickets.push(ticket);
                 var values = "'" + ticket.scenicId + "'" + ',' 
                            + "'" + ticket.goodsId + "'" + ',' 
                            +  "'" + ticket.title + "',"  
                            + "'" + ticket.price + "'" + ',' 
                            + "'" + ticket.lvprice + "'";
                 ticketSql = ticketSql + 'insert ignore into ticket(scenicId,goodsId,title,price,lvprice) VALUES(' + values + ');';
              })
              arrayList.push(dataModel);
          })
          lvmamaSql = scenicSql + ticketSql;
          // 存数据库
          pool.getConnection(function(err, connection) {
            if(err){
                var data = {
                    status: 103,
                    message: '数据库连接失败'
                  }
                  res.end(JSON.stringify(data));
                return;
            }
            connection.query(lvmamaSql,function (err, results) {
                connection.release();
                if (err){
                    var data = {
                        status: 104,
                        message: '数据库插入失败',
                        err: JSON.stringify(err)
                      }
                      res.end(JSON.stringify(data));
                }else{
                    var data = {
                        status: 100,
                        message: '操作成功',
                        allPage: tempPage,
                        allCount: parseInt(allCount),
                        currentPage: parseInt(page),
                        dataList: arrayList
                      }
                      res.end(JSON.stringify(data));
                }
            })
         })
       }else{
         var data = {
            status: 101,
            message: '操作失败'
          }
          res.end(JSON.stringify(data));
       }
    });
})

其实具体的获取过程就是解析当前网页html的过程,这里不再赘述。可以看这里《Node.js爬虫技术》
这是之前写的一个,其实大致思路一样,只是这次加了定时爬取。
下面就是定时爬取景点门票的全国33个省市区的数据了。
1.制作省市区数组:

var shenfenName = [
    '北京','天津','上海','重庆','河北','山西','辽宁','吉林','黑龙江',
    '江苏','浙江','安徽','福建','江西','山东','河南','湖北','湖南','广东',
    '海南','四川','贵州','云南','陕西','甘肃','青海','台湾','内蒙',
    '广西','西藏','宁夏','新疆','香港','澳门'
];

2.定时获取,自动下一页,下一个省份。

router.all('/api/lvmama/allscenic', function(req, res, next) {
    var ticket_i = 0;   
    var ticket_page = 1;
    console.log(getNowTime() + ':景点门票开始存储');
    var ruleS     = new schedule.RecurrenceRule();  
    var second       = [1,9,17,33,41,49,57];  
    ruleS.second  = second;  
    // 每个8秒钟执行一次 
    var minJ = schedule.scheduleJob(ruleS, function(){
       var urlPage = 'http://localhost:8082/api/lvmama/ticket?page=' + ticket_page + '&keyword=' + encodeURI(shenfenName[ticket_i]);
       request(urlPage, function (error, response, body) {
           if (!error && response.statusCode == 200) {
              var body = JSON.parse(body);
              if (body.status == 100) {
                  var allPage = body.allPage;
                  console.log(getNowTime() + ':' + shenfenName[ticket_i] + '第' + ticket_page + '页存储成功');
                  ticket_page ++;
                  if (ticket_page > allPage) {
                    console.log(shenfenName[ticket_i] + '完成');
                    ticket_page = 1;
                    ticket_i++;
                    if (ticket_i > shenfenName.length) {
                        minJ.cancel();
                        console.log(getNowTime() + ':抓取完成');
                    }
                    return;
                  }
              }else{
                console.log(getNowTime() + ':' + shenfenName[ticket_i] + '第' + ticket_page + '页存储失败--');
                ticket_page ++;
              }
           }else{
             console.log('请求失败');
           }
       });
    });  
})
// 获取当前时间
function getNowTime(){
    var myDate = new Date();//获取系统当前时间
    var dateStr = myDate.toLocaleString( ); //获取日期与时间
    return dateStr;
}

这里就多用了一个定时的包:node-schedule。它的使用,看文档就行。
上面的自动爬取,主要的就是什么时候爬取下一页,下一个省份。其实这里和之前玩单片机,自己用定时去去写始终程序是一样的思路,就是一个进位的问题,把页码比作秒针,省份比作分针,秒钟改变间隔是8秒。但是什么时候到省份就需要自动处理了,每个省份的页码是不一定的,需要先缓存省份的总页数。这也就是为什么我在定时上面加了两个变量

var ticket_i = 0;
var ticket_page = 1;

的原因。这里把这个机制暂称为:”时钟机制”。
其他几个接口的爬取就不再写了。
这里写图片描述
这是部分数据截图。

MySQL存储中文报错

在实际操作的过程中发现MySQL存储中文报错。我环境是ubuntu 16.04 + Node.js + MySQL + pm2;
由于这个服务器上的数据库之前没怎么用,所以没遇见过存储中文报错的问题。最后还是解决了,是建数据库的时候没指定字符集。

// 新建数据库
CREATE DATABASE `lvmama` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 
// 新建 景点门票 表
CREATE TABLE IF NOT EXISTS scenic (   11380
  id INT(11) NOT NULL AUTO_INCREMENT,
  itemId VARCHAR(45) NOT NULL,
  itemHref TEXT NOT NULL,
  itemImg TEXT NOT NULL,
  itemTitle TEXT NOT NULL,
  itemProvice TEXT NOT NULL,
  itemCity TEXT NOT NULL,
  itemLocal TEXT NOT NULL,
  itemPrice TEXT NOT NULL,
  itemCommentLevel TEXT NOT NULL,
  PRIMARY KEY (id),
  UNIQUE (itemId)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

总结

像这样的爬虫去爬取数据,感觉还是效率有些低,且异常处理还有待提高。像爬取自由行时,北京、上海、和云南的html结构和其他的不一样,宁夏又没有数据;有些页面部分列表数据有异常又需要跳过,这些都是在爬取过程中发现有异常再来修复的。目前只是8W多条数据,一旦数据到了百万级,感觉就是一场噩梦了。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页