koa+mongodb打造掘金關注者分析面板

前言

最近掘金更新了掘力值和等級規則,大部分用戶都帶上了等級徽章,並且每一個人的掘力值也都很清晰明瞭,我想這也是掘金激勵用戶輸出高質量文章的一種方式,當看到本身掘力值不斷增加和等級不斷升高的時候,想必心裏都會有種成就感。看到本身的掘力值後,發現本身還須要繼續努力,繼續分享更多本身的開發經驗和好的想法。html

那麼此次又要搞什麼事情呢?賣個關子先放張效果圖前端

idea

看過我文章的朋友應該感受到了,我喜歡分享本身作一個小項目或者小工具的經驗,分享針對細節或者某個知識點的內容不多。我想這個和本身的愛好有很大關係,我喜歡從零完成一個項目,從本身的一個想法到原型繪製,而後到UI設計,接着使用本身熟悉的語言寫先後端代碼,而後到先後端聯調,最後優化加部署到服務器。這一系列的過程我感受本身能學到更多的東西,也能從這一系列流程中向外擴展不少知識點和發現作項目中本身容易忽略的細節。vue

此次的掘金粉絲(其實也不能說粉絲,主要是關注者和關注了很差區分😂)分析工具也是個人一個突發奇想,由於加入掘金寫的第一篇文檔就是《掘金最熱文章收藏評論分析》,但那個項目也只是簡單的獲取文章的基礎數據,其實也談不上分析,並且如今回過頭看界面簡直有點粗糙(感受就像回看初中殺馬特般的照片同樣,哈哈)。因此此次就趁着掘金把掘力值和等級上線,來一個我的數據分析,其實主要是粉絲數據分析和關注的用戶分析。node

主要功能

  • 根據用戶ID獲取用戶的粉絲或關注的用戶數據
  • 分析粉絲或關注用戶,發佈文章、文章獲贊、文章閱讀數、粉絲數、掘力值TOP10
  • 分析粉絲或關注用戶等級分佈
  • 我的成就面板
  • 更多分析功能後續開發中...(期待你的建議)

體驗與源碼

體驗地址juejinfan.xkboke.comgit

github:github.com/gengchen528… (若是喜歡的話,歡迎給個star)github

爲了更好的方便你們體驗,目前已經部署到個人服務器了,能夠經過juejinfan.xkboke.com來訪問,必定要是https,因爲服務器帶寬限制,可能剛開始加載會比較慢,請耐心等待,或者你能夠直接把源碼部署在本地,這樣速度會快一點。若是你的粉絲比較多的話,在點擊分析後也會等較長時間,不過點擊後,你能夠等四五分鐘後再來看,數據會在爬取完成後立馬加載出來的。web

注:mongodb

  • uid指的是用戶id,並非用戶名,能夠在掘金個人主頁瀏覽器上方地址欄看到數據庫

  • token須要點擊到任意用戶的主頁,而後打開控制檯,刷新頁面,能夠看到get_multi_user這個請求,在請求參數一欄找到tokenexpress

uid查找

token查找

安裝

前提要安裝好mongodb,而且是默認端口。若是端口已更改請在/monogodb/config.js中修改端口號

git clone https://github.com/gengchen528/juejinAnalyze.git
cd juejinAnalyze
npm install 
npm run start

複製代碼

若是執行npm run dev,請全局安裝nodemon,若是使用pm2,請全局安裝pm2

技術棧

  • koa
  • mongoose
  • superagent
  • pm2

此次分析使用了相對expresss而言比較輕量的koa,數據庫就使用了mongodb,爬取數據的主要就是好搭檔superagent了。

掘金接口分析

我的主頁信息獲取

看到登陸頁面,可能你們會問既然是爬取,爲何還須要token呢,這個就要來講一下掘金渲染我的主頁的方式了。通過分析後,我發如今沒有登陸的狀況下,掘金是採用ssr(基於vue的服務端渲染)方式渲染的,這種方式渲染出來的頁面爬取起來是比較繁瑣的。然而登陸後會發現頁面上的數據都是從接口中獲取的,這個數據看起來就很開心了,基本上全部須要的數據都有了。那麼這個接口須要哪些參數呢,通過測試後發現主要有四個參數ids,token,src,cols,因此這裏就明白爲什麼登陸頁有token了吧。

  • ids: 用戶id,瀏覽器地址欄能夠找到

  • token: 打開控制檯,找到get_multi_user這個請求後能夠找到,這個接口必須是登錄後打開別人的主頁纔有,在本身主頁是沒有這個請求的。

  • src: 來源web(能夠默認爲web)

  • cols: 須要獲取的用戶信息(默認)

ssr

未登陸狀態

登陸後

所需參數

粉絲及關注的用戶列表獲取

粉絲列表及關注的用戶列表最初的時候遇到了不少問題,由於剛開始找到接口後,發現並非簡單的分頁,每次只能獲取20條數據。並且每次參數都不相同,第一次獲取會發現基本的參數只有三個uid,currentUid,src,可是在加載下一頁數據時會發現多了一個參數before,那麼這個before是怎麼來的呢。剛開始的時候爲了找到這個規律,我請求了數十次,而且把每次的before參數寫出來,最後發現居然沒有一點規律,我瞬間懷疑了人生😒,難道個人想法就這麼夭折了麼。還好當我打開每一個數組找規律時,發現了原來before取的是上個請求中最後一個用戶的關注時間,既然知道了規律就很簡單了,開始捲起袖子擼代碼。

第一次請求

分頁請求

核心代碼

目錄結構

schema設計

最初的時候只設計了用戶表,存的都是用戶基本信息和粉絲列表follower和關注的用戶列表followees,後來在完成第一個版本的開發後,發現若是已經查詢過的用戶再次查詢後還會再次爬取和寫入數據,這個就比較消耗服務器的資源了,而後就增添了一個子表searchSchema.js用來存放已查詢過用戶的狀態

mongodb/schema.js

const mongoose = require('./config')
const Schema = mongoose.Schema

let jueJinUser = new Schema({
	uid: {type:String,unique:true,index: true,}, // 用戶Id
	username: String, // 用戶名
	avatarLarge: String, // 頭像
	jobTitle: String, // 職位
	company: String, // 公司
	createdAt: Date, // 帳號註冊時間
	
	rankIndex: Number, // 排名,級別
	juejinPower: Number, // 掘力值
	postedPostsCount: Number, // 發佈文章數
	totalCollectionsCount: Number, // 得到點贊數
	totalCommentsCount: Number, // 得到評論總數
	totalViewsCount: Number, // 文章被閱讀數
	
	subscribedTagsCount: Number, // 關注標籤數
	collectionSetCount: Number, // 收藏集數
	
	likedPinCount: Number, // 點讚的沸點數
	collectedEntriesCount: Number, // 點讚的文章數
	pinCount: Number, // 發佈沸點數
	
	postedEntriesCount: Number, // 分享文章數
	
	purchasedBookletCount: Number, // 購買小冊數
	bookletCount: Number, // 撰寫小冊數
	
	followeesCount: Number, // 關注了多少人
	followersCount: Number, // 關注者
	
	level: Number, // 等級
	
	topicCommentCount: Number, // 話題被評論數
	viewedEntriesCount: Number, // 猜想是主頁瀏覽數
	
	followees: {type:Array,default: []}, // 存放你關注的列表
	follower: {type:Array,default: []} // 存放粉絲列表
})

module.exports = mongoose.model('JueJinUser', jueJinUser)
複製代碼

mongodb/searchSchema.js

const mongoose = require('./config')
const Schema = mongoose.Schema

// 掘金用戶查詢表: 記錄已經查詢過的用戶,防止重複爬取數據,同時記錄爬取狀態
let JueJinSearch = new Schema({
	uid: {type:String,unique:true,index: true,}, // 用戶Id
	follower: Boolean, // 是否查詢過粉絲
	followees: Boolean, // 是否查詢過關注用戶
	followerSpider: String, // 粉絲爬取狀態  success 爬取完成  loading 爬取中  none 未爬取
	followeesSpider: String // 關注用戶爬取狀態  success 爬取完成  loading 爬取中  none 未爬取
})

module.exports = mongoose.model('JueJinSearch', JueJinSearch)
複製代碼

koa路由配置

目前提供了5個接口

  • /api/getUserFlower:爬取粉絲列表
  • /api/getUserFlowees:爬取關注的用戶列表
  • /api/getSpiderStatus:獲取爬取狀態
  • /api/getCurrentUserInfo:獲取查詢用戶的基本信息
  • /api/getAnalyzeData: 獲取分析數據

config/koa.js

const Koa = require("koa")
const Router = require("koa-router")
const path = require('path')
const bodyParser = require('koa-bodyparser')
const koaStatic = require('koa-static')
const ctrl = require("../controller/index")
const app = new Koa()
const router = new Router()
const publicPath = '../public'
app.use(bodyParser())
app.use(koaStatic(
	path.join(__dirname, publicPath)
))
router.post('/api/getUserFlower', async(ctx, next) => { // 爬取並寫入關注者信息
	let body = ctx.request.body;
	let res = await ctrl.spiderFlowerList(body);
	ctx.response.status = 200;
	ctx.body = { code: 200, msg: "ok", data: res.data }
	next()
})

router.post('/api/getUserFlowees', async(ctx, next) => { // 爬取並寫入關注信息
	let body = ctx.request.body;
	let res = await ctrl.spiderFloweesList(body);
	ctx.response.status = 200;
	ctx.body = { code: 200, msg: "ok", data: res.data }
	next()
})

router.post('/api/getSpiderStatus', async(ctx, next) => { // 獲取爬取狀態
	let body = ctx.request.body;
	let res = await ctrl.spiderStatus(body);
	ctx.response.status = 200;
	ctx.body = { code: 200, msg: "ok", data: res.data }
	next()
})

router.post('/api/getCurrentUserInfo', async(ctx, next) => { // 獲取當前用的基本信息
	let body = ctx.request.body;
	let res = await ctrl.getUserInfo(body)
	ctx.response.status = 200;
	ctx.body = { code: 200, msg: "ok", data: res }
	next()
})
router.post('/api/getAnalyzeData', async(ctx, next) => { // 獲取你的關注者分析數據
	let body = ctx.request.body;
	let res = await ctrl.getAnalyze(body)
	ctx.response.status = 200;
	ctx.body = { code: 200, msg: "ok", data: res }
	next()
})

const handler = async(ctx, next) => {
	try {
		await next();
	} catch (err) {
		console.log('服務器錯誤',err)
		ctx.respose.status = 500;
		ctx.response.type = 'html';
		ctx.response.body = '<p>出錯啦</p>';
		ctx.app.emit('error', err, ctx);
	}
}

app.use(handler)
app.on('error', (err) => {
	console.error('server error:', err)
})

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(9080, () => {
	console.log('juejinAnalyze is starting at port 9080')
	console.log('please Preview at http://localhost:9080')
})
複製代碼

controller

controller裏是最主要的爬取和插入數據的邏輯,標準的後端項目應該再拆分一層服務用來給controller調用,可是因爲項目比較小,這裏就沒有作拆分。重要的邏輯部分在代碼中都有註釋。因爲趁着週末兩天作的項目,因此這裏的邏輯有些比較臃腫,後期會慢慢優化一下,有興趣的也能夠fork下來後自行修改爲本身想要的效果。

const {request} = require("../config/superagent")
const constant = require("../untils/constant")
const model = require("../mongodb/model")

function getLastTime(arr) {
	let obj = arr.pop()
	return obj.createdAtString
}

// 爬取用戶信息並插入到mongodb
// @ids 用戶id  @token token @tid 關注者用戶id
async function spiderUserInfoAndInsert(ids, token, tid, type) {
	let url = constant.get_user_info
	let param = {
		token: token,
		src: constant.src,
		ids: ids,
		cols: constant.cols
	}
	try {
		let data = await request(url, 'GET', param)
		let json = JSON.parse(data.text)
		let userInfo = json.d[ids]
		let insertData = {
			uid: userInfo.uid,
			username: userInfo.username,
			avatarLarge: userInfo.avatarLarge,
			jobTitle: userInfo.jobTitle,
			company: userInfo.company,
			createdAt: userInfo.createdAt,
			rankIndex: userInfo.rankIndex, // 排名,級別
			juejinPower: userInfo.juejinPower, // 掘力值
			postedPostsCount: userInfo.postedPostsCount, // 發佈文章數
			totalCollectionsCount: userInfo.totalCollectionsCount, // 得到點贊數
			totalCommentsCount: userInfo.totalCommentsCount, // 得到評論總數
			totalViewsCount: userInfo.totalViewsCount, // 文章被閱讀數
			subscribedTagsCount: userInfo.subscribedTagsCount, // 關注標籤數
			collectionSetCount: userInfo.collectionSetCount, // 收藏集數
			likedPinCount: userInfo.likedPinCount, // 點讚的沸點數
			collectedEntriesCount: userInfo.collectedEntriesCount, // 點讚的文章數
			pinCount: userInfo.pinCount, // 發佈沸點數
			postedEntriesCount: userInfo.postedEntriesCount, // 分享文章數
			purchasedBookletCount: userInfo.purchasedBookletCount, // 購買小冊數
			bookletCount: userInfo.bookletCount, // 撰寫小冊數
			followeesCount: userInfo.followeesCount, // 關注了多少人
			followersCount: userInfo.followersCount, // 關注者
			level: userInfo.level, // 等級
			topicCommentCount: userInfo.topicCommentCount, // 話題被評論數
			viewedEntriesCount: userInfo.viewedEntriesCount, // 猜想是主頁瀏覽數
		}
		await model.user.insert(insertData)
		if (ids !== tid) {
			if (type === 'followees') {
				updatefollower(ids, tid) // 更新關注你的用戶列表
				updatefollowees(tid, ids) // 更新你關注用戶的列表
			} else {
				updatefollower(tid, ids) // 更新關注你的用戶列表
				updatefollowees(ids, tid) // 更新你關注用戶的列表
			}
		}
		return 'ok'
	} catch (e) {
		console.log('用戶信息獲取失敗',ids, e,)
	}
}

// 更新用戶的關注列表
// @uId 用戶id @tId 關注的用戶Id
async function updatefollowees(uId, tId) {
	let data = {
		uid: uId,
		followUid: tId
	}
	model.followees.updatefollowees(data)
}

// 更新用戶的被關注列表
// @uId 關注的用戶id @tId 被關注的用戶Id
async function updatefollower(uId, tId) {
	let data = {
		uid: uId,
		followUid: tId
	}
	model.follower.updatefollower(data)
}

// 爬取用戶的關注者列表
// @uid 用戶的id @token token @before 循環獲取關注列表的必須參數,取上一組數據中最後一個數據的關注時間
async function getFollower(uid, token, before) {
	let param = {
		uid: uid,
		src: constant.src
	}
	if (before) {
		param.before = before
	}
	try {
		let url = constant.get_follow_list
		let list = await request(url, 'GET', param)
		let followList = list.body.d
		followList.forEach(async function (item) { // 循環獲取關注者的信息
			await spiderUserInfoAndInsert(item.follower.objectId, token, uid, 'follower')
		})
		if (followList&&followList.length === 20) {  // 獲取的數據長度爲20繼續爬取
			let lastTime = getLastTime(followList)
			await updateSpider(uid, 'followerSpider', 'loading') // 更新爬取狀態爲loading
			await getFollower(uid, token, lastTime)
		} else {
			await updateSpider(uid, 'follower', true) // 設置已經爬取標誌
			await updateSpider(uid, 'followerSpider', 'success') // 更新爬取狀態爲success
		}
	} catch (err) {
		console.log('獲取粉絲列表失敗',err)
		return {data: err}
	}
}

// 更新爬取狀態與結果
// @uid 用戶id @key 更新的字段 @value 更新的值
async function updateSpider(uid, key, value) {
	let condition = {
		uid: uid,
		key: key,
		value: value
	}
	model.search.update(condition)
}

// 爬取你關注的列表
// @uid 用戶的id @token token @before 循環獲取關注列表的必須參數,取上一組數據中最後一個數據的關注時間
async function getFollowee(uid, token, before) {
	let param = {
		uid: uid,
		src: constant.src
	}
	if (before) {
		param.before = before
	}
	try {
		let url = constant.get_followee_list
		let list = await request(url, 'GET', param)
		let followList = list.body.d
		followList.forEach(async function (item) { // 循環獲取關注者的信息
			await spiderUserInfoAndInsert(item.followee.objectId, token, uid, 'followees')
		})
		if (followList.length === 20) {
			let lastTime = getLastTime(followList)
			await updateSpider(uid, 'followeesSpider', 'loading') // 更新爬取狀態爲loading
			await getFollowee(uid, token, lastTime)
		} else {
			await updateSpider(uid, 'followees', true) // 設置已經爬取標誌
			await updateSpider(uid, 'followeesSpider', 'success') // 更新爬取狀態爲loading
		}
	} catch (err) {
		console.log('獲取關注者列表失敗',err)
		return {data: err}
	}
}

// 用戶數據分析
// @uid 用戶id  @top 可配置選取前多少名  @type 獲取數據類型:粉絲 follower 關注的人 followees
async function getTopData(uid, top, type) {
	let data = {
		uid: uid,
		top: parseInt(top),
		type: type
	}
	try {
		let article = model.analyze.getTopUser(data, 'postedPostsCount')
		let juejinPower = model.analyze.getTopUser(data, 'juejinPower')
		let liked = model.analyze.getTopUser(data, 'totalCollectionsCount')
		let views = model.analyze.getTopUser(data, 'totalViewsCount')
		let follower = model.analyze.getTopUser(data, 'followersCount')
		let level = model.analyze.getLevelDistribution(data)
		let obj = {
			postedPostsCount: await article,
			juejinPower: await juejinPower,
			totalCollectionsCount: await liked,
			totalViewsCount: await views,
			followersCount: await follower,
			level: await level
		}
		return obj
	} catch (err) {
		console.log('err', err)
		return err
	}
}

module.exports = {
	spiderFlowerList: async (body) => {  // 獲取用戶的關注者列表
		let uid = body.uid
		let token = body.token
		let searchStatus = await model.search.findOrInsert({uid: uid})
		if (searchStatus.followerSpider == 'success') {
			return {data: 'success'}
		} else if (searchStatus.followerSpider == 'loading') {
			return {data: 'loading'}
		} else if (searchStatus.followerSpider == 'none') {
			spiderUserInfoAndInsert(uid, token, uid) // 把本身的信息也插入mongodb
			getFollower(uid, token)
			return {data: 'none'}
		}
	},
	spiderFloweesList: async (body) => { // 獲取用戶的關注列表
		let uid = body.uid
		let token = body.token
		let searchStatus = await model.search.findOrInsert({uid: uid})
		if (searchStatus.followeesSpider == 'success') {
			return {data: 'success'}
		} else if (searchStatus.followeesSpider == 'loading') {
			return {data: 'loading'}
		} else if (searchStatus.followeesSpider == 'none') {
			spiderUserInfoAndInsert(uid, token, uid) // 把本身的信息也插入mongodb
			getFollowee(uid, token)
			return {data: 'none'}
		}
	},
	spiderStatus: async (body) => {
		let uid = body.uid
		let type = body.type + 'Spider'
		let spiderStatus = await model.search.getSpiderStatus({uid: uid, type: type})
		if (spiderStatus[type] === 'loading' || spiderStatus[type] === 'none') {
			return {data: false}
		} else if (spiderStatus[type] === 'success') {
			return {data: true}
		}
	},
	getUserInfo: async (body) => { // 獲取當前用戶基本信息
		let uid = body.uid
		let data = {
			uid: uid
		}
		let result = await model.user.getUserInfo(data)
		return result
	},
	getAnalyze: async (body) => { // 獲取關注者數據分析
		let uid = body.uid
		let top = body.top
		let type = body.type
		let res = await getTopData(uid, top, type)
		return res
	}
}

複製代碼

頁面設計

爲了擺脫最初時候的殺馬特形象,此次採用了比較流行的大數據面板展現。不過整個頁面的設計主要歸功於擁有一個作設計的女盆友(沒有任何撒糧的行爲😆,主要是爲了感謝),在這裏感謝一下提供幫助的女盆友😂,感謝犧牲週末時間陪我改設計圖。 另外因爲考慮到不一樣屏幕適配問題,在前端代碼上只採用了等比縮小放大效果,因此有的屏幕下顯示會有點變形,這屬於正常狀況。

分析截圖

看了本身的概況後發現已經加入掘金851天了,發佈文章12篇,發佈沸點11條,得到關注者211位,感謝各位關注個人用戶。

最後

作完整個小項目後,最大的想法就是把整個過程寫下來,不只是分享給你們,更是從新回顧整個項目過程當中遇到的問題和當時解決問題的方法有何改進之處。但願你們可以喜歡這個項目,同時也提醒一下你們不要拿着個項目作壞事啊,這個項目主要是用來技術交流和幫助你們查看一下粉絲和關注的人的數據分析。若是在使用過程當中遇到任何問題均可以在下方留言,或者直接加微信聯繫我,若是看到了我會及時回覆。

項目地址:

github:github.com/gengchen528… (若是喜歡的話,歡迎給個star)

同時也歡迎關注個人公衆號,輕發語音會有驚喜。不按期分享文章~~

我的博客:www.xkboke.com

我的微信:

相關文章
相關標籤/搜索