前端社团程序员让前端飞

Node.js 爬取赶集网杭州租房信息

2017-03-06  本文已影响208人  淡就加点盐

附结果 杭州租房信息情况分析

P1 工具:Node.js、cheerio、superagent、fs、async

  • 利用 superagent 请求获取 html 内容

P2 分析赶集网租房页面html结构

1.分析页面结构

租房块 .f-list-item 对应的 html 代码

2. 分析结果:

租房.title = $('.f-list-item').find('.dd-item.title a').attr('title');
租房.address = $('.f-list-item').find('.area').text().replace(/\s+/g, '');  //去除字符串内的全部空格
租房.price = $('.f-list-item').find('.price').text();
租房.size = $('.f-list-item').find('.dd-item.size').text().replace(/\s+/g, '');

P3 利用 superagent 发出 GET 请求获取页面的 html

参考文档:superagent Github通读superagent文档[整理稿]好用的 HTTP模块SuperAgent

1. 将要请求的页面链接放进一个数组,便于并发执行多个任务

// 生成爬取页面的链接
var pages = 2000;
function getLinkArr(cb) {
  var links = [];
  for(var i = 0; i < pages; i++) {
    var url = 'http://hz.ganji.com/fang1/o' + (i + 1) + '/';  // 杭州房源网页地址
    //var url = 'http://sh.ganji.com/fang1/o' + (i + 1) + '/';  // 上海房源网页地址
    links.push(url);
  }
  readFile();
  cb(null, links);
}

2.GET请求页面 html

//开始处理请求得到的html
function start(links, cb) {
  async.eachLimit(links, 5, function(item, callback) {  // 限制允许并发执行的任务数为5,防止访问过快
    setTimeout(function () {                            // 设置每批任务之间休息1s,防止访问过快
      request.get(item).end(function (err, res) {       // get请求页面
        if(res.ok) {
          console.log(item);
          getRoom(res.text);
          console.log(item + ' is processing...');
          callback();
        } else {
          console.log(item + ' failed...');
          callback();
        }
      })
    }, 1000)
  }, function(err) {
    if(err) {
      console.log('A file failed to process');
    } else {
      console.log('All files have been processed successfully');
      cb(null, data);
    }
  });
}

P4 利用 cheerio 处理获取到的html

cheerio 文档

//获取房屋基本信息
function getRoom(html) {
  var $ = cheerio.load(html);
  var room = $('.f-list-item').toArray();
  var len = room.length;
  console.log('room is:' + len);
  for(var i = 0; i < len; i++) {
    var arr = {};
    arr.title = $(room[i]).find('.dd-item.title a').attr('title');  // 房屋标题
    arr.address = $(room[i]).find('.area').text().replace(/\s+/g, ''); // 地址
    arr.price = $(room[i]).find('.price').text(); // 价格/月
    arr.size = $(room[i]).find('.dd-item.size').text().replace(/\s+/g, ''); // 房屋大小等简介
    var item = '{"title":"' + arr.title + '","address":"' + arr.address + '","price":"' + arr.price + '","size":"' + arr.size +'"}';  // 将数据组装成json格式的字符串
    if(findItem(data, arr) == -1) {
      data.push(JSON.parse(formatString(item)));  // 将json格式的字符串push进data数组
    }
  }
}

P5 利用 fs 读写文件,在上一次爬取基础上增加数据

nodejs 数据读写详解

1.读文件

//读取保存的文本数据
function readFile() {
  var fileData = '';
  var readStream = fs.createReadStream(path);

  readStream.setEncoding('UTF8');
  readStream.on('data', function (chunk) {
    fileData += chunk;
  });
  readStream.on('end', function () {
    data = JSON.parse(formatString(fileData));
    console.log(data)
  });
  readStream.on('error', function (err) {
    console.log(err.stack);
  })
}

2.写文件

//写入数据
function writeFile(data, cb) {
  console.log('最终爬取到:' + data.length + ' 条数据');
  console.log("开始写入数据...");
  fs.writeFile(path, JSON.stringify(data), function (err) {
    if (err) {
      return console.error(err);
    }
    console.log("数据写入成功!");
  });
  cb(null, 'Job Is Done !');
}

P6 总结

1.避免获取到重复数据

不知什么原因,爬了4天才爬到4万多条数据,与赶集网上的数据显示12万+相差甚远,大约每天爬到1万的新数据。

//去重,data为 json
function findItem(data, item) {
  for(var i = 0; i < data.length; i++) {
    if(item.title === data[i].title && item.address === data[i].address && item.price === data[i].price && item.size === data[i].size) {
      return i;
    }
  }
  return -1;
}

// 或者将data转换成字符串,然后 indexOf()
var str = JSON.stringify(data);
if(str.indexOf('xxx') === -1) {
    data.push(JSON.parse(formatString(item))); 
}

2.读取JSON文本内容报错

报错: Unexpected token in JSON at position
原因: UTF-8的BOM头导致解析JSON异常,在保存一个以UTF-8编码的文件时,会在文件开始的地方插入三个不可见的字符(0xEF 0xBB 0xBF,即BOM)。

// 解析json前去掉BOM报头(UTF-8签名)
function formatString(str) {
  if (str != null) {
    str = str.replace("\ufeff", "").replace(/\\/g, "/");
  }
  return str;
}

3.写文件总是在superagent获取并处理页面之前,使用async的waterfall解决之

async 文档

//async按顺序执行
async.waterfall([
  getLinkArr,
  start,
  writeFile
], function (err, result) {
  if(err) {
    console.log('error: ' + err);
  } else {
    console.log('任务完成!!!');
  }
});

4.访问页面过快,被服务器识别出,需要填验证码,封IP

利用 async 的eachLimit限制允许并发执行的任务数,防止访问过快;async 批内并行执行,批与批之间顺序执行。

async.eachLimit(["123",  "456",  "789"],  2,  function(item, callback){
    console.log(item);
    callback();  // 必须调用,才能触发下一个任务执行
}, function(error){
    if(error){
        console.error("error: " + error);
    }
});

5. Nodejs中“循环+异步” 好深的坑!!

上一篇下一篇

猜你喜欢

热点阅读