koa+mongodb打造掘金關注者分析面板
最近掘金更新了掘力值和等級規則,大部分使用者都帶上了等級徽章,而且每個人的掘力值也都很清晰明瞭,我想這也是掘金激勵使用者輸出高質量文章的一種方式,當看到自己掘力值不斷增長和等級不斷升高的時候,想必內心都會有種成就感。看到自己的掘力值後,發現自己還需要繼續努力,繼續分享更多自己的開發經驗和好的想法。
那麼這次又要搞什麼事情呢?賣個關子先放張效果圖

idea
看過我文章的朋友應該感覺到了,我喜歡分享自己做一個小專案或者小工具的經驗,分享針對細節或者某個知識點的內容很少。我想這個和自己的愛好有很大關係,我喜歡從零完成一個專案,從自己的一個想法到原型繪製,然後到UI設計,接著使用自己熟悉的語言寫前後端程式碼,然後到前後端聯調,最後優化加部署到伺服器。這一系列的過程我感覺自己能學到更多的東西,也能從這一系列流程中向外擴充套件很多知識點和發現做專案中自己容易忽略的細節。
這次的掘金粉絲(其實也不能說粉絲,主要是關注者和關注了不好區分:joy:)分析工具也是我的一個突發奇想,因為加入掘金寫的第一篇文件就是 《掘金最熱文章收藏評論分析》 ,但那個專案也只是簡單的獲取文章的基礎資料,其實也談不上分析,而且現在回過頭看介面簡直有點粗糙(感覺就像回看初中殺馬特般的照片一樣,哈哈)。所以這次就趁著掘金把掘力值和等級上線,來一個個人資料分析,其實主要是粉絲資料分析和關注的使用者分析。
主要功能
- 根據使用者ID獲取使用者的粉絲或關注的使用者資料
- 分析粉絲或關注使用者,釋出文章、文章獲贊、文章閱讀數、粉絲數、掘力值TOP10
- 分析粉絲或關注使用者等級分佈
- 個人成就面板
- 更多分析功能後續開發中...(期待你的建議)
體驗與原始碼
體驗地址: juejinfan.xkboke.com

為了更好的方便大家體驗,目前已經部署到我的伺服器了,可以通過juejinfan.xkboke.com來訪問,一定要是https,由於伺服器頻寬限制,可能剛開始載入會比較慢,請耐心等待,或者你可以直接把原始碼部署在本地,這樣速度會快一點。如果你的粉絲比較多的話,在點選分析後也會等較長時間,不過點選後,你可以等四五分鐘後再來看,資料會在爬取完成後立馬加載出來的。
github: github.com/gengchen528… (如果喜歡的話,歡迎給個star)
安裝
前提要安裝好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
: 需要獲取的使用者資訊(預設)

未登入狀態

登入後

所需引數
粉絲及關注的使用者列表獲取
粉絲列表及關注的使用者列表最初的時候遇到了很多問題,因為剛開始找到介面後,發現並不是簡單的分頁,每次只能獲取20條資料。而且每次引數都不相同,第一次獲取會發現基本的引數只有三個 uid
, currentUid
, src
,但是在載入下一頁資料時會發現多了一個引數 before
,那麼這個 before
是怎麼來的呢。剛開始的時候為了找到這個規律,我請求了數十次,並且把每次的 before
引數寫出來,最後發現竟然沒有一點規律,我瞬間懷疑了人生:unamused:,難道我的想法就這麼夭折了麼。還好當我開啟每個陣列找規律時,發現了原來 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('pleasePreview athttp://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 } } 複製程式碼
頁面設計
為了擺脫最初時候的殺馬特形象,這次採用了比較流行的大資料面板展示。不過整個頁面的設計主要歸功於擁有一個做設計的女盆友(沒有任何撒糧的行為:laughing:,主要是為了感謝),在這裡感謝一下提供幫助的女盆友:joy:,感謝犧牲週末時間陪我改設計圖。 另外由於考慮到不同螢幕適配問題,在前端程式碼上只採用了等比縮小放大效果,所以有的螢幕下顯示會有點變形,這屬於正常情況。
分析截圖



看了自己的概況後發現已經加入掘金851天了,釋出文章12篇,釋出沸點11條,獲得關注者211位,感謝各位關注我的使用者。
最後
做完整個小專案後,最大的想法就是把整個過程寫下來,不僅是分享給大家,更是重新回顧整個專案過程中遇到的問題和當時解決問題的方法有何改進之處。希望大家能夠喜歡這個專案,同時也提醒一下大家不要拿著個專案做壞事啊,這個專案主要是用來技術交流和幫助大家檢視一下粉絲和關注的人的資料分析。如果在使用過程中遇到任何問題都可以在下方留言,或者直接加微信聯絡我,如果看到了我會及時回覆。
專案地址:
github: github.com/gengchen528… (如果喜歡的話,歡迎給個star)
同時也歡迎關注我的公眾號,輕發語音會有驚喜。不定期分享文章~~

個人部落格:www.xkboke.com
個人微信:
