手摸手製做一份 2019 年 GitHub 年度報告

前言

咱們即將與 2019 揮手道別,踏入嶄新的 2020。一到年底,各個平臺都在整理數據,出具一份屬於本身平臺的「年度報告」。而對於技術人而言,若是你是一位開源愛好者,GitHub 的年度報告就是你 2019 年的技術總結。html

阮一峯老師曾在科技愛好者週刊中提到「數據的力量」:node

GitHub 我的頁有一個日曆欄目,只要當天有代碼提交,那一天的小方格就會變成綠色。若是這一年,你天天編碼,日曆就全是綠的,不然就會有白色的小方塊。全部人均可以看到這個「編碼日曆」。不少人爲了讓綠色小方格子不要中斷,就會盡可能天天提交代碼。時間一長,真的多作了很多項目。python

所以,此次年度報告我想主要針對這份「編碼日曆」,把你的「編碼日曆」組裝到一張圖片上展現給別人。git

由於前一段時間正好在學習 GraphQL,因此將經過 GitHub 的接口 GitHub GraphQL API v4 來獲取相關的用戶數據。github

這份年度報告涉及到的主要技術:json

  • GraphQL
  • Python
    • requests(發起請求)
    • PIL: Image/ImageDraw/ImageFont(圖片處理)
    • werobot(接入微信公衆號)

需求確立

在開始 Coding 以前須要先梳理一下需求。生成報告的整個流程大體以下:後端

項目流程圖

所以,須要作的事包括:api

  1. 調通 GitHub GraphQL API v4,獲取到須要的數據
  2. 對數據進行統計整理
  3. 設計一份年度報告
  4. 結合整理後的數據生成報告,並將最終報告返回給用戶
  5. 接入微信公衆平臺,走通整個流程

數據獲取

何爲 GraphQL?

由於要經過 GitHub GraphQL API v4 獲取數據,因此先來聊聊 GraphQL。bash

官方對於 GraphQL 的定義是:服務器

一種用於 API 的查詢語言,是一個使用基於類型系統來執行查詢的服務端運行時(類型系統由你的數據定義)。

這樣說很抽象,你們可能對 RESTful 比較熟悉些,那麼咱們就拿 GitHub REST API v3 與 GitHub GraphQL API v4 獲取數據的方式作一個簡單的對比,GraphQL 的特色天然就一目瞭然。

以獲取用戶數據爲例,相關接口文檔:

對於 RESTful 風格而言,天然是要發起一個 GET 請求。因爲咱們要獲取某個指定用戶的數據,因此須要在 PATH 中指定 :username

GET /users/:username
複製代碼

請求成功後 GitHub 將會返回如下數據:

{
  "login": "octocat",
  "id": 1,
  "node_id": "MDQ6VXNlcjE=",
  "avatar_url": "https://github.com/images/error/octocat_happy.gif",
  "gravatar_id": "",
  "url": "https://api.github.com/users/octocat",
  "html_url": "https://github.com/octocat",
  "followers_url": "https://api.github.com/users/octocat/followers",
  "following_url": "https://api.github.com/users/octocat/following{/other_user}",
  "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
  "organizations_url": "https://api.github.com/users/octocat/orgs",
  "repos_url": "https://api.github.com/users/octocat/repos",
  "events_url": "https://api.github.com/users/octocat/events{/privacy}",
  "received_events_url": "https://api.github.com/users/octocat/received_events",
  "type": "User",
  "site_admin": false,
  "name": "monalisa octocat",
  "company": "GitHub",
  "blog": "https://github.com/blog",
  "location": "San Francisco",
  "email": "octocat@github.com",
  "hireable": false,
  "bio": "There once was...",
  "public_repos": 2,
  "public_gists": 1,
  "followers": 20,
  "following": 0,
  "created_at": "2008-01-14T04:33:35Z",
  "updated_at": "2008-01-14T04:33:35Z"
}
複製代碼

但有時咱們不須要這麼多的數據,咱們可能只想獲取用戶的頭像地址。在 RESTful 風格的接口下,咱們沒法只獲取某一條數據,但對於 GraphQL 接口,咱們能夠發起這樣一條請求:

{
    user(login: "username") {
        avatarUrl
    }
}
複製代碼

這樣一來,服務端將根據咱們請求數據的格式,返回給咱們對應的字段,即僅返回 user 下的 avatarUrl 數據:

{
    "data":{
        "user":{
            "avatarUrl":"url"
        }
    }
}
複製代碼

在 RESTful 中,咱們被迫接收服務端已組裝好的數據,但 GraphQL 給了咱們更多的自由,讓咱們能夠只取所需。

除此以外,RESTful 以資源劃分接口,數據之間相對離散,若是想請求不一樣的資源則須要發起屢次請求。而 GraphQL 的數據更具總體性,資源之間以(即 Graph 名稱的由來)的形式彼此關聯,一次請求便可獲取多種資源。

構造 GraphQL 請求

我想要獲取的數據主要有:

  1. 用戶名
  2. 用戶在 2019 年每日的貢獻狀況
  3. 用戶 Followers 數量

根據接口文檔 UserContributionsCollection 可知,這些數據都在 user 中,對應的字段以下:

  • 用戶暱稱:name
  • Followers 數量:followers.totalCount
  • 編碼日曆:contributionsCollection.contributionCalendar
    • 總貢獻數量:totalContributions
    • 每週貢獻狀況:weeks
      • 每日貢獻狀況:contributionDays
        • 當天日曆顏色:color
        • 當天貢獻數:contributionCount
        • 當天日期:date

所以,能夠構造出以下 query

query = """ { user(login: "%s") { followers { totalCount } name contributionsCollection( from: "%s", to: "%s" ) { contributionCalendar { totalContributions weeks { contributionDays { color contributionCount date } } } } } } """% (github_id, begin, end)
複製代碼

構造好 query 後,咱們使用 requests 發起請求:

import requests


access_token = "xxx"

# 請求 headers 帶上 access_token
headers = {"Authorization": "bearer %s" % access_token}

# 發起請求
response = requests.post(
    "https://api.github.com/graphql",
    headers=headers,
    json={'query': query}
)
複製代碼

若請求成功,GitHub 會返回以下格式的 JSON 數據:

{
    "data":{
        "user":{
            "name":"江不知",
            "followers":{
                "totalCount":71
            },
            "contributionsCollection":{
                "contributionCalendar":{
                    "totalContributions":2234,
                    "weeks":[
                        {
                            "contributionDays":[
                                {
                                    "color":"#c6e48b",
                                    "contributionCount":30,
                                    "date":"2019-01-01"
                                }
                            ]
                        }
                    ]
                }
            }
        }
    }
}
複製代碼

數據統計

我主要針對 weeks 作了一些簡單的數據統計。主要包括:

  • 有提交代碼的天數(contributionCount > 0
  • 連續提交代碼的最大天數
  • 完成貢獻次數最多的日期

這些數據對 weeks 進行一次遍歷便可得出,在此很少作贅述。

設計報告

做爲一個後端開發,真的沒有多少設計天賦,說多了都是淚……

整份報告大體分紅三個區域:

  1. 頭部 Title
  2. Title 下的「編碼日曆」
  3. 中間部分顯示一些分析數據
  4. 底部宣示主權

反反覆覆改了多版,詢問了不少朋友的意見,最後的結果依舊不是很好看……

年度報告設計最終版

數據拼接

報告設計完成之後就能夠把最終要展現的數據拼接到報告上了。

繪製「編碼日曆」

在遍歷 weeks 統計數據的過程當中,能夠順便完成「編碼日曆」的繪製。

「編碼日曆」中的每一天就是一個小方塊,方塊的顏色咱們已經從接口返回數據的 color 字段中獲取到了。我選擇使用 line() 繪製一條顏色爲 color 的直線表明方塊,把直線的 width 加粗,以得到方塊的效果。

from PIL import Image, ImageDraw

# 打開圖片
f = open(self.IMAGE_FILE_PATH, 'rb')
image = Image.open(f)
# 建立一個 draw 實例
drawImage = ImageDraw.Draw(image)

# 遍歷每週數據
for week in weeks:
    # 遍歷每日數據
    for day in week['contributionDays']:
        # 取出當天的顏色
        color = day['color'] 
        # 繪製直線
        drawImage.line([(x_point, y_point), (x_point + square_width, y_point)], fill=color, width=square_width)
        # 改變下一個方格的 y 座標
        y_point += move_width
    # 改變下一個方格的 x 座標
    x_point += move_width
    # 下一週開始,y 座標恢復原處
    y_point = y_begin
複製代碼

粘貼文字

報告的其餘部分就主要是文字內容了,設置好字體、顏色等,使用 text() 在指定位置貼上文字。

from PIL import ImageFont

font_size = 60
# 設置字體與字號
font = ImageFont.truetype("./font/fzlt.ttf", font_size)
font_color = "#F7FFF7"

# 設置座標
x, y = 0

# 在圖片寫上文字
draImage.text((x, y), "要顯示的文字", fill=font_color, font=font)
複製代碼

接入公衆號

公衆號方面直接使用了開發框架 WeRoBot

設定:當用戶發送信息爲「2019 $github_id」時觸發生成年度報告。

import werobot

robot = werobot.WeRoBot(token='token')

# 回覆包含指定文本的信息
@robot.filter(re.compile("2019(\s)+(.*)?"))
def annual_report(message, session, match):
    if match:
        # do something...
複製代碼

生成年度報告後,咱們使用微信的新增臨時素材接口上傳報告圖片,並獲取到臨時素材的編號 media_id

from werobot.client import Client

config = {
    "APP_ID": "app_id",
    "APP_SECRET": "app_secret"
}

client = Client(config)
# 上傳臨時素材
response = client.upload_media('image', image) # image 爲生成的報告圖片
# 獲取臨時素材 ID
media_id = response['media_id']
複製代碼

而後,咱們再將這一圖片信息返回給用戶:

from werobot.replies import ImageReply

# 要返回的圖片數據
reply = ImageReply(message=message, media_id=media_id)
return reply
複製代碼

結果展現

當用戶在公衆號發送 2019+空格+github_id 時,將返回 github_id 所對應的報告。最終生成的報告以下:

個人 2019 GitHub 年度報告

源碼見 GitHub 倉庫:github.com/JalanJiang/…

接入的服務器爲辣雞配置,還請各位大佬手下留情。

總結

整個過程涉及到微信公衆號和 GitHub 接口的調用,用戶從輸入到數據返回須要等待幾秒的時間。爲了不超時的尷尬狀況,這裏只對用戶提交記錄作了簡單的分析。

在完成這個項目的過程當中幾度由於設計出的報告太醜而想要放棄,感謝幾位朋友一直鼓勵我、給我提出修改意見才讓我堅持了下來。

2019 年再見啦,但願 2020 年能嘗試更多有趣的事情。:)

相關文章
相關標籤/搜索