使用事件、项目描述和语言词云监控 GitHub 活动






4.92/5 (7投票s)
近乎实时地监控 GitHub 事件,我们将显示事件、项目描述和项目语言的词云。
引言
又一个有趣的词云小程序来了。在 我之前的文章中,我演示了如何接收 Twitter 推文并在词云中显示结果。这让我很好奇 GitHub 上发生了什么。上面的截图显示了三个词云:
-
事件
-
项目描述
-
语言
Twitter 词云文章中使用的很多代码在这里被重新利用了,所以我只会展示与此应用程序相关的代码。如果您对如何使用力导向图生成词云感兴趣,我建议您阅读之前的文章。
源代码
可以通过克隆获取源代码
https://github.com/cliftonm/githubdashboard.git
GitHub 身份验证
这说起来容易做起来难,尽管代码本身最终非常简单。
简单的方法
讽刺的是,我是在自己摸索出困难的方法并在正要写这篇文章时才发现这个信息的。所以,简单的方法就是:
-
请 遵循这些说明
-
将生成的令牌放在文件 "authorization.txt" 的第三行(前两行可以为空)的 bin\Debug 和/或 bin\Release 文件夹中。
困难的方法
如果你想走困难的路:
-
创建一个 GitHub 应用程序(你会在上面第 1 步的同一页面上看到该选项)
-
将你的客户端 ID 和客户端密钥令牌分别作为第一行和第二行放在 bin\Debug 和/或 bin\Release 文件夹的 "authorization.txt" 文件中。第三行留空。
现在,当你运行应用程序时,它会弹出一个网页浏览器,让你登录到你的 GitHub 账户并授权该应用程序。
在后台,会显示一个包含 WebBrowser 控件的对话框,并且代码会连接到 Navigated 事件。代码还会带你进入 GitHub 的 OAuth 登录页面。
auth = new Authorize(); auth.Show(); auth.browser.Navigated += OnNavigated; auth.browser.Navigate("https://github.com/login/oauth/authorize?scope=user:notifications&client_id=" + clientId);
请注意,这里我们只使用了应用程序的客户端 ID。
登录后,GitHub 会尝试导航到你的应用程序设置中提供的 URL。这里我们使用了技巧 #1,拦截导航事件。
/// <summary> /// Once the user authorizes the application, we get a "code" back from GitHub /// We use that code to obtain the access token. /// </summary> protected void OnNavigated(object sender, WebBrowserNavigatedEventArgs e) { if (e.Url.Query.Contains("?code")) { authCode = e.Url.Query.RightOf("="); WebClient wc = new WebClient(); accessToken = wc.DownloadString("https://github.com/login/oauth/access_token?client_id=" + clientId + "&client_secret=" + secretId + "&code=" + authCode + "&accept=json").Between("=", "&"); auth.Close(); File.WriteAllLines("authorization.txt", new string[] { clientId, secretId, accessToken }); StartQueryThread(); } }
这将为我们提供一个可以用来获取访问令牌的密钥代码。请注意,上面的代码同时使用了应用程序的客户端 ID、客户端密钥以及认证代码。从现在开始,我们就可以使用访问令牌了。
查询 GitHub
查询 GitHub 是在一个工作线程中完成的。我本可以使用 Task 对象、async/await,但那似乎过于复杂。我们需要的是一个连续的后台进程,它查询 "events" API 并协调将词云更新到主应用程序线程。所以我们创建一个后台线程。
protected void StartQueryThread() { queryThread = new Thread(new ThreadStart(QueryGitHubThread)); queryThread.IsBackground = true; queryThread.Start(); }
使用访问令牌
没有访问令牌,每小时只能访问 API 60 次。有了访问令牌,每小时可以访问 API 5000 次。顺便说一句,要查看你还剩下多少访问次数,请在命令行上运行此命令:
curl -i https://api.github.com/events?access_token=[your access token]
在标题中,你会看到两个字段,可以用来验证你的访问速率限制和剩余访问次数。
X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 4999
工作线程
工作线程确保我们不会超过此限制,方法是将 API 调用限制为每秒最多一次(每小时有 3600 秒,我们将低于每小时 5000 次的访问限制)。
protected void QueryGitHubThread() { then = DateTime.Now; while (true) { ElapseOneSecond(); string data = GetData("https://api.github.com/events"); if (!String.IsNullOrEmpty(data)) { ProcessEvents(data); } } }
ElapsedOneSecond 方法执行时间检查。
/// <summary> /// To avoid exceeding the 5000 requests per hour limit, we ensure that we only /// make one request a second (3600 requests per hour) /// </summary> protected void ElapseOneSecond() { int msToSleep = 1000 - (int)(DateTime.Now - then).TotalMilliseconds; then = DateTime.Now; // If there's any time remaining to sleep before our next query, do so now. if (msToSleep > 0) { Thread.Sleep(msToSleep); } }
获取事件数据
弄清楚这里的“技巧”花了好几个小时的挖掘,而且我还在注释中留下了那些不起作用的解决方案,至少在这个例子中是这样。关键信息是必须设置 UserAgent 属性。GitHub 文档中没有清晰地描述这一点,这非常令人沮丧!
protected string GetData(string url) { string ret = String.Empty; HttpWebRequest request = WebRequest.Create(url + "?access_token=" + accessToken) as HttpWebRequest; request.Method = "GET"; // After 3 hours of googling and reading answers on SO, I found that this is necessary. Thank you Budda for posting that info. request.UserAgent = "Hello There"; // Other answers I found regarding the server error response, but that did not solve the problem: // This is unnecessary: // request.Accept = "application/json; charset=utf-8"; // request.KeepAlive = false; // request.ContentType = "application/json; charset=utf-8"; // request.UseDefaultCredentials = true; // Also this, in app.config, was not necessary: //<system.net> // <settings> // <httpWebRequest useUnsafeHeaderParsing="true" /> // </settings> //</system.net> try { using (WebResponse response = request.GetResponse()) { using (StreamReader reader = new StreamReader(response.GetResponseStream())) { ret = reader.ReadToEnd(); } } } catch(Exception ex) { Console.WriteLine(ex); } return ret; }
处理 JSON
Newtonsoft.Json 再次派上了用场,因为我们可以传入事件信息并接收一个 dynamic 对象,我们可以用它来提取一些关键信息。然后我们还要检查项目页面(再次确保我们每秒查询不超过一次),以获取项目描述和语言。
/// <summary> /// Process the event information. Here, we extract the ID so we don't process the same event multiple times. /// We also get the repo URL and query the API for the repo's description and language, which are used to /// populate the other two word clouds. /// </summary> /// <param name="html"></param> protected void ProcessEvents(string html) { dynamic events = JsonConvert.DeserializeObject<List<Object>>(html); foreach (dynamic ev in events) { string id = ev.id.ToString(); if (!eventIdTypeMap.ContainsKey(id)) { string eventType = ev.type.ToString(); eventIdTypeMap[id] = eventType; string repoUrl = ev.repo.url.ToString(); ElapseOneSecond(); // Again, don't overtax the API. string repoData = GetData(repoUrl); if (!String.IsNullOrEmpty(repoData)) { dynamic repoInfo = JsonConvert.DeserializeObject(repoData); string description = repoInfo.description; string language = repoInfo.language; // Don't collide with the WinForm thread's Paint functions. // TODO: Could be optimized a bit to spend less time in the locked state. lock (this) { if (!String.IsNullOrEmpty(eventType)) ++totalEvents; AddOrUpdateNode(eventType, rootNodeEvents, eventsWordNodeMap, () => totalEvents); if (!String.IsNullOrEmpty(language)) ++totalLanguages; AddOrUpdateNode(language, rootNodeLanguages, languagesWordNodeMap, () => totalLanguages); if (!String.IsNullOrEmpty(description)) { description.Split(' ').ForEach(w => { if (!EliminateWord(w)) { // We never show more than 100 description words. if (descriptionsWordNodeMap.Count > 100) { RemoveAStaleWord(descriptionsWordNodeMap); } if (!String.IsNullOrEmpty(w)) ++totalDescriptionWords; AddOrUpdateNode(w, rootNodeDescriptions, descriptionsWordNodeMap, () => totalDescriptionWords); } }); } } } } } }
这驱动着三个词云:事件、描述和语言。
确定节点颜色和字体大小
一个有趣的小细节是如何确定节点的颜色和字体大小,作为给定词云所有计数百分比的命中数。
public override void DrawNode(Graphics gr, RectangleF bounds) { int percent = 100 * count / getTotalWords(); Font font; int fontSize = Math.Min(8 + 16 * percent / 100, 24); if (!fontSizeMap.TryGetValue(fontSize, out font)) { font = new Font(FontFamily.GenericSansSerif, fontSize); fontSizeMap[fontSize] = font; } if (count >= GitHubDashboard.Dashboard.CountThreshold) { // Create a color based on count, from 1 to a max of 24 int red = 255 * percent / 100; int blue = 255 - red; Brush brush = new SolidBrush(Color.FromArgb(red, 0, blue)); SizeF strSize = gr.MeasureString(text, font); PointF textCenter = PointF.Subtract(bounds.Location, new Size((int)strSize.Width / 2 - 5, (int)strSize.Height / 2 - 5)); Region = Rectangle.FromLTRB((int)textCenter.X, (int)textCenter.Y, (int)(textCenter.X + strSize.Width), (int)(textCenter.Y + strSize.Height)); gr.DrawString(text, font, brush, textCenter); brush.Dispose(); } }
因为 C# 本身不支持指针,所以我不能传递给节点“这里有一个指向你所属集合的总词计数器的指针。” 相反,我们可以向构造函数传递一个返回计数的函数。
public TextNode(string text, PointF location, Func<int> getTotalWords)
而且,如果你注意到处理事件的代码,我们会使用返回适合节点集合计数的函数来实例化节点,无论是:
() => totalEvents
或
() => totalLanguages
或
() => totalDescriptionWords
结论
就这样!看到 GitHub 活动相当有趣。总的来说,语言分布似乎相当均匀,但是(奇怪的是)我注意到晚上会有更多关于 JavaScript 的工作,并且也有更多的 fork 事件。白天:
有更多的 push 事件和更多样化的语言!