六、StackOverflow

2008 年,互联网上的编程问题市场被一家名为 Experts Exchange 的公司所主导。 许多人对网站的文化和人们必须注册才能查看答案的要求不满意。 程序员杰夫·阿特伍德(Jeff Atwood)和乔尔·斯波尔斯基(Joel Spolsky)推出了“问答”网站 Stack Overflow。 从那时起,该网站开始腾飞,迅速成长为互联网上排名前 100 的网站之一。 用户可以在网站上询问和回答关于各种编程主题的问题。 回答好一个问题或问一个深思熟虑的问题会赢得声望,这些声望会显著地显示出来。 虽然 Stack Overflow 不是像 Facebook 和 Twitter 那样的社交媒体网站,但它的内容都是由用户创建和审核的。 Stack Overflow 提供了一个 API,您可以根据它查询各种有趣的信息。

认证

许多查询 API 无需身份验证即可使用。 但是,如果您想要用户的私人信息或想要写入网站,那么您就需要进行身份验证。 对于经过身份验证的应用,也有更高的请求限制。 如果不进行身份验证,一个 IP 地址每天被限制为 300 个请求。 对于经过身份验证的应用,这个限制将提高到 10,000 个请求。

提示

速率限制

许多社交媒体网站在其 api 中使用了费率限制。 设置这些限制是为了防止站点超载,也可以避免请求太多数据。 Twitter 每秒处理 4000 多条推文。 如果没有特别的准备,如果您要处理所有的基础设施,那么您的基础设施很快就会不堪重负。

同样,这是一个利用OAuth对用户进行授权的站点。 然而,他们使用了 OAuth 2.0,这比我们在前一章中使用的 OAuth 1.0a 容易得多。 我们将限制自己使用公共信息以避免身份验证。 如果你想认证,我保证它比 Twitter 更容易。 详情请浏览https://api.stackexchange.com/docs/authentication。 Stack Overflow 使用的是和 Facebook 一样的授权系统,所以 OAuth 章节的例子应该是完美的。

创建可视化

Stack Overflow 上的许多问题都有大量的答案。 网站没有优化到显示最新答案; 答案按最被接受的答案进行排名,然后随机排序。 这样做是为了让所有答案都有机会显示在接近顶部的位置,理论上,这应该鼓励人们投票选出最佳答案,而不是只给出第一个答案。

对于这种可视化,我想展示一个问题是如何随着时间的推移而得到回答的。 最近的答案有可能得到更高的分数吗? 第一个答案总是最好的吗?

让我们从单个问题的数据入手,这个问题有很多答案。 为此,我们将利用问题 API。 所有的 API 端点都托管在https://api.stackexchange.com上。 我们将使用最新的 API 版本 2.1。 这也被编码到 URI 中,就像特定的端点和 ID 一样。 在问题 API 中,我们对答案感兴趣,因此我们可以针对它们进行查询,并给出一个 URIhttps://api.stackexchange.com/2.1/questions/{id}/answers

在查询字符串中,我们将指定要查询的站点。 Stack Exchange 拥有几十个以 Stack Overflow 为模型的问答站点,所有这些站点都来自同一个 API 端点,所以有必要通过传入site=stackoverflow来过滤 Stack Overflow:

function retrieveQuestionAnswers(id){
  var page = 1;
  var has_more = true;
  var results = [];
  while(has_more) {
    $.ajax(https://api.stackexchange.com/2.1/questions/ + id + "/answers?site=stackoverflow&page=" + page,{ 
        success: function(json){
          has_more = json.has_more;
          results = results.concat(json.items);},
        failure: function() { 
          has_more = false;},
          async: false
        });
        page++;
    }
  return results;
}

Twitter 为我们提供了延续令牌,我们可以将其传递回 Twitter 以请求下一页数据。 Stack Overflow 采用不同的方法并指定页码,使我们能够轻松地浏览结果。 每个 API 调用的响应中都嵌入了一个名为has_more的令牌,只要有更多的数据页与当前查询匹配,这个令牌就会出现。

在这段代码中,我们利用延续令牌和页码执行尽可能多的查询来检索所有的答案。 我们使用 jQuery 函数ajax,而不是更常见的getJson函数,因为我们希望同步检索数据。 我们这样做是因为我们想一次得到整个数据集。 如果可视化允许动态添加数据,那么可以放宽async:false要求。

返回的是一个对象数组,每个对象代表一个问题的答案。 如果我们给retrieveQuestionAnswers方法一个 ID,例如901115,那么我们将返回一个包含 50 个答案的数组。 在两个请求的过程中返回这些结果,上面的代码将它们合并到返回的结果数组中。

每个Answer包含许多字段。 默认返回的字段列表可以在https://api.stackexchange.com/docs/types/answer中找到。 为了直观化,我们最感兴趣的是这个答案最初是什么时候被提出的,它的分数,以及它是否被选为公认的答案。 这些信息可以在字段creation_datescoreis_accepted中找到。 我们暂时忽略其他字段。

现在我们有了一些基本的数据,我们可以开始考虑可视化了。 我们试图传达一个问题的年龄和它的分数之间的关系。 这听起来很像散点图的用途。 数据点是独立的,可以沿日期和点两个轴放置。 在此之前,我的理论是,答案越老,得分越高,因为他们收集分数的时间越长。 人们天生就认为上升的数字是正的,所以让我们利用这一点,画出点与年龄的关系,如果我的理论成立,右边的值会更高。

当然,散点图很无聊,除了 Excel 我们还可以生成其他内容。 我们将添加一些交互性,但在开始时,我们仍然需要一个简单的散点图。

这是很容易做到的一对比例和一些圆圈,如下所示的代码:

var graph = d3.select("#graph");
var axisWidth = 50;
var graphWidth = graph.attr("width");
var graphHeight = graph.attr("height");
var xScale = d3.scale.linear()
  .domain([0, d3.max(data, function(item){ return item.age;})])
  .range([axisWidth,graphWidth-axisWidth]);
var yScale = d3.scale.log()
  .domain([d3.max(data, function(item){return item.score;}),1])
  .range([axisWidth,graphHeight-axisWidth]);

这给出了一个非常平坦的图表,大多数数据接近于零,而评分超过 2000 的一个高异常值使量表发生了倾斜,如下图所示:

Creating a visualization

这可以通过使用对数标度加以改善。 当您使用非标准的刻度(如对数)时,您将希望放入轴标签,以防止引起混淆或误导可视化的消费者。

var yAxis = d3.svg.axis()
  .scale(yScale)
  .orient('left')
  .tickValues([1,5,10,50,100,500,1000,2000])
  .tickFormat(function(item){return item;});
graph.append("g")
  .attr("transform", "translate(" + axisWidth +",0)")
  .call(yAxis);
graph.append("text")
  .attr("x", "0")
  .attr("y", graphHeight/2)
  .attr("transform", "rotate(90, 0, " + graphHeight/2 + ")")
  .text("Score");

这个图中的标签是手工分配的,以提供最佳的传播效果。 您可以自动分配标签,但我发现它们在奇怪的地方声明。 我还定义了一个函数来格式化标签,否则它们会倾向于使用科学符号(2 * 10^3)进行格式化。 最后,我添加了一些文本作为轴标签。 我还添加了一个年龄轴,以天为单位列出答案的年龄。

var xAxis = d3.svg.axis().scale(xScale).orient('bottom');
graph.append("g")
  .attr("transform", "translate(0," + (graph.attr("height") - axisWidth)  +")")
  .call(xAxis);
graph.append("text")
  .attr("x", graphWidth/2)
  .attr("y", graphHeight-5)
  .style("text-anchor", "middle")
  .text("Age in days");

在这段代码中唯一值得注意的是使用变换旋转标签,因为它沿着垂直轴出现。 得到的图表如下图所示:

Creating a visualization

现在,我们已经有了一个基本的可视化,我们可以开始通过一些交互来美化它。

我们可以添加的最简单的交互是,当有人将鼠标指针移动到某个点上时,弹出一个标签。

这可以通过使用d3on()功能来实现。 这个函数可以将事件监听器绑定到作为 SVG 一部分创建的元素上。 首先,我们在循环的末尾加上从上面开始的附加,如下面的代码所示:

//append circle
.on("mouseover", function(item){
  showTip(item);
});

在这里,当用户将鼠标悬停在上图中的一个圆上时,将调用showTip()函数。 传递给事件处理程序的参数item是附加到悬停圆上的数据集合中的项。 如果您需要关于事件的额外信息(我们也需要),那么可以找到附加到全局变量d3.event的信息。

在事件处理程序中,我们首先高亮选中的圆圈,确保其他所有的圆圈都是黑色的,然后将选中的圆圈设置为蓝色:

function showTip(item){
  d3.selectAll(".score").attr("fill", "black");
  d3.select(d3.event.srcElement).attr("fill", "blue");

改变圆的大小以吸引更多的注意力也是有用的。 这可以通过简单地更新它的属性来实现。 接下来,我们隐藏上一个技巧,并设置技巧的内部内容以从选定的数据元素获取值:

  d3.select("#tip").style("opacity", 0);
  d3.select("#count").text(item.score);
  d3.select("#age").text(Math.floor(item.age));
  d3.select("#profileImage").attr("src",
  item.owner.profile_image);
  d3.select("#profileName").text(item.owner.display_name);

最后,我们移动工具提示到圆的旁边,让它淡入:

  d3.select("#tip").style("left", d3.event.x + "px");
  d3.select("#tip").style("top", d3.event.y + "px");
  d3.select("#tip").transition().duration(400).style("opacity", .75);
}

最终结果如下图所示:

Creating a visualization

将交互性添加到可视化中可以让您显示比正常情况下多得多的数据。 隐藏数据,使其只能通过移动鼠标,或单击它,以防止淹没您的用户,同时仍提供最大数量的信息。

滤镜

查询返回的数据并不是我们想要的。 例如,我们不关心last_edit_datelast_activity_date,但我们关心的是上下票的数量。 通过拉回额外的数据,我们浪费了带宽,并减慢了用户的可视化速度。 幸运的是,Stack Overflow 有一个过滤器形式的解决方案。

提示

深度查询

如果你发现你需要探索中的 StackOverflow 数据深度大于提供的 API,您可以下载整个站点的转储 http://www.clearbits.net/creators/146-stack-exchange-data-dump。 该转储每三个月提供一次,目前压缩的容量为 13.4 GB。 有了这个转储,您可以运行更复杂的查询,而不必担心达到速率限制。

过滤器控制从 API 返回的数据,可以用于添加或删除字段。 它们是静态创建的,所以您只需要创建它们一次,不需要在每次查询站点时创建新的过滤器,甚至在每次启动应用时都需要创建新的过滤器。 实际上,我使用 Stack Exchange 提供的 API 资源管理器提前创建过滤器。 创建过滤器的 URL 为:https://api.stackexchange.com/docs/create-filter

include字段中,可以使用分号来包含分隔的一系列名称。 所有属于 answer 对象的内容都以 answer 开头,因此答案的所有者会被称为answer.owner。 默认的过滤器是相当包容的,所以作为一个基本过滤器,我使用了特殊的none过滤器。 这将不包括任何字段,除非它们被显式包含。 使用none过滤器作为基础是减少多余查询的最佳实践,如下图所示:

Filters

如果您确实从none过滤器开始,请确保将令牌.items.has_more添加到 include 列表中。 如果没有条目,条目集合(根据查询保存问题、答案或用户)将不包括在内,需要使用has_more来判断是否有其他页面。 对于我们的目的,以下过滤器是完美的:

answer.answer_id;answer.owner;answer.score;answer.down_vote_count;answer.upvote_count;answer.creation_date;shallow_user.profile_image;shallow_user.display_name;.items;.has_more

create过滤器返回一个字母数字字符串,然后可以在查询中使用该字符串来适当地过滤它。 我们要查询的 URL 变成了:

"https://api.stackexchange.com/2.1/questions/" + id + "/answers?site=stackoverflow&filter=!2BjddbKa0El(rE-eV_QT8)5M&page=" + page

通过使用一个过滤器,我能够将 API 返回的有效负载从 22kB 减少到 3kB。 这是一个显著的节省,特别是在低带宽连接上。

小结

您现在应该能够查询堆栈交换 API,不仅针对 StackOverflow,而且针对所有堆栈交换站点。 您还应该了解如何通过使用d3向可视化添加交互性。 在下一章中,我们将看看如何使用 Facebook 作为可视化数据来源。