65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (7投票s)

2010年7月12日

Ms-PL

4分钟阅读

viewsIcon

27221

downloadIcon

598

一个在 .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 打开时执行此操作,您将看到以下内容

fiddlerreader.png

按下添加按钮后,您可以看到访问了 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);
}    

此函数使用 URLGoogleParameters 来构建 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 和路径的单个类
  • ReaderCommandEnum 表示常见任务(如添加订阅)
  • 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,...
  • ReaderCommandReaderCommandFormatterReaderUrl 执行所有与 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 日:初始发布
© . All rights reserved.