1.經過node中的 superagent
模擬http請求,去讀取豆瓣小組的信息,對讀取到的信息經過cheerio
插件進行解析格式化以便於獲取body
中的信息存儲到mongodb
中前端
2.由於豆瓣會ban掉一寫爬蟲ip,因此爬取過程當中會使用ip池
挑選沒有使用過的ip進行代理去爬取,而且會避免併發 使用mapLimit
vue
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
// 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 /** * 數據庫封裝 * */ 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