4

手摸手制作一份 2019 年 GitHub 年度报告

 3 years ago
source link: http://jalan.space/2019/12/31/2019/2019-github-annual-report/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

手摸手制作一份 2019 年 GitHub 年度报告

0 Comments

我们即将与 2019 挥手作别,踏入崭新的 2020。一到年末,各个平台都在整理数据,出具一份属于自己平台的「年度报告」。而对于技术人而言,如果你是一位开源爱好者,GitHub 的年度报告就是你 2019 年的技术总结。

阮一峰老师曾在科技爱好者周刊中提到「数据的力量」:

GitHub 个人页有一个日历栏目,只要当天有代码提交,那一天的小方格就会变成绿色。如果这一年,你每天编码,日历就全是绿的,否则就会有白色的小方块。所有人都可以看到这个「编码日历」。很多人为了让绿色小方格子不要中断,就会尽量每天提交代码。时间一长,真的多做了不少项目。

因此,这次年度报告我想主要针对这份「编码日历」,把你的「编码日历」组装到一张图片上展示给别人。

因为前一段时间正好在学习 GraphQL,所以将通过 GitHub 的接口 GitHub GraphQL API v4 来获取相关的用户数据。

这份年度报告涉及到的主要技术:

  • GraphQL
  • Python
    • requests(发起请求)
    • PIL: Image/ImageDraw/ImageFont(图片处理)
    • werobot(接入微信公众号)

在开始 Coding 之前需要先梳理一下需求。生成报告的整个流程大致如下:

项目流程图

因此,需要做的事包括:

  1. 调通 GitHub GraphQL API v4,获取到需要的数据
  2. 对数据进行统计整理
  3. 设计一份年度报告
  4. 结合整理后的数据生成报告,并将最终报告返回给用户
  5. 接入微信公众平台,走通整个流程

何为 GraphQL?

因为要通过 GitHub GraphQL API v4 获取数据,所以先来聊聊 GraphQL。

官方对于 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": "[email protected]",
"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. 用户在 2019 年每日的贡献情况
  2. 用户 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 仓库:https://github.com/JalanJiang/2019-github-annual-report

接入的服务器为辣鸡配置,还请各位大佬手下留情。

整个过程涉及到微信公众号和 GitHub 接口的调用,用户从输入到数据返回需要等待几秒的时间。为了避免超时的尴尬情况,这里只对用户提交记录做了简单的分析。

在完成这个项目的过程中几度因为设计出的报告太丑而想要放弃,感谢几位朋友一直鼓励我、给我提出修改意见才让我坚持了下来。

2019 年再见啦,希望 2020 年能尝试更多有趣的事情。:)

  • 本文作者:江五渣。欢迎关注我的公众号:「编程拯救世界」,在编程世界一起冒险,一起成长!
  • 本文链接:http://jalan.space/2019/12/31/2019/2019-github-annual-report/
  • 版权声明:本博客所有文章除特别声明外,均采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。
分享

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK