後端
申請令牌
先至 GitHub 的 Personal access tokens 頁面申請一個存取令牌。
工具
文件
查詢語法
由於 GraphQL 的客戶端比較單純,因此直接使用字串替換的方式去改變一個 GraphQL 的請求。
比方說有一個 owners.graphql
檔,可以查詢一般使用者或組織:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| query Owners { search(<SearchArguments>) { edges { cursor node { ... on User { imageUrl: avatarUrl createdAt followers { totalCount } location login name } ... on Organization { imageUrl: avatarUrl createdAt location login name } } } pageInfo { endCursor hasNextPage } } rateLimit { cost limit nodeCount remaining resetAt used } }
|
將這個檔案讀取後,利用字串替換的方式把 <SearchArguments>
標籤替換掉就可以了。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func (q Query) String() string { query := q.Schema query = strings.Replace(query, "<Type>", q.Type, 1) query = strings.Replace(query, "<SearchArguments>", util.ParseStruct(q.SearchArguments, ","), 1) query = strings.Replace(query, "<OwnerArguments>", util.ParseStruct(q.OwnerArguments, ","), 1) query = strings.Replace(query, "<GistsArguments>", util.ParseStruct(q.GistsArguments, ","), 1) query = strings.Replace(query, "<RepositoriesArguments>", util.ParseStruct(q.RepositoriesArguments, ","), 1)
payload := struct { Query string `json:"query"` }{ Query: query, } b, err := json.Marshal(payload) if err != nil { log.Fatal(err.Error()) }
return string(b) }
|
蒐集資料
由於 GitHub 的「搜尋」(search
)端點,並不允許開發者一次撈取所有的歷史資料,即使有分頁,最多也只有 10 頁。因此需要指定一個時間區間。如果要蒐集 GitHub 上所有的一般使用者或組織,就得從 GitHub 創始的時間開始蒐集。
以蒐集組織的資料為例,使用一個遞迴方法從 2007 年 10 月 1 日開始蒐集:
1 2 3 4 5 6 7 8 9 10 11
| func (o *Organization) Travel() error { if o.From.After(o.To) { return nil }
o.From = o.From.AddDate(0, 0, 7)
return o.Travel() }
|
由於還需要蒐集每個組織各自的儲存庫(repository),因此還需要使用一個遞迴方法去蒐集。這個部分就沒有 10 頁的限制,只需要利用分頁指標(cursor)不斷切換下一頁就可以:
1 2 3 4 5 6 7 8 9 10 11
| func (o *Organization) FetchRepositories(repositories *[]model.Repository) error { if !res.Data.Organization.Repositories.PageInfo.HasNextPage { o.RepositoryQuery.RepositoriesArguments.After = "" return nil } o.RepositoryQuery.RepositoriesArguments.After = strconv.Quote(res.Data.Organization.Repositories.PageInfo.EndCursor)
return o.FetchRepositories(repositories) }
|
速度限制
GitHub GraphQL API 有速度限制,因此需要稍微控制一下速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func (r RateLimit) Throttle(collecting int64) { logger.Debug(fmt.Sprintf("Rate Limit: %s", strconv.Quote(util.ParseStruct(r, " ")))) resetAt, err := time.Parse(time.RFC3339, r.ResetAt) if err != nil { log.Fatal(err.Error()) } remainingTime := resetAt.Add(time.Second).Sub(time.Now().UTC()) time.Sleep(time.Duration(remainingTime.Milliseconds()/r.Remaining*collecting) * time.Millisecond) if r.Remaining > collecting { return } logger.Warning("Take a break...") time.Sleep(remainingTime) }
|
解析地理位置
由於每個一般使用者和組織的地理位置都是自由填寫的,所以需要去解析這個帳號所填寫的地理位置究竟是哪個國家和城市,因此需要自行準備一個地區列表和解析方法來處理。
排名語法
使用 MongoDB 的聚合(aggregation),可以利用各種管道(pipeline)完成排名。以一般使用者的排名管道為例,需要針對追蹤者數量、程式碼片段和儲存庫,在不同地理位置和程式語言的條件下進行排名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func RankUser() (pipelines []*Pipeline) { rankType := app.TypeUser fields := []string{ "followers", "gists.forks", "gists.stargazers", "repositories.forks", "repositories.stargazers", "repositories.watchers", } for _, field := range fields { pipelines = append(pipelines, rankByField(rankType, field)) pipelines = append(pipelines, rankByLocation(rankType, field)...) } pipelines = append(pipelines, rankOwnerRepositoryByLanguage(rankType, "repositories.stargazers")...) pipelines = append(pipelines, rankOwnerRepositoryByLanguage(rankType, "repositories.forks")...) pipelines = append(pipelines, rankOwnerRepositoryByLanguage(rankType, "repositories.watchers")...) return }
|
每個排名管道都經過封裝過:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func rankByField(rankType string, field string) *Pipeline { return &Pipeline{ Pipeline: &mongo.Pipeline{ operator.Project(bson.D{ id(), imageUrl(), totalCount(field), }), operator.Sort("total_count", descending), }, Type: rankType, Field: field, } }
|
根據計算,一般使用者有 26,325 個管道,組織有 14,010 個管道,而儲存庫有 1,698 個管道需要被執行。
排名時間戳
當每一次執行排名時,都相當耗時。為了可以讓前端一直取得正確的排名資訊,當前的排名不可以被覆蓋或刪除。每一次有新的排名被存入資料庫,都會產生一個時間戳來指定這批排名資料已經完成,並且可以被使用。而這個時間戳在每一次排名完成後,就會被寫進環境變數檔裡,如此一來可以確保排名資料的原子性。
查詢
所有的排名資料都是相同的格式:
1 2 3 4 5 6 7 8 9 10 11 12 13
| { "_id" : "", "name" : "", "image_url" : "", "rank" : 0, "rank_count" : 0, "item_count" : 0, "type" : "", "field" : "", "language" : "", "location" : "", "created_at" : "" }
|
快取
快取的部分暫時使用 in-memory 類型的快取套件進行處理。
資料庫
資料庫使用 MongoDB,排名資料表的部分,有為以下 5 個欄位特別建立索引:
name
type
language
location
created_at
前端
前端的部分使用 Vue 和 Vuetify 進行實作。需要注意使用名字去查詢的時候,需要實作去抖(debounce),不要一直呼叫 API。由於後端有時回覆較慢,也需要特別去撤銷(cancel)被覆蓋的 HTTP 請求。
共用資源
前後端有一模一樣的共用資源,像是語言列表和地區列表,這個部分可以使用 Git Submodules 處理。
網站
程式碼