在本文中,我们通过编写有趣的棒球击球记录程序程序展示了如何使用这三个 Google Code API。本文的编排以及应用程序代码的灵感均来自 Joseph Adler 的杰作 Baseball Hacks(参见 参考资料)。我们使用了三个 Google Code API:Google Chart API、Google Spreadsheets Data API 和 Google Gadgets API。我们不打算详尽介绍这些 API,但希望我们的介绍恰到好处,能激起您的兴趣。更详细的内容可以参考 Google 文档和教程。我们的目的是构建一个 Google Gadget 来显示美国棒球联盟球队的最新击球数据。为实现此目的,我们需要以下三个步骤。
首先,从一个棒球网站下载当前的棒球数据并将这些数据存储于以逗号分隔的文本文件。其次,使用 Google Spreadsheets Data API 将这些数据上传到 Google 电子数据表。第三,编写一个 Google Gadget 来从此电子数据表读取这些数据并利用 Google Chart API 以条形图显示这些数据。
第一步是下载数据。我们的代码基于 Baseball Hacks 中的 “Hack #25: Spider Baseball Sites for Data”。我们在本文中不打算详细介绍它,因为在此书中已有很多的解释。(我们的代码存于 get_all_teams_hitting.pl Perl 程序和 GetStats.pm Perl 模块,均包括在本文的 示例代码下载 中)。但有以下几点值得一提。
首先,虽然针对比赛结果(即积分表)的 RSS 和 Atom 提要有很多,但针对实时的球员积分数据的 RSS 和 Atom 提要好像没有。如果有的话,就需要从这些 Web 提要中获取数据。然而,我们却不得不从 HTML 页面获取数据。这样做不是很好,而且不可靠。比如,如果 HTML 的格式突然变了,代码很容易崩溃。第二点是在没有得到站点主人的同意之前不要从其站点获取数据。请先查看站点的 “使用条款”。第三,如果可以从该站点获取数据,也不要太过分。在连续对页面进行单击的空隙间,请留出一两秒的停顿。这不仅是个礼貌之举,而且,如果不这么做,您很可能会被拒之站外。(我们进行了测试,没有停顿的代码会遭到拒绝)。
我们或取 HTML 页、下载数据并将其存储在以逗号分隔的各个文本文件中,每个队一个文件,我们希望将这些数据存储在一个 Google 电子数据表中以供 Google Gadget 随后访问。Google Spreadsheets Data API 让您能够按行或按单元格添加、更改和删除 Google 电子数据表的内容。Google Spreadsheet Data API 对 Java™、.NET、PHP 和 Python 都可用。我们使用的是 Python,因为我们更喜欢它。
在 清单 1 中,Python 代码循环遍历这组以逗号分隔的文本文件,然后调用向电子数据表上传数据的代码。(我们还使用了另一个数据文件 MLBTeams.csv,其中包含了这些球队的列表)。
清单 1. upload_all_teams_hitting.py 的片段
mlb_teams = open('../../data/MLBTeams.csv')
try:
lines = mlb_teams.readlines( );
finally:
mlb_teams.close( )
# Skip header.
for line in lines[1:]:
line = line.rstrip();
if line != '':
parts = line.split(",")
curr_wksht_id = parts[1].rstrip()
teamAbbrev = parts[0].rstrip()
print "Updating: " + teamAbbrev
infile = "../../tempData/" + teamAbbrev + "Hitting.txt"
uploader = Uploader(user, pw, infile)
if curr_key != '':
uploader.set_spreadsheet(curr_key)
if curr_wksht_id != '':
uploader.set_worksheet(curr_wksht_id)
uploader.prompt_delete_old_rows_and_add_new_rows() |
以编程的方式与 Google 电子数据表交互的方式有两种:基于行的提要和基于单元格的提要。在这个应用程序中,由于我们通常都只按行删除旧数据并按行添加新数据,所以我们主要使用基于行的提要。
下面是我们使用的 gdata.spreadsheet 方法和字段:
-
SpreadsheetsListFeed:让您可以在行的级别处理电子数据表 -
SpreadsheetsList:工作表中的一行 -
SpreadsheetsCellsFeed:让您可以在单元格级别处理电子数据表-
row_count.text:用于获得工作表内的行数
-
-
service.SpreadsheetsService:用来访问此电子数据表的客户 API 对象-
email:Gmail 电子邮件地址 -
password:Gmail 密码 -
source:访问此 API 的应用程序名 -
ProgrammaticLogin():登录到 Google 帐户 -
GetCellsFeed(key, wksht_id):提供电子数据表的一个单元格提要(SpreadsheetsCellsFeed)。必须指定此电子数据表的 ID 和电子数据表中的工作表的 ID。 -
GetListFeed(key, wksht_id):提供电子数据表的一个列表提要(SpreadsheetsListFeed)。必须指定此电子数据表的 ID 和电子数据表中的工作表的 ID。 -
InsertRow(row_data, key, wksht_id):向电子数据表添加一个新行 -
DeleteRow(entry):删除SpreadsheetsList(一行)
-
清单 2 所包含的代码可将我们的击球数据上传到 Google 电子数据表。首先,我们删除当前工作表(在 _delete_and_add_rows 中)的所有行,然后我们用来自 .csv 文件(在 _fill_in_rows 中)的数据添加新行。
清单 2. upload_stats.py 的片段
import gdata.spreadsheet.service
class Uploader(CRUD):
"""
Deletes old contents of a Google Worksheet and adds new contents from a .csv
file.
"""
def __init__(self, email, password, infile):
"Initialize attributes of an Uploader."
self.gd_client = gdata.spreadsheet.service.SpreadsheetsService()
self.gd_client.email = email
self.gd_client.password = password
self.gd_client.source = 'Upload Batting Stats'
self.gd_client.ProgrammaticLogin()
self.curr_key = ''
self.curr_wksht_id = ''
self.list_feed = None
self.infile = infile
def prompt_delete_old_rows_and_add_new_rows(self):
"""
Delete old rows and add new ones, but prompt user for spreadsheet and
worksheet IDs if they're not already set.
"""
if not self.curr_key:
self._PromptForSpreadsheet()
if not self.curr_wksht_id:
self._PromptForWorksheet()
self._delete_and_add_rows()
def _delete_and_add_rows(self):
"""
First, delete all the rows (except the header row) in the spreadsheet
and then call method to add rows.
"""
cells_feed = self.gd_client.GetCellsFeed(self.curr_key, self.curr_wksht_id)
row_count = int(cells_feed.row_count.text)
while (row_count > 1):
self._list_delete_action(0)
cells_feed = self.gd_client.GetCellsFeed(self.curr_key, self.curr_wksht_id)
row_count = int(cells_feed.row_count.text)
self._fill_in_rows()
def _list_delete_action(self, index):
"Delete the row with the given index."
self.list_feed = self.gd_client.GetListFeed(self.curr_key, self.curr_wksht_id)
self.gd_client.DeleteRow(self.list_feed.entry[index])
print 'Deleted!'
def _fill_in_rows(self):
"Create new rows with the new data."
infile = open(self.infile)
header_line = infile.readline().strip()
headers = header_line.split(',')
col_count = len(headers)
while infile:
line = infile.readline().strip()
if len(line) > 0:
row_data = {}
parts = line.split(',')
for col in range(col_count):
row_data[headers[col].lower()] = parts[col].strip()
try:
self._list_insert_action(row_data)
except (gdata.service.RequestError, SyntaxError):
print 'Error inserting row:', row_data
print 'Will skip that row.'
else:
break;
def _list_insert_action(self, row_data):
"Insert a new row with data."
entry = self.gd_client.InsertRow(row_data,
self.curr_key, self.curr_wksht_id)
if isinstance(entry, gdata.spreadsheet.SpreadsheetsList):
print 'Inserted!' |
接下来,我们将在 Google 条形图中显示这些数据。
用 Google Chart API 创建一个 Google 条形图
Google Chart API 易于使用并可通过特定的 google.com URL 访问。指定所需的参数后,Google Chart API 就会返回定制图表。
让我们来看一个示例。图 1 展示了我们的这个击球统计数据的图表。该图表显示了平均打击率(AVG)、上垒率(OBP)和多垒安打率(SLG)。
图 1. Google 条形图示例
生成此示例图表的 URL 如 清单 3 所示,我们稍候会分别解释它的每个组件。
清单 3. Google Chart API 的示例 URL
var chartURL = "http://chart.apis.google.com/chart?cht=bvg&" +
"chd=t:0.284,0.299,0.271|0.328,0.37,0.298|0.355,0.494,0.4&chs=298x180&" +
"chco=c6d9fd,4d89f9,8a31fb&chdl=AVG|OBP|SLG&chds=0,0.700&" +
"chtt=Minnesota%20Twins+Batting&chxt=y,x&" +
"chxl=0:|0.000|0.100|0.200|0.300|0.400|0.500|0.600|0.700|" +
"1:|D+Young|J+Morneau|C+Gomez&chbh=15"
|
各参数所指定的内容如下:
-
cht:图表类型。bvg意味着我们想要一个垂直方向的条形图,而且其中的柱形条是分组的(而非堆叠的)。 -
chd:图表数据 -
chs:图表大小(以像素为单位) -
chco:条的颜色 -
chdl:图表数据标签 -
chds:图表比例参数 -
chtt:图表标题 -
chxt:多轴的方位 -
chxl:图表标签 -
chbh:条的高度(默认地,条是水平的,但本例中的条是垂直的,所以,这实际上是条的宽度)。
最后,创建我们的 Google Gadget。Google Gadget 是可以放置于 Web 页面(比如 iGoogle 页面)内的一些小部件。它们由 XML、HTML 和 JavaScript 组成。
Google Gadget 包含了我们的图表,如图 2 所示。
图 2. 击球数据 Google Gadget
正如您所见,Google Gadget 可以提示用户进行参数选择。在我们的示例中,我们让用户选择棒球队以及用户希望显示的球员的数量。
图 3. 选择参数后的 Gadget
清单 4 给出了这个 gadget 的代码。此代码主要完成三个功能:
- 从 Google 电子数据表获取击球数据
- 为 Google Chart 构造 URL
- 将此图表添加到 gadget 中的 HTML
我们使用 Spreadsheets Data API 检索 JSON(JavaScript Object Notation)提要以便从电子数据表获得数据。这个 JSON 提要再返回给 listStats 回调函数,然后我们在该函数内解析电子数据表数据。
清单 4. mlb-batting-stats.xml 的片段
<Module>
<ModulePrefs
title="MLB Batting Stats"
screenshot="http://mrsabermetrics.sourceforge.net/images/MLBBattingScreenShot.PNG"
description="Displays up-to-date MLB batting stats."
height="200"
width="675"
thumbnail="http://mrsabermetrics.sourceforge.net/images/Babe_Ruth.jpg"
title_url="http://mrsabermetrics.sourceforge.net/"
scrolling="true"
>
</ModulePrefs>
<UserPref name="num_players" display_name="How many players to show?"
datatype="enum" default_value="9" required="true">
<EnumValue value="2" display_value="2"/>
<EnumValue value="3" display_value="3"/>
<EnumValue value="4" display_value="4"/>
.
.
.
<EnumValue value="15" display_value="15"/>
</UserPref>
<UserPref name="team"
display_name="Team"
datatype="enum"
required="true"
>
<EnumValue value="ARI" display_value="Arizona Diamondbacks"/>
<EnumValue value="ATL" display_value="Atlanta Braves"/>
<EnumValue value="BAL" display_value="Baltimore Orioles"/>
.
.
.
<EnumValue value="MON" display_value="Washington Nationals"/>
</UserPref>
<Content type="html">
<![CDATA[
<div id="statsdiv"></div>
<script>
function listStats(root) {
var feed = root.feed;
var entries = feed.entry || [];
var html = [''];
var avgs = [];
var obps = [];
var slgs = [];
var names = [];
// Construct the URL for the Google Chart.
var cht = 'bvg';
html.push(
'<img src="http://chart.apis.google.com/chart?cht=' + cht + '&chd=t:');
for (var i = 0; i < feed.entry.length; ++i) {
var entry = feed.entry[i];
var title = entry.title.$t;
names.push(title);
var content = entry.content.$t;
var parts = content.split(',');
var avg = extractStat(parts[0]);
avgs.push(avg);
var obp = extractStat(parts[1]);
obps.push(obp);
var slg = extractStat(parts[2]);
slgs.push(slg);
}
var numPlayersPref = getStringPref("num_players");
// Show highest AB players
var numPlayers = parseInt(numPlayersPref);
if (numPlayers > names.length) {
numPlayers = names.length;
}
for (var i = 0; i < numPlayers; ++i) {
html.push(avgs[i]);
if (i < numPlayers - 1) {
html.push(",");
}
}
html.push("|");
for (var i = 0; i < numPlayers; ++i) {
html.push(obps[i]);
if (i < numPlayers - 1) {
html.push(",");
}
}
html.push("|");
for (var i = 0; i < numPlayers; ++i) {
html.push(slgs[i]);
if (i < numPlayers - 1) {
html.push(",");
}
}
var width = 1000;
if (numPlayers <= 9) {
var width = 58 * numPlayers + 124;
}
var chs = width + 'x180';
var chds = '0,0.700';
html.push(
"&chs=" + chs +
"&chco=c6d9fd,4d89f9,8a31fb&chdl=AVG|OBP|SLG&chds=" + chds);
var teamAbbrevToName = new Object();
for (var i = 0; i < teamInfoArray.length; i += 3) {
teamAbbrevToName[teamInfoArray[i]] = teamInfoArray[i + 2];
}
var teamName = teamAbbrevToName[getStringPref("team")];
var chxt = 'y,x';
html.push('&chtt=' + teamName + '+Batting&chxt=' + chxt +
'&chxl=0:|0.000|0.100|0.200|0.300|0.400|0.500|0.600|0.700|1:');
for (var i = 0; i < numPlayers; ++i) {
var parts = names[i].split(' ');
name = parts.join("+");
html.push('|' + name)
}
html.push('&chbh=15"/>');
var generatedHTML = html.join("");
// Add the chart to the HTML in the gadget.
document.getElementById("statsdiv").innerHTML = generatedHTML;
}
function extractStat(nameAndStat) {
var parts = nameAndStat.split(":");
var stat = parts[1];
var fStat = parseFloat(stat);
return fStat;
}
function getStringPref(prefName) {
var prefs = new _IG_Prefs();
var stringPref = prefs.getString(prefName);
return stringPref;
}
var teamInfoArray =
new Array('MIN', 'od6', "Minnesota Twins", 'ANA', 'od7', "Los Angeles Angels",
'ARI', 'od4', "Arizona Diamondbacks", 'ATL', 'od5', "Atlanta Braves",
'BAL', 'oda', "Baltimore Orioles", 'BOS', 'odb', "Boston Red Sox",
.
.
.
'TEX', 'ocm', "Texas Rangers", 'TOR', 'ocn', "Toronto Blue Jays");
</script>
<script language="javascript">
var teamToWkshtID = new Object( );
for (var i = 0; i < teamInfoArray.length; i += 3) {
teamToWkshtID[teamInfoArray[i]] = teamInfoArray[i + 1];
}
var wkshtID = teamToWkshtID[getStringPref("team")];
// Get the batting stats from the Google spreadsheet.
// Retrieve a JSON feed using the Spreadsheets Data API
var parmsrc =
"http://spreadsheets.google.com/feeds/list/" +
"o15075496074116042190.8006901556347913051/" + wkshtID +
"/public/basic?alt=json-in-script&callback=listStats";
scriptNode = document.createElement('script');
scriptNode.src = parmsrc;
scriptNode.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(scriptNode);
</script>
]]>
</Content>
</Module>
|
对这个 gadget 还可以进一步改进。以下是几个改进建议:
- 使它能显示各条所代表的实际数值。比如,当鼠标箭头在其中一个条上悬浮时,实际的数值就会出现(正如工具提示那样)。
- 使它能比较球队的 AVG/OBP/SLG 统计数据。
- 在 Gadget 代码中有多列球队信息,我们希望将它们作为数据保存在其他文本文件中,而不是保存在代码本身。另一方面,这些数据不太可能经常更改(虽然 Devil Rays 球队的确将其球队名称缩短为 “Rays”),所以将数据保存在代码中也没有太大的问题。
您可能会认为我们根本没必要采用电子数据表这一中间步骤,而是直接从棒球网站上获得 gadget 代码中的统计数据。我们采用电子数据表这一中间步骤是有原因的。首先,这些统计数据每天只改变一次,而且对每个用户都是一样的。所以,没有必要重复打扰获取数据的网站。第二,本文的主旨是展示 Google Code API 中可用的东西,而这是一个很妙的 Google Spreadsheet API 示例。
您能创建棒球 Google Gadget 的数量是没有限制的。如能创建多个 gadget 组成一个 “指示板” 来协助您管理棒球队,那会很有趣。
Google Gadget 也不是惟一的小部件类型。Mac OS X Dashboard Widget 是另一种流行的小部件类型。
在本文中,您对使用 Google Gadget、Google Spreadsheet API 和 Google Chart API 构建的 Web 应用程序类型有了一些了解。这个代码示例为使用这些 Google Code API 编写自己的应用程序提供了足够的参考。在 参考资料 部分,您会看到关于详细信息的链接。我们还建议您阅读 Joseph Adler 的杰作 Baseball Hacks。即使您觉得自己不喜欢棒球,也会发现这本书的确很有趣。
- 代码示例: 本文的源代码
- 演示: MLB Batting Stats Google Gadget1
注意:
- 在本文中构建的 Google Gadget。
学习
- 您可以参阅本文在 developerWorks 全球网站上的 英文原文。
- Joseph Adler 的著作
Baseball Hacks
(Copyright 2006 O'Reilly Media, Inc.,0-596-00942-9)很棒,它是本文的灵感来源。
- Michael M. Lewis 的著作
Moneyball: The Art of Winning an Unfair Game
(Copyright 2004 W. W.
Norton & Company,978-0393324815)讲述了如何在棒球管理中客观地使用统计数据。即使您对棒球不是很感兴趣,您也会发现这本书让人很着迷。
- 下载和了解下列开发工具:
- 浏览 技术书店 获得关于这些主题和其他技术主题的图书。
讨论
Paul Reiners 是 Catfish Hunter、Rollie Fingers 和 Vida Blue 时代 Oakland Athletics 棒球队的球迷。Paul 是几个开放源码程序的开发者,包括 Automatous Monk、Twisted Life 和 Leipzig。他于 1991 年 5 月在伊利诺斯大学 Urbana-Champaign 分校获得应用数学(计算理论)硕士学位,他目前住在明尼苏达州,他在业余时间喜欢演奏电子贝斯和在 TopCoder 大赛上一展身手。

