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

基本思路

1.經過node中的 superagent 模擬http請求,去讀取豆瓣小組的信息,對讀取到的信息經過cheerio插件進行解析格式化以便於獲取body中的信息存儲到mongodb前端

2.由於豆瓣會ban掉一寫爬蟲ip,因此爬取過程當中會使用ip池挑選沒有使用過的ip進行代理去爬取,而且會避免併發 使用mapLimitvue

3.前端界面用vue提供ip選,和篩選結果分頁展現,未部署到遠程的,本地跑起來涉及到代理,主要在vue.config.js中,而後讀取已經存在mongodb中的數據展現在前端node

代碼實現

目錄結構
...
├── 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,未安裝的須要自行安裝ios

2.命令行中輸入 yarn dev啓動本地前端項目git

3.命令行中輸入 nodemon server.js 啓動後端項目github

項目地址
相關文章
相關標籤/搜索