使用 .NET 访问 Google (Reader):第二部分 - 使用 .NET 的 Google Reader






4.55/5 (7投票s)
一个在 .NET 中实现的 Google Reader
引言
本文从上一篇文章(使用 .NET 访问 Google (Reader):第一部分 - 身份验证)停止的地方开始。
这是一个在 .NET 中实现的 Google Reader。
在开始之前,重要的是要知道有一个网站提供了有关 Google Reader API 的大量信息:http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI。但我们还将使用 Fiddler 来逆向工程 Google Reader API。
逆向工程
当您通过浏览器访问 Google Reader 时,基本上总是发送 GET
和 POST
请求。 使用 Fiddler,您可以查看这些请求的内容。
假设我们要订阅 http://sandrinodimattia.net/Blog。 当您登录到 Google Reader 时,您可以按添加订阅按钮,输入 URL 并按添加。 如果您在 Fiddler 打开时执行此操作,您将看到以下内容

按下添加按钮后,您可以看到访问了 URL /reader/api/0/subscription/quickadd,并且发布了 2 个字段(quickadd
和 T
)。 对于 Google Reader 中的每个可用操作,您可以使用 Fiddler 查看隐藏在底下的 URL 和 post 字段。
如果您仔细查看屏幕截图,您会看到一个名为 T
的字段,这是一个令牌。 它标识您的会话,但会快速过期。 这就是为什么您会看到我们的代码为每个新请求请求一个新令牌。
向 GoogleSession 添加 POST
在上一篇文章中,我们创建了 GoogleSession
类。 此类可帮助我们从 Google 获取数据。 但是现在我们必须发出 POST
请求,因此我们还需要将数据发送到 Google。 这就是为什么我们将以下方法添加到我们的 GoogleSession
类中
/// <summary>
/// Send a post request to Google.
/// </summary>
/// <param name="url"></param>
/// <param name="postFields"></param>
/// <returns></returns>
public void PostRequest(string url, params GoogleParameter[] postFields)
{
// Format the parameters.
string formattedParameters = string.Empty;
foreach (var par in postFields.Where(o => o != null))
formattedParameters += string.Format("{0}={1}&", par.Name, par.Value);
formattedParameters = formattedParameters.TrimEnd('&');
// Append a token.
formattedParameters += String.Format("&{0}={1}", "T", GetToken());
// Get the current post data and encode.
ASCIIEncoding ascii = new ASCIIEncoding();
byte[] encodedPostData = ascii.GetBytes(
String.Format(formattedParameters));
// Prepare request.
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = encodedPostData.Length;
// Add the authentication header.
request.Headers.Add("Authorization", "GoogleLogin auth=" + auth);
// Write parameters to the request.
using (Stream newStream = request.GetRequestStream())
newStream.Write(encodedPostData, 0, encodedPostData.Length);
// Get the response and validate.
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode != HttpStatusCode.OK)
throw new LoginFailedException(
response.StatusCode, response.StatusDescription);
}
此函数使用 URL 和 GoogleParameters
来构建 POST
请求。 我们讨论的令牌也会自动包含在 post 字段中。 借助此函数,我们将能够轻松地向 Google Reader 发送 POST
请求。
GoogleService 基类
在真正开始之前,我们将创建一个基类,我们可能希望在实现其他 Google 服务时重用它。 它封装了身份验证机制和 GoogleSession
类。
public abstract class GoogleService : IDisposable
{
/// <summary>
/// Current google session.
/// </summary>
protected GoogleSession session;
/// <summary>
/// Creating this class will automatically try to log in and create a session.
/// That way for each service we create we don't need to worry
/// about the implementation of authentication and session.
/// </summary>
/// <param name="service"></param>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="source"></param>
protected GoogleService
(string service, string username, string password, string source)
{
// Get the Auth token.
string auth = ClientLogin.GetAuthToken(service, username, password, source);
// Create a new session using this token.
this.session = new GoogleSession(auth);
}
/// <summary>
/// Clean up the session.
/// </summary>
public void Dispose()
{
if (session != null)
session.Dispose();
}
}
项目类型
从 Google Reader 读取时,您将使用 2 种不同类型的数据。 纯 XML 和联合项目。 联合项目 (SyndicationItem
在 .NET 中) 实际上是来自 feed 的项目。 例如,如果您访问此站点,您将获得一个 atom feed 形式的已读项目列表:http://www.google.com/reader/atom/user/-/state/com.google/read。 此 feed 中的每个项目都是一个 SyndicationItem
。
对于这两种类型,我们将使用一些基本的基类
public abstract class GoogleSyndicationItem
{
/// <summary>
/// Initialize the item.
/// </summary>
/// <param name="item"></param>
internal GoogleSyndicationItem(SyndicationItem item)
{
if (item != null)
{
LoadItem(item);
}
}
/// <summary>
/// Load the item (to be implemented by inheriting classes).
/// </summary>
/// <param name="item"></param>
protected abstract void LoadItem(SyndicationItem item);
/// <summary>
/// Get the text from a TextSyndicationContent.
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public string GetTextSyndicationContent(SyndicationContent content)
{
TextSyndicationContent txt = content as TextSyndicationContent;
if (txt != null)
return txt.Text;
else
return "";
}
}
public abstract class GoogleXmlItem : SyndicationItem
{
/// <summary>
/// Initialize the item.
/// </summary>
/// <param name="item"></param>
internal GoogleXmlItem(XElement item)
{
if (item != null)
{
LoadItem(item);
}
}
/// <summary>
/// Load the item (to be implemented by inheriting classes).
/// </summary>
/// <param name="item"></param>
protected abstract void LoadItem(XElement item);
/// <summary>
/// Get a list of descendants.
/// </summary>
/// <param name="item"></param>
/// <param name="descendant"></param>
/// <param name="attribute"></param>
/// <param name="attributeValue"></param>
/// <returns></returns>
protected IEnumerable<XElement> GetDescendants
(XElement item, string descendant, string attribute, string attributeValue)
{
return item.Descendants(descendant).Where(o => o.Attribute(attribute) != null &&
o.Attribute(attribute).Value == attributeValue);
}
/// <summary>
/// Get a descendant.
/// </summary>
/// <param name="item"></param>
/// <param name="descendant"></param>
/// <param name="attribute"></param>
/// <param name="attributeValue"></param>
/// <returns></returns>
protected XElement GetDescendant(XElement item, string descendant,
string attribute, string attributeValue)
{
return GetDescendants(item, descendant, attribute, attributeValue).First();
}
/// <summary>
/// Get the value of a descendant.
/// </summary>
/// <param name="item"></param>
/// <param name="descendant"></param>
/// <param name="attribute"></param>
/// <param name="attributeValue"></param>
/// <returns></returns>
protected string GetDescendantValue
(XElement item, string descendant, string attribute, string attributeValue)
{
var desc = GetDescendant(item, descendant, attribute, attributeValue);
if (desc != null)
return desc.Value;
else
return "";
}
}
对于 Google Reader 中的几乎每种数据类型,我也创建了一个类
Feed
(feed 的 URL,带有该 feed 的标题)ReaderItem
(你可以说这是一篇文章,一篇博文,...)State
(这是 Google Reader 中项目的状态,如已读、已加星标、...)Subscription
(Google Reader 中的订阅是您订阅的 feed)
这是订阅的实现
public class Subscription : GoogleXmlItem
{
/// <summary>
/// Id of the subscription.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Title of the feed.
/// </summary>
public string Title { get; set; }
/// <summary>
/// URL to the subscription.
/// </summary>
public string Url { get; set; }
/// <summary>
/// List of categories.
/// </summary>
public List<string> Categories { get; set; }
/// <summary>
/// Initialize the subscription.
/// </summary>
/// <param name="item"></param>
internal Subscription(XElement item)
: base(item)
{
}
/// <summary>
/// Load the subscription item.
/// </summary>
/// <param name="item"></param>
protected override void LoadItem(XElement item)
{
// Initialize categories list.
Categories = new List<string>();
// Regular fields.
Id = GetDescendantValue(item, "string", "name", "id");
Title = GetDescendantValue(item, "string", "name", "title");
// Parse the URL.
if (Id.Contains('/'))
Url = Id.Substring(Id.IndexOf('/') + 1, Id.Length - Id.IndexOf('/') - 1);
// Get the categories.
var catList = GetDescendant(item, "list", "name", "categories");
if (catList != null && catList.HasElements)
{
var categories = GetDescendants(item, "string", "name", "label");
Categories.AddRange(categories.Select(o => o.Value));
}
}
}
随处可见的 URL
如前所述,Google Reader API 基于 URL 和 GET
/POST
请求。 为了组织它,我们还有一些关于 URL 的类
ReaderUrl
:一个包含所有必需的 URL 和路径的单个类ReaderCommand
:Enum
表示常见任务(如添加订阅)ReaderCommandFormatter
:包含ReaderCommand
的扩展方法的类,用于将这些enum
值转换为实际的 Google Reader URL
public static class ReaderUrl
{
/// <summary>
/// Base url for Atom services.
/// </summary>
public const string AtomUrl = "https://www.google.com/reader/atom/";
/// <summary>
/// Base url for API actions.
/// </summary>
public const string ApiUrl = "https://www.google.com/reader/api/0/";
/// <summary>
/// Feed url to be combined with the desired feed.
/// </summary>
public const string FeedUrl = AtomUrl + "feed/";
/// <summary>
/// State path.
/// </summary>
public const string StatePath = "user/-/state/com.google/";
/// <summary>
/// State url to be combined with desired state. For example: starred
/// </summary>
public const string StateUrl = AtomUrl + StatePath;
/// <summary>
/// Label path.
/// </summary>
public const string LabelPath = "user/-/label/";
/// <summary>
/// Label url to be combined with the desired label.
/// </summary>
public const string LabelUrl = AtomUrl + LabelPath;
}
public enum ReaderCommand
{
SubscriptionAdd,
SubscriptionEdit,
SubscriptionList,
TagAdd,
TagEdit,
TagList,
TagRename,
TagDelete
}
public static class ReaderCommandFormatter
{
/// <summary>
/// Get the full url for a command.
/// </summary>
/// <param name="comm"></param>
/// <returns></returns>
public static string GetFullUrl(this ReaderCommand comm)
{
switch (comm)
{
case ReaderCommand.SubscriptionAdd:
return GetFullApiUrl("subscription/quickadd");
case ReaderCommand.SubscriptionEdit:
return GetFullApiUrl("subscription/edit");
case ReaderCommand.SubscriptionList:
return GetFullApiUrl("subscription/list");
case ReaderCommand.TagAdd:
return GetFullApiUrl("edit-tag");
case ReaderCommand.TagEdit:
return GetFullApiUrl("edit-tag");
case ReaderCommand.TagList:
return GetFullApiUrl("tag/list");
case ReaderCommand.TagRename:
return GetFullApiUrl("rename-tag");
case ReaderCommand.TagDelete:
return GetFullApiUrl("disable-tag");
default:
return "";
}
}
/// <summary>
/// Get the full api url.
/// </summary>
/// <param name="append"></param>
/// <returns></returns>
private static string GetFullApiUrl(string append)
{
return String.Format("{0}{1}", ReaderUrl.ApiUrl, append);
}
}
最后... ReaderService
最后是 Google Reader 中最常见任务的实现
public class ReaderService : GoogleService
{
/// <summary>
/// Current username.
/// </summary>
private string username;
/// <summary>
/// Initialize the Google reader.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="source"></param>
public ReaderService(string username, string password, string source)
: base("reader", username, password, source)
{
this.username = username;
}
#region Feed
/// <summary>
/// Get the contents of a feed.
/// </summary>
/// <param name="feedUrl">
/// Must be exact URL of the feed,
/// ex: http://sandrinodimattia.net/blog/syndication.axd
/// </param>
/// <param name="limit"></param>
/// <returns></returns>
public IEnumerable<ReaderItem> GetFeedContent(string feedUrl, int limit)
{
try
{
return GetItemsFromFeed(String.Format("{0}{1}",
ReaderUrl.FeedUrl, System.Uri.EscapeDataString(feedUrl)), limit);
}
catch (WebException wex)
{
HttpWebResponse rsp = wex.Response as HttpWebResponse;
if (rsp != null && rsp.StatusCode == HttpStatusCode.NotFound)
throw new FeedNotFoundException(feedUrl);
else
throw;
}
}
#endregion
#region Subscription
/// <summary>
/// Subscribe to a feed.
/// </summary>
/// <param name="feed"></param>
public void AddSubscription(string feed)
{
PostRequest(ReaderCommand.SubscriptionAdd,
new GoogleParameter("quickadd", feed));
}
/// <summary>
/// Tag a subscription (remove it).
/// </summary>
/// <param name="feed"></param>
/// <param name="folder"></param>
public void TagSubscription(string feed, string folder)
{
PostRequest(ReaderCommand.SubscriptionEdit,
new GoogleParameter("a", ReaderUrl.LabelPath + folder),
new GoogleParameter("s", "feed/" + feed),
new GoogleParameter("ac", "edit"));
}
/// <summary>
/// Get a list of subscriptions.
/// </summary>
/// <returns></returns>
public List<Subscription> GetSubscriptions()
{
// Get the XML for subscriptions.
string xml = session.GetSource(ReaderCommand.SubscriptionList.GetFullUrl());
// Get a list of subscriptions.
return XElement.Parse(xml).Elements
("list").Elements("object").Select(o => new Subscription(o)).ToList();
}
#endregion
#region Tags
/// <summary>
/// Add tags to an item.
/// </summary>
/// <param name="feed"></param>
/// <param name="folder"></param>
public void AddTags(ReaderItem item, params string[] tags)
{
// Calculate the amount of parameters required.
int arraySize = tags.Length + item.Tags.Count + 2;
// Set all parameters.
GoogleParameter[] parameters = new GoogleParameter[arraySize];
parameters[0] = new GoogleParameter("s", "feed/" + item.Feed.Url);
parameters[1] = new GoogleParameter("i", item.Id);
int nextParam = 2;
// Add parameters.
for (int i = 0; i < item.Tags.Count; i++)
parameters[nextParam++] = new GoogleParameter
("a", ReaderUrl.LabelPath + item.Tags[i]);
for (int i = 0; i < tags.Length; i++)
parameters[nextParam++] = new GoogleParameter
("a", ReaderUrl.LabelPath + tags[i]);
// Send request.
PostRequest(ReaderCommand.TagAdd, parameters);
}
/// <summary>
/// Rename a tag.
/// </summary>
/// <param name="tag"></param>
/// <param name="newName"></param>
public void RenameTag(string tag, string newName)
{
PostRequest(ReaderCommand.TagRename,
new GoogleParameter("s", ReaderUrl.LabelPath + tag),
new GoogleParameter("t", tag),
new GoogleParameter("dest", ReaderUrl.LabelPath + newName));
}
/// <summary>
/// Remove tag (for all items).
/// </summary>
/// <param name="tag"></param>
public void RemoveTag(string tag)
{
PostRequest(ReaderCommand.TagDelete,
new GoogleParameter("s", ReaderUrl.LabelPath + tag),
new GoogleParameter("t", tag));
}
/// <summary>
/// Remove tag from a single item.
/// </summary>
/// <param name="item"></param>
/// <param name="tag"></param>
public void RemoveTag(ReaderItem item, string tag)
{
PostRequest(ReaderCommand.TagEdit,
new GoogleParameter("r", ReaderUrl.LabelPath + tag),
new GoogleParameter("s", "feed/" + item.Feed.Url),
new GoogleParameter("i", item.Id));
}
/// <summary>
/// Get a list of tags.
/// </summary>
/// <returns></returns>
public List<string> GetTags()
{
string xml = session.GetSource(ReaderCommand.TagList.GetFullUrl());
// Get the list of tags.
var tagElements = from t in XElement.Parse(xml).Elements
("list").Descendants("string")
where t.Attribute("name").Value == "id"
where t.Value.Contains("/label/")
select t;
// Create a list.
List<string> tags = new List<string>();
foreach (XElement element in tagElements)
{
string tag = element.Value.Substring(element.Value.LastIndexOf('/') + 1,
element.Value.Length - element.Value.LastIndexOf('/') - 1);
tags.Add(tag);
}
// Done.
return tags;
}
/// <summary>
/// Get all items for a tag.
/// </summary>
/// <param name="tag"></param>
/// <param name="limit"></param>
/// <returns></returns>
public IEnumerable<ReaderItem> GetTagItems(string tag, int limit)
{
return GetItemsFromFeed(String.Format("{0}{1}",
ReaderUrl.LabelPath, System.Uri.EscapeDataString(tag)), limit);
}
#endregion
#region State
/// <summary>
/// Add state for an item.
/// </summary>
/// <param name="item"></param>
/// <param name="state"></param>
public void AddState(ReaderItem item, State state)
{
PostRequest(ReaderCommand.TagEdit,
new GoogleParameter("a",
ReaderUrl.StatePath + StateFormatter.ToString(state)),
new GoogleParameter("i", item.Id),
new GoogleParameter("s", "feed/" + item.Feed.Url));
}
/// <summary>
/// Remove a state from an item.
/// </summary>
/// <param name="item"></param>
/// <param name="state"></param>
public void RemoveState(ReaderItem item, State state)
{
PostRequest(ReaderCommand.TagEdit,
new GoogleParameter("r",
ReaderUrl.StatePath + StateFormatter.ToString(state)),
new GoogleParameter("i", item.Id),
new GoogleParameter("s", "feed/" + item.Feed.Url));
}
/// <summary>
/// Get the content of a state.
/// For example: Get all starred items.
/// </summary>
/// <param name="state"></param>
/// <param name="limit"></param>
/// <returns></returns>
public IEnumerable<ReaderItem> GetStateItems(State state, int limit)
{
return GetItemsFromFeed(String.Format("{0}{1}",
ReaderUrl.StateUrl, StateFormatter.ToString(state)), limit);
}
#endregion
/// <summary>
/// Post a request using a reader command.
/// </summary>
/// <param name="command"></param>
/// <param name="postFields"></param>
private void PostRequest(ReaderCommand command, params GoogleParameter[] postFields)
{
session.PostRequest(ReaderCommandFormatter.GetFullUrl(command), postFields);
}
/// <summary>
/// Get items from a feed and convert them to a GoogleReaderItem.
/// </summary>
/// <param name="url"></param>
/// <param name="limit"></param>
/// <returns></returns>
private IEnumerable<ReaderItem> GetItemsFromFeed(string url, int limit)
{
SyndicationFeed feed = session.GetFeed(url,
new GoogleParameter("n", limit.ToString()));
return feed.Items.Select<SyndicationItem, ReaderItem>(o => new ReaderItem(o));
}
}
正如你所见,ReaderService
做了几件事
- 订阅(列表、添加)
- 标签(添加、重命名、删除、...)
- 状态(添加、删除、列表)
- Feed(列表内容)
它实际上重用了我们讨论过的一些内容
GoogleSession
发送 post 请求,获取 feeds,...ReaderCommand
、ReaderCommandFormatter
、ReaderUrl
执行所有与 URL 相关的操作GoogleParameter
设置POST
字段(我们可以使用 Fiddler 找到的字段)
整合
控制台应用程序也使用我们的 ReaderService
进行了更新
class Program
{
static void Main(string[] args)
{
// Empty line.
Console.WriteLine("");
// Get username.
Console.Write(" Enter your Google username: ");
string username = Console.ReadLine();
// Get password.
Console.Write(" Enter your password: ");
string password = Console.ReadLine();
// Query.
using (ReaderService rdr = new ReaderService
(username, password, "Sandworks.Google.App"))
{
// Display.
Console.WriteLine("");
Console.WriteLine(" Last 5 articles from Sandrino's Blog: ");
foreach (ReaderItem item in rdr.GetFeedContent
("http://sandrinodimattia.net/blog/syndication.axd?format=rss", 5))
{
Console.WriteLine(" - " + item.Author + ": " + item.Title);
}
}
// Pause.
Console.ReadLine();
}
}
现在你已经拥有了使用 .NET 开始使用 Google Reader 所需的一切。 下一篇文章将介绍创建一个基本的 WPF 应用程序,以拥有一个简单的 Google Reader 桌面版本。
享受吧...
历史
- 2010 年 7 月 12 日:初始发布