资讯专栏INFORMATION COLUMN

爬豆瓣小组中的租房信息(mongo+node+vue)

vvpvvp / 1785人阅读

摘要:数目限制应该省略的条数数目限制排序方式函数的参数个数,必须是个,或者个。

基本思路

1.通过node中的 superagent 模拟http请求,去读取豆瓣小组的信息,对读取到的信息通过cheerio插件进行解析格式化以便于获取body中的信息存储到mongodb

2.因为豆瓣会ban掉一写爬虫ip,所以爬取过程中会使用ip池挑选没有使用过的ip进行代理去爬取,并且会避免并发 使用mapLimit

3.前端界面用vue提供ip选,和筛选结果分页展示,未部署到远程的,本地跑起来涉及到代理,主要在vue.config.js中,然后读取已经存在mongodb中的数据展示在前端

代码实现
目录结构
...
├── app.js
├── babel.config.js
...
...
├── server // 服务端代码
│   ├── db.js // 数据库增删改查接口
│   └── urls.js // 目前写了豆瓣小组的url,后续可以考虑手动输入
├── server.js // 服务端启动文件
...
├── src // 前端vue界面入口
│   ├── App.vue
│   ├── api 
│   ├── assets
│   ├── components
│   └── main.js
├── updatePoxy.js
├── vue.config.js
node端
//  server.js
// 服务启动
// 服务启动
// 服务启动
const express = require("express");
const app = express();
let server = app.listen(2333, "127.0.0.1", function () {
  let host = server.address().address;
  let port = server.address().port;
  console.log("Your App is running at" + host + ":" + port, );
})


// 插件
// 插件
// 插件
const superagent = require("superagent");
const eventproxy = require("eventproxy");
const ipProxy = require("ip-proxy-pool");
const cheerio = require("cheerio");
const async = require("async");
require("superagent-proxy")(superagent);


// 爬虫基本配置,后续可以从界面端传进来
const groups = require("./server/urls") // 租房小组的url,
let page = 1 // 抓取页面数量
let start = 24  // 页面参数拼凑

// 构造爬虫ulr
let ep = new eventproxy()  //  实例化eventproxy
global.db = require("./server/db")
let allLength = 0
groups.map((gp) => {
  gp.pageUrls = [] // 要抓取的页面数组
  allLength = allLength + 1
  for (let i = 0; i < page; i++) {
    allLength = allLength + i
    gp.pageUrls.push({
      url: gp.url + i * start // 构造成类似 https://www.douban.com/group/liwanzufang/discussion?start=0
    });
  }
})

// 接口中部分函数定义
const getPageInfo = (ip, pageItem, callback) => {
  //  设置访问间隔
  console.log("ip", ip)
  let delay = parseInt((Math.random() * 30000000) % 1000, 10)
  let resultBack = {label: pageItem.key, list: []}
  pageItem.pageUrls.forEach(pageUrl => {
    superagent.get(pageUrl.url).proxy(ip)
      // 模拟浏览器
      .set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36")
      //  如果你不乖乖少量爬数据的话,很可能被豆瓣kill掉,这时候需要模拟登录状态才能访问
      .set("Cookie", "")
      .end((err, pres) => {
        if (err || !pres) {
          ep.emit("preparePage", [])
          return
        }
        console.log("pres.text", pres.text)
        let $ = cheerio.load(pres.text) // 将页面数据用cheerio处理,生成一个类jQuery对象
        let itemList = $(".olt tbody").children().slice(1, 26) // 取出table中的每行数据,并过滤掉表格标题
        // 遍历页面中的每条数据
        for (let i = 0; i < itemList.length; i++) {
          let item = itemList.eq(i).children()

          let title = item.eq(0).children("a").text() || "" // 获取标题
          let url = item.eq(0).children("a").attr("href") || "" // 获取详情页链接
          // let author = item.eq(1).children("a").attr("href").replace("https://www.douban.com/people", "").replace(///g, "") || ""  // 获取作者id
          let author = item.eq(1).children("a").text() || "" // 这里改为使用作者昵称而不是id的原因是发现有些中介注册了好多账号,打一枪换个地方。虽然同名也有,但是这么小的数据量下,概率低到忽略不计
          let markSum = item.eq(2).text() // 获取回应数量
          let lastModify = item.eq(3).text() // 获取最后修改时间

          let data = {
            title,
            url,
            author,
            markSum,
            lastModify,
            label: pageItem.key
          }
          resultBack.list.push(data)
        }
        // ep.emit("事件名称", 数据内容)
        console.log("resultBack", resultBack)
        ep.emit("preparePage", resultBack) // 每处理完一条数据,便把这条数据通过preparePage事件发送出去,这里主要是起计数的作用
        setTimeout(() => {
          callback(null, pageItem.url);
        }, delay);
      })
  })
}
function getData(ip, res) {
  //  遍历爬取页面
  async.mapLimit(groups, 1, function (item, callback) {
    getPageInfo(ip, item, callback);
  }, function (err) {
    if (err) {
      console.log(err)
    }
    console.log("抓取完毕")
  });
}
ep.after("preparePage", allLength, function (data, res) {
  // 这里我们传入不想要出现的关键词,用"|"隔开 。比如排除一些位置,排除中介常用短语
  let filterWords = /求组|合租|求租|主卧/
  // 再次遍历抓取到的数据
  let inserTodbList = []
  data.forEach(item => {
    //  这里if的顺序可是有讲究的,合理的排序可以提升程序的效率
    item.list = item.list.filter(() => {
      if (item.markSum > 100) {
        console.log("评论过多,丢弃")
        return false
      }
      if (filterWords.test(item.title)) {
        console.log("标题带有不希望出现的词语")
        console.log("item", item)
        return false
      }
      return true
    })
    inserTodbList.push(...item.list)
  })
  global.db.__insertMany("douban", inserTodbList, function () {
    ep.emit("spiderEnd", {})
  })
});
// 接口
// 接口
// 接口
app.get("/api/getDataFromDouBan", (req, res) => {
  let {ip} = req.query
  getData(ip, res)
  ep.after("spiderEnd", 1, function() {
    res.send({
      data: "爬取结束"
    })
  })
})
// 获取ip
app.get("/api/getIps", (req, res) => {
  async function getIps(callback) {
    let ips = ipProxy.ips
    ips((err,response) => {
      callback(response)
    })
  }
  getIps(function (ipList) {
    res.send({
      msg: "获取成功",
      list: ipList
    })
  })
})
// 更新ip池
app.get("/api/updateIps", (req, res) => {
  ipProxy.run(() => {
    console.log("更新完毕")
  })
})
app.get("/api/doubanList", (req, res) => {
  let {label, page = 1, pageSize = 10} = req.query
  let param = []
  label && label.map((item) => {
    param.push({label: item})
  })
  let queryJson = {
    // $where: "label"
  }
  if (param.length) queryJson["$or"] = param
  global.db.__find("douban", {queryJson, page, pageSize}, function (data) {
    res.send({
      msg: "获取成功",
      ...data
    })
  })
})
db.js
// db.js
/**
 *  数据库封装
 * 
 */

var MongodbClient = require("mongodb").MongoClient
var assert = require("assert")
var url = "mongodb://localhost:27017";
/**
 * 连接数据库
 */

function __connectDB(callback) {
  MongodbClient.connect(url, function (err, client) {
    let db = client.db("zufangzi")
    callback(err, db, client)
  })
}


/**
 * 插入一条数据
 * @param {*} collectionName 集合名
 * @param {*} Datajson 写入的json数据
 * @param {*} callback 回调函数
 */
function __insertOne(collectionName, Datajson, callback) {
  __connectDB(function (err, db, client) {
    var collection = db.collection(collectionName);
    collection.insertOne(Datajson, function (err, result) {
      callback(err, result); // 通过回调函数上传数据
      client.close();
    })
  })
}
/**
 * 插入多条数据
 * @param {*} collectionName 集合名
 * @param {*} Datajson 写入的json数据
 * @param {*} callback 回调函数
 */
function __insertMany(collectionName, Datajson, callback) {
  __connectDB(function (err, db, client) {
    var collection = db.collection(collectionName);
    collection.insertMany(Datajson, function (err, result) {
      callback(err, result); // 通过回调函数上传数据
      client.close();
    })
  })
}

/**
 * 查找数据
 * @param {*} collectionName 集合名
 * @param {*} Datajson 查询条件
 * @param {*} callback 回调函数
 */

function __find(collectionName, {queryJson, page, pageSize}, callback) {
  var result = [];
  if (arguments.length != 3) {
    callback("find函数必须传入三个参数哦", null)
    return
  }
  __connectDB(async function (err, db, client) {
    var cursor = db.collection(collectionName).find(queryJson).skip((page - 1) * pageSize).limit(10);
    let total = await cursor.count()
    if (!err) {
      await cursor.forEach(function (doc) {
        // 如果出错了,那么下面的也将不会执行了
        // console.log("doc", doc)
        if (doc != null) {
          result.push(doc)
        }
      })
      callback({list: result, total})
    }
    client.close();
  })
}

/**
 * 
 * 删除数据(删除满足条件的所有数据哦)
 * @param {*} collectionName 集合名
 * @param {*} json 查询的json数据
 * @param {*} callback 回调函数
 */

function __DeleteMany(collectionName, json, callback) {
  __connectDB(function (err, db, client) {
    assert.equal(err, null)
    //删除
    db.collection(collectionName).deleteMany(
      json,
      function (err, results) {
        assert.equal(err, null)
        callback(err, results);
        client.close(); //关闭数据库
      }
    );
  });
}


/**
 * 修改数据
 * @param {*} collectionName 集合名
 * @param {*} json1 查询的对象
 * @param {*} json2 修改
 * @param {*} callback 回调函数
 */

function __updateMany(collectionName, json1, json2, callback) {
  __connectDB(function (err, db, client) {
    assert.equal(err, null)
    db.collection(collectionName).updateMany(
      json1,
      json2,
      function (err, results) {
        assert.equal(err, null)
        callback(err, results)
        client.close()
      }
    )
  })
}


/**
 * 获取总数
 * @param {*} collectionName 集合名
 * @param {*} json 查询条件
 * @param {*} callback 回调函数
 */

function __getCount(collectionName, json, callback) {
  __connectDB(function (err, db, client) {
    db.collection(collectionName).count(json).then(function (count) {
      callback(count)
      client.close();
    })
  })
}


/**
 * 分页查找数据
 * @param {*} collectionName 集合名
 * @param {*} JsonObj 查询条件
 * @param {*} C 【可选】传入的参数,每页的个数、显示第几页
 * @param {*} C  callback
 */

function __findByPage(collectionName, JsonObj, C, D) {
  var result = []; //结果数组
  if (arguments.length == 3) {
      //那么参数C就是callback,参数D没有传。
      var callback = C;
      var skipnumber = 0;
      //数目限制
      var limit = 0;
  } else if (arguments.length == 4) {
      var callback = D;
      var args = C;
      //应该省略的条数
      var skipnumber = args.pageamount * args.page || 0;
      //数目限制
      var limit = args.pageamount || 0;
      //排序方式
      var sort = args.sort || {};
  } else {
      throw new Error("find函数的参数个数,必须是3个,或者4个。");
      return;
  }

  //连接数据库,连接之后查找所有
  __connectDB(function (err, db, client) {
      var cursor = db.collection(collectionName).find(JsonObj).skip(skipnumber).limit(limit).sort(sort);
      cursor.each(function (err, doc) {
          if (err) {
              callback(err, null);
              client.close(); //关闭数据库
              return;
          }
          if (doc != null) {
              result.push(doc); //放入结果数组
          } else {
              //遍历结束,没有更多的文档了
              callback(null, result);
              client.close(); //关闭数据库
          }
      });
  });
}


module.exports = {
  __connectDB,
  __insertOne,
  __insertMany,
  __find,
  __DeleteMany,
  __updateMany,
  __getCount,
  __findByPage
}
接口
const axios = require("axios");
export const getHousData = async (arg = {}) => {
  // 从数据库中获取已经爬取到的数据
  let respones = await axios.get("/api/doubanList", {
    params: arg
  })
  return respones.data
}
export const spiderData = async (arg = {}) => {
  // 向爬取数据
  let respones = await axios.get("/api/getDataFromDouBan/", {
    params: arg
  })
  return respones.data
}
 
export const updateIps = async (arg = {}) => {
  // 更新ip池
  let respones = await axios.get("/api/updateIps/", {
    params: arg
  })
  return respones.data
}
 
export const getIps = async (arg = {}) => {
  // 获取ip池
  let respones = await axios.get("/api/getIps/", {
    params: arg
  })
  return respones.data
}
 
项目启动

1.命令行中启动mongodb 输入:mongod,未安装的需要自行安装

2.命令行中输入 yarn dev启动本地前端项目

3.命令行中输入 nodemon server.js 启动后端项目

项目地址

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106084.html

相关文章

  • 作为前端,如何帮帝都的朋友租到合适的房子

    摘要:前后翻几页我们不难发现,豆瓣是利用后面的参数来实现分页的。最后我们打开看一看效果吧,标红的是回复数量,点击标题可以直接跳转到豆瓣对应的页面。   在帝都打拼的小伙伴都知道,要租个合适的房子真心不易。中介要收一个月的房租作为中介费。而且很多黑中介打着租房的旗号各种坑蒙拐骗。要想在茫茫帖子中找到真正的房东,宛如大海捞针,同时需要和各路黑中介斗智斗勇。接下来就讲讲我浴血奋战的故事。   那么...

    pubdreamcc 评论0 收藏0
  • 作为前端,如何帮帝都的朋友租到合适的房子

    摘要:前后翻几页我们不难发现,豆瓣是利用后面的参数来实现分页的。最后我们打开看一看效果吧,标红的是回复数量,点击标题可以直接跳转到豆瓣对应的页面。   在帝都打拼的小伙伴都知道,要租个合适的房子真心不易。中介要收一个月的房租作为中介费。而且很多黑中介打着租房的旗号各种坑蒙拐骗。要想在茫茫帖子中找到真正的房东,宛如大海捞针,同时需要和各路黑中介斗智斗勇。接下来就讲讲我浴血奋战的故事。   那么...

    xiaolinbang 评论0 收藏0
  • 前方来报,八月最新资讯--关于vue2&3的最佳文章推荐

    摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...

    izhuhaodev 评论0 收藏0
  • 23个Python虫开源项目代码,包含微信、淘宝、豆瓣、知乎、微博等

    摘要:今天为大家整理了个爬虫项目。地址新浪微博爬虫主要爬取新浪微博用户的个人信息微博信息粉丝和关注。代码获取新浪微博进行登录,可通过多账号登录来防止新浪的反扒。涵盖链家爬虫一文的全部代码,包括链家模拟登录代码。支持微博知乎豆瓣。 showImg(https://segmentfault.com/img/remote/1460000018452185?w=1000&h=667); 今天为大家整...

    jlanglang 评论0 收藏0
  • scrapy入门教程——豆瓣电影Top250!

    摘要:注意爬豆爬一定要加入选项,因为只要解析到网站的有,就会自动进行过滤处理,把处理结果分配到相应的类别,但偏偏豆瓣里面的为空不需要分配,所以一定要关掉这个选项。 本课只针对python3环境下的Scrapy版本(即scrapy1.3+) 选取什么网站来爬取呢? 对于歪果人,上手练scrapy爬虫的网站一般是官方练手网站 http://quotes.toscrape.com 我们中国人,当然...

    senntyou 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<