Goodreads API 教程






4.33/5 (5投票s)
如何以 Goodreads.com 为例查询 API。
引言
在本篇文章中,我们将介绍设置一个网页的过程,该网页可以通过另一个网站的 API 查询信息。例如,我们将使用 Goodreads 提供的 API,我们的目标是在提供作者和书名时显示有关书籍的一般信息。为了实现这一点,我们将使用 Javascript 和一点 jQuery,以及一个使用 C# 作为后端的 HTTP Handler,并讨论将 XML 转换为 JSON,以便我们的 Javascript 能够快速读取我们查询的数据。最后,我们将实现如何缓存数据以减少服务器负载。
开始之前
通常,要使用 API,您需要一个开发者密钥。这是一个与您的帐户关联的唯一密钥。如果滥用,可能会被禁止,因此在将 API 实现到您的网站之前,阅读与 API 相关的服务条款非常重要。要获取开发者密钥,并查看可用的不同方法,请访问 Goodreads 的 http://www.goodreads.com/api。我将在本文中调用的特定方法是 book.title。获得开发者密钥后,将提供一个示例 URL,您可以在其中查看我们收到的 XML 示例以及其中的信息。请注意 URL 的格式;这将是我们也将需要遵循的模式。
我们还将使用一点 jQuery,因此我们需要下载 jQuery Javascript 文件;可以在 https://jqueryjs.cn/download/ 找到。我推荐在此练习中使用精简版。下载文件后,您可以将其拖放到解决方案窗口中;不过,我建议将其放在解决方案中的一个名为“scripts”的单独文件夹中。
创建客户端控件
首先,让我们创建一个简单的测试页面来显示我们的数据。我将在此页面上放置一些基本的 HTML 和一个对 Javascript 函数的调用,该函数将显示数据。
<body>
<form id="form1" runat="server">
<h2>Get Book Information</h2>
Author: <input type="text" id="authorTextbox" value="Patrick Rothfuss" /> <br />
Title: <input type="text" id="titleTextbox" value="The Name of the Wind" /> <br />
<input type="button" value="Get Book Information" id="getButton" onclick='getBookInformation()' />
<br /><br />
<div id="DataContainer" ></div>
</form>
</body>
我添加了一个作者文本框和一个书名文本框。我还预先为文本框添加了值,以便我们可以轻松测试页面是否正常工作,而无需不断添加值。当然,您不必这样做。除了文本框外,还有一个按钮,当点击时,会调用一个名为 getBookInformation 的 Javascript 函数。我们将在稍后的单独 Javascript 文件中定义它。
最后重要的一个项目是 div。它现在是空的,但在我们的 js 文件中,我们将引用该 div 并用我们从 Goodreads 获取的数据填充它。它必须有一个我们可以引用的 id,在这里,我称之为 DataContainer。
一旦创建了 js 文件,我们就需要在此页面上添加对它的引用,否则我们就完成了这个页面,让 Javascript 处理其余的工作。
创建 HTTP Handler 和 Javascript
HTTP Handler 是我们将指定数据来源以及如何格式化数据的地方。首先,我们将向我们的项目添加一个 Generic Handler。我将其命名为 goodreadsHttpHandler.ashx。我们将在 ProcessRequest 方法中编写代码,并保持 IsReuseable 不变。在忘记之前,我们要更改 Response.ContentType。我们将从 Goodreads 获取 XML,然后将其转换为 JSON,因此我们希望将内容类型更改为“application/json”,以反映我们最终返回的内容。
context.Response.ContentType = "application/json";
接下来,我们将向我们的解决方案添加一个 Javascript 文件。目前,我们只添加一个简单的函数,该函数获取我们文本框中的信息并将其传递给 HTTP Handler。
function getBookInformation() {
$.get('goodreadsHttpHandler.ashx'
, { bookAuthor: $('#authorTextbox').val(), bookTitle: $('#titleTextbox').val() }
, function (data) {
}
);
}
本质上,这里发生的是函数查找 handler,然后传递它所需的信息(在本例中为 bookAuthor 和 bookTitle)。然后,我们将 handler 返回的内容(在本例中称为“data”)传递给该函数。稍后,我们将把数据显示为 HTML,我们在函数内部定义。请注意,authorTextbox 和 titleTextbox 是我们在客户端控件页面上创建的文本框的 id。
回到我们的 handler,我们需要一种方法来引用 Javascript 传递给它的内容。我们使用 QueryString 方法来做到这一点。
string bookTitle = context.Request.QueryString["bookTitle"];
string bookAuthor = context.Request.QueryString["bookAuthor"];
现在我们的 handler 知道了书名和作者,它就拥有了查询 Goodreads API 所需的所有信息。所以,让我们在 handler 中创建一个名为 GetGoodreadsURI 的新方法,它将接受 bookAuthor 和 bookTitle 作为参数。
public string GetGoodreadsURI(string bookAuthor, string bookTitle)
{
string myKey = keyManager.GetConfigurationByKey("goodreadsDeveloperKey");
string uri = "http://www.goodreads.com/book/title?format={0}&author={1}&key={2}&title={3}";
return String.Format(uri, "xml", bookAuthor, myKey, bookTitle);
}
在我的示例中,我从另一个引用 web.config 文件的类中获取我的开发者密钥。您的密钥也应该以类似的方式隐藏在用户看不到的地方。要将文件添加到 web.config,请在“configuration 块”中添加以下内容。
<appSettings>
<add key="goodreadsDeveloperKey" value="YOUR_KEY_HERE"/>
</appSettings>
无论您是否将其添加到另一个类中,要在此 web.config 中引用它,您将使用
WebConfigurationManager.AppSettings["goodreadsDeveloperKey"];
回到我们的 GetGoodreadsURI 方法,请注意,对于格式,我们选择 XML。这个特定的 API 方法允许我们专门选择 JSON 格式。但是,如果我们这样做,Goodreads 只会返回评论。由于我们想获取有关书籍的更多信息,因此我们将使用 XML(提供更多一般信息)进行调用并将其转换为 JSON。
回到我们的 ProcessRequest 方法,我们将调用我们新的 GetGoodreadsURI 方法,并调用其返回值 uri。
string bookTitle = context.Request.QueryString["bookTitle"];
string bookAuthor = context.Request.QueryString["bookAuthor"];
string uri = GetGoodreadsURI(bookAuthor, bookTitle);
现在我们知道在哪里查找 XML,我们想将其复制到一个字符串中,然后将其转换为 JSON。
public string GetResponseFromAPI(string uri, out int serverStatus)
{
string responseData = String.Empty;
try
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
HttpWebResponse res = (HttpWebResponse)req.GetResponse();
serverStatus = (int)res.StatusCode;
using (Stream s = res.GetResponseStream())
{
using (StreamReader sr = new StreamReader(s))
{
responseData = sr.ReadToEnd();
}
}
return responseData;
}
catch (WebException e)
{
HttpWebResponse res = (HttpWebResponse)e.Response;
serverStatus = (int)res.StatusCode;
return responseData;
}
}
首先,我们只是发出请求,然后存储响应。之后,我们阅读了返回的整个响应。最后,我们将其放入一个易于以后读取的字符串中。
您不必获取服务器返回的 StatusCode,但出于调试目的,或者为用户提供相关的错误消息,这可能会很有价值。如果一切成功,我们将收到 StatusCode 200。在上面的代码中,我只捕获了 404 Not Found 的异常(即,用户搜索 Goodreads 数据库中找不到的书籍)。
现在我们已经将 Goodreads 的信息捕获到一个字符串中,我们可以将其转换为 JSON 发送回我们的 Javascript 函数。转换并不难,但很繁琐。首先,我们要创建一个文本文件,该文件将保持我们期望发送到 Javascript 的 JSON 格式。您可以查看 Goodreads 提供的示例 XML 文件,以确定您想要返回给用户的信息。根据我的选择,我的文本文件如下所示:
{
"Author": "{{!Author!}}"
, "Title": "{{!Title!}}"
, "Description": "{{!Description!}}"
, "Average_Rating": "{{!Average_Rating!}} / 5"
, "Cover_Image": "{{!Cover_Image!}}"
, "Publication_Year": "{{!Publication_Year!}}"
, "Publisher": "{{!Publisher!}}"
, "ISBN": "{{!ISBN!}}"
, "Reviews": "{{!Reviews!}}"
, "Status": "{{!Status!}}"
}
还记得我们之前的空 Javascript 函数吗?如果我们将其内容传递给它,并称之为 data(我们正在这样做),那么我们就可以引用 data.Author,我们将得到 {{!Author!}}。我们正在努力做的是从保存 XML 信息的字符串中提取信息,并替换我们文本文件中的相关字段(我们也将将其放入一个字符串中)。因此,通过将 {{!Author!}} 替换为 Patrick Rothfuss,例如,当我们调用 Javascript 函数中的 data.Author 时,我们将获得该作者的姓名。
现在我们有了这个文本文件,让我们将其放入一个字符串中。我将创建一个名为 GetGoodreadsJSONResponse 的新方法。
public string GetGoodreadsJSONResponse()
{
string returnData;
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(
"YOUR_SOLUTION_NAME.JSON_Responses.goodreads.BookInformationFields.txt"))
{
using (StreamReader sr = new StreamReader(s))
{
returnData = sr.ReadToEnd();
}
}
return returnData;
}
在我的示例中,我有一个名为“goodreads”的文件夹,位于一个名为“JSON Responses”的文件夹内。在“goodreads”文件夹内是我的文本文件。您不必遵循该文件夹结构,但请注意,使用 GetManifestResourceStream 方法时,您会在通常看到斜杠的地方使用句点来传递文件名。否则,这个方法所做的就是将我们的文本文件作为字符串返回。
现在,我们可以将这两个新方法添加到我们的 ProcessRequest 中。
string returnData = GetGoodreadsJSONResponse();
string responseData = GetResponseFromAPI(uri, out serverStatusCode);
if (serverStatusCode == 200)
{
returnData = ConvertGoodreadsXMLtoJSON(responseData, returnData);
}
在上面的代码中,有一个方法我们还没有定义:将响应字符串(XML 格式)中的所有内容放入我们的返回字符串(JSON 格式)。不幸的是,将 XML 转换为 JSON 是一个很大的方法。
public string ConvertGoodreadsXMLtoJSON(string responseData, string bookFieldsAsJSON)
{
XDocument xmlResponseData = XDocument.Parse(responseData);
XElement goodreadsRoot = xmlResponseData.Element("GoodreadsResponse");
XElement bookRootElement = goodreadsRoot.Element("book");
XElement workRootElement = bookRootElement.Element("work");
}
我们做的第一件事是基于包含响应数据的字符串创建一个 XML 文档。我们的字符串已经是 XML 格式,所以这不是问题。接下来,我们需要深入到每个元素中以获取其中的信息。所以,如果您再次查看示例 XML 页面,您会注意到有一个名为“GoodreadsResponse”的元素,其中包含所有其他元素。在该元素内部是另一个名为“book”的元素,它包含所有与实际书籍相关的内容。在“book”元素下是名为“title”的元素。现在您会注意到“title”元素实际上包含我们正在寻找的信息(特别是书名)。
XElement titleElement = bookRootElement.Element("title");
string bookTitleValue = titleElement.Value;
所以我们基本上已经深入到“title”元素,然后获取了其中的值。要获取作者姓名,您需要从“GoodreadsResponse”到“book”到“authors”到“author”到“name”,然后才能找到包含您想要的值的元素。因此,为每一条信息都这样做可能是一个繁琐的过程。
现在我们有了 title 元素中的值,我们想将其替换为 JSON 字符串中的占位符值。
bookFieldsAsJSON = bookFieldsAsJSON.Replace("{{!Title!}}", bookTitleValue);
您现在将为要从原始 XML 中提取的每个值重复此过程。但是,一个问题是“reviews_widget”和“description”值可能包含 HTML。如果您将此 HTML 按原样放入 JSON 字符串中,它将不起作用。为此,我们必须对其进行序列化(即将其转换为 JSON 格式的字符串)。
JavaScriptSerializer js = new JavaScriptSerializer();
XElement descriptionElement = bookRootElement.Element("description");
string bookDescriptionValue = js.Serialize(descriptionElement.Value).Trim();
bookDescriptionValue = bookDescriptionValue.Substring(1, bookDescriptionValue.Length - 2);
最后两行代码可能看起来很奇怪,但每次使用 Serialize 方法时它们都是必需的。Serialize 会自动在它创建的字符串周围加上引号,但正如您可能记得的那样,在我们 JSON 文本文件中,我们已经在我们想要显示的每个值周围加上了引号。最后两行代码会删除创建的字符串周围多余的引号。如果不删除它们,它将不起作用。
您需要序列化任何可能包含 HTML 的内容,因此如果您想显示“reviews_widget”,您绝对需要对其进行序列化。您不需要序列化 URL,因此您可以按原样传递“image_url”的值,而无需修改它。
万一找不到书籍,我想显示一条适当的消息,因此我的 JSON 文本文件中有一个 status 字段。在将所有 XML 转换为 JSON 的最后,我将 status 更改为 OK,然后返回我的新字符串。
bookFieldsAsJSON = bookFieldsAsJSON.Replace("{{!Status!}}", "Status_OK");
return bookFieldsAsJSON;
现在我们可以将其发送回我们的 Javascript 函数了。因此,此时,我们的 ProcessRequest 方法已完成并且可以正常工作,尽管稍后我们会回来添加缓存。
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json";
string bookTitle = context.Request.QueryString["bookTitle"];
string bookAuthor = context.Request.QueryString["bookAuthor"];
string uri = GetGoodreadsURI(bookAuthor, bookTitle);
string returnData = GetGoodreadsJSONResponse();
string responseData = GetResponseFromAPI(uri, out serverStatusCode);
if (serverStatusCode == 200)
{
returnData = ConvertGoodreadsXMLtoJSON(responseData, returnData);
}
context.Response.Write(returnData);
}
完成 Javascript 函数
最后,我们的 getBookInformation 函数已准备好进行一些工作。我们将使用 jQuery 将包含 handler 返回的信息的 HTML 插入到我们的页面中。为此,我们将向函数添加一个简单的 jQuery 命令。
function getBookInformation() {
$.get('goodreadsHttpHandler.ashx'
, { bookAuthor: $('#authorTextbox').val(), bookTitle: $('#titleTextbox').val() }
, function (data) {
$('#DataContainer').html(
)
}
);
}
请记住,DataContainer 是我们之前在主 HTML 页面中指定的 div 的名称,因此我们放在 .html() 中的所有内容都将作为 HTML 插入到该 div 中。这意味着我们可以直接将 HTML 放入该函数中,但是除了 Javascript 之外的所有内容都必须放在引号内(包括 HTML)。您可以使用 HTML 和 CSS 精确地按照您想要的方式设置内容样式,它将自动填充到 div 中。要引用 handler 返回的任何内容,我们只需使用 data.whatever。所以,如果我们想获取作者,我们将调用 data.Author(记住,这些是基于您在 JSON 文本文件中为值指定的名称)。
function getBookInformation() {
$.get('goodreadsHttpHandler.ashx'
// Specify location of Author and Title to search for.
, { bookAuthor: $('#authorTextbox').val(), bookTitle: $('#titleTextbox').val() }
, function (data) {
var titleStyle = "<p style=\"color:#666600;font-family:georgia,serif;\">";
var spanStart = "<span style=\"color:black;\">";
var spanEnd = "</span>";
var reviewsDiv = "<div id=\"ReviewContainer\"><p style=\"cursor:pointer;color:#666600;font-family:georgia,serif;\">";
var infoFromGoodreads = "<br/><br/><br/><p style=\"font-size:9px;\">
Information provided by <a href=\"http://www.goodreads.com\">goodreads</a>.</p>";
if (data.Status === "Status_OK") {
// Specify div to fill (change in else statements below also).
$('#DataContainer').html(
"<table><tbody><tr>"
+ "<td style=\"width:100px;\">" + "<img src=" + data.Cover_Image + "></>" + "</td>"
+ "<td>" + titleStyle
+ "Author: " + spanStart + data.Author + spanEnd
+ "<br/>" + "Title: " + spanStart + data.Title + spanEnd
+ "<br/>" + "Average Rating: " + spanStart + data.Average_Rating + spanEnd
+ "<br/>" + "First Published: " + spanStart + data.Publication_Year + spanEnd
+ "<br/>" + "Publisher: " + spanStart + data.Publisher + spanEnd
+ "<br/>" + "ISBN: " + spanStart + data.ISBN
+ "</p></td></tr></tbody></table>"
+ titleStyle + "Description" + "<br/><br/>" + spanStart + data.Description + spanEnd + "</p>"
+ reviewsDiv + "Show reviews..." + "</p></div>"
+ infoFromGoodreads
);
} else if (data.Status === "Bad_XML") {
$('#DataContainer').html("Unexpected XML encountered." + infoFromGoodreads);
} else {
$('#DataContainer').html("No book found." + infoFromGoodreads);
}
$("#ReviewContainer").click(function () { $('#ReviewContainer').html(data.Reviews); });
}
);
}
我们需要做的最后一件事是在将显示此信息的 HTML 页面上添加对脚本的引用。因此,返回客户端控件并将我们创建的 Javascript 以及 jQuery 脚本拖放到 HTML 页面的顶部。
<script src="scripts/jquery-1.8.0.min.js" type="text/javascript"></script>
<script src="scripts/goodreadsGetBookInfoJavascript.js" type="text/javascript"></script>
至此,我们已经有了一个可以调用 Goodreads API 并显示我们请求的数据的有效网页。但是,没有什么可以阻止用户以最快的速度点击“获取图书信息”按钮,从而导致我们的开发者密钥被禁止。此外,如果人们反复搜索相同的信息,通常不值得一遍又一遍地调用 Goodreads。
缓存数据
首先,让我们创建一个新的接口并将其命名为 ICacheData。
interface ICacheData
{
string GetCacheValue(string key);
void InsertToCache(string key, string value);
}
现在,创建一个名为 CacheManager 的新类,它继承自我们刚刚创建的接口。这个类只需要两个方法。第一个是检查我们是否已经有缓存中的内容,然后返回找到的内容。如果返回了某个东西,我们将将其传递给用户,而不是再次命中 Goodreads。
public string GetCacheValue(string key)
{
string returnData = null;
if (null != HttpContext.Current.Cache[key])
{
returnData = HttpContext.Current.Cache[key].ToString();
}
return returnData;
}
请注意,我们接受一个字符串作为参数。这需要是每次搜索的唯一标识。一个可能的键是我们最初用于获取信息的 URI(它包含作者和书名),因此在我们的 handler 中,我们将将其作为“key”值传递。
基本上,我们只是检查这个特定的 uri 键是否在我们的缓存中。如果存在,则返回存储的值(我们的 JSON 字符串),否则返回 null。回到我们的 handler,如果我们得到 null,我们想将其插入到缓存中。
public void InsertToCache(string key, string value)
{
lock (_lockObject)
{
if (null == HttpContext.Current.Cache.Get(key))
{
HttpContext.Current.Cache.Add(
key
, value
, null
, DateTime.Now.Add(TimeSpan.FromSeconds(300))
, System.Web.Caching.Cache.NoSlidingExpiration
, System.Web.Caching.CacheItemPriority.Low
, null
);
}
}
}
同样,我们将 URI 作为 key 参数传递。value 参数将是我们想要缓存的信息。在我们的例子中,这是我们要传递给 Javascript 函数的最终 JSON 字符串。其他需要注意的代码是 DateTime.Now.Add()。我们说的是在当前时间上加上 5 分钟(在生产代码中,您需要使其可配置),这就是我们的数据将在缓存中保留多长时间。如果您期望您的数据变化非常快,您可能需要将其设置为更短的时间。另一方面,如果您期望您的数据变化不大,您可以将其设置为更长的时间。
现在我们有了缓存数据的方法,让我们让我们的 handler 实现它们。
CacheManager cm = new CacheManager();
string returnData = cm.GetCacheValue(uri);
if (null == returnData)
{
returnData = GetGoodreadsJSONResponse();
string responseData = GetResponseFromAPI(uri, out serverStatusCode);
if (serverStatusCode == 200)
{
returnData = ConvertGoodreadsXMLtoJSON(responseData, returnData);
}
cm.InsertToCache(uri, returnData);
}
context.Response.Write(returnData);
正如您所见,实现简单的数据缓存形式的改动很小。我们首先检查数据是否被缓存,如果是,我们直接返回它。如果没有缓存数据,我们就会执行与之前相同的代码,但在继续之前我们会进行缓存。
*注意:您可能会注意到,如果您搜索一个作者并大写其名称,则该数据将按预期缓存。但是,如果您然后再次搜索但未大写任何内容,它将为不同的情况缓存相同的数据。您可以通过将所有搜索词小写后再将其放入 URI 来避免这种情况。无论出于何种原因,在撰写本文时,如果您将作者姓名全部小写或全部大写,Goodreads 将返回 404 错误。如果您正在处理不同的 API,这应该不会有问题。