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