之前在开发博客园新闻客户端的时候,需要获取博客园的新闻数据,最早开发出来的版本使用的是手机版的 html 解释的方式。效果算是做出来了,但是感觉获取到的数据太冗余,于是便查了一下有没有相应的接口。皇天不负有心人,博客园果然开放了些接口供我们使用。
博客服务接口:http://wcf.open.cnblogs.com/blog/help
新闻服务接口:http://wcf.open.cnblogs.com/news/help
博客服务:
Uri | Method | Description |
48HoursTopViewPosts/{itemCount} | GET | 48小时阅读排行 |
bloggers/recommend/{pageIndex}/{pageSize} | GET | 分页获取推荐博客列表 |
bloggers/recommend/count | GET | 获取推荐博客总数 |
bloggers/search | GET | 根据作者名搜索博主 |
post/{postId}/comments/{pageIndex}/{pageSize} | GET | 获取文章评论 |
post/body/{postId} | GET | 获取文章内容 |
sitehome/paged/{pageIndex}/{pageSize} | GET | 分页获取首页文章列表 |
sitehome/recent/{itemcount} | GET | 获取首页文章列表 |
TenDaysTopDiggPosts/{itemCount} | GET | 10天内推荐排行 |
u/{blogapp}/posts/{pageIndex}/{pageSize} | GET | 分页获取个人博客文章列表 |
新闻服务:
Uri | Method | Description |
GetData | GET | 获取新闻列表 |
hot/{itemcount} | GET | 获取热门新闻列表 |
item/{contentId} | GET | 获取新闻内容 |
item/{contentId}/comments/{pageIndex}/{pageSize} | GET | 获取新闻评论 |
recent/{itemcount} | GET | 获取最新新闻列表 |
recent/paged/{pageIndex}/{pageSize} | GET | 分页获取最新新闻列表 |
recommend/paged/{pageIndex}/{pageSize} | GET | 分页获取推荐新闻列表 |
可以看见,博客园官方团队还算是挺厚道的,基本的接口都开放出来了。(除了博客文章按分类获取-_-|||博主我的碎碎念)
由于需要尽可能使我们封装好的类库尽可能在多个平台使用,于是乎就想到了使用可移植类库(PCL)来开发。
建起项目
当然是通通选上,哪知道以后哪天会不会心血来潮再搞个什么平台的客户端。
因为就分两大类,于是果断码上 BlogService 和 NewsService 两个静态类。
作为相应服务的入口。
接下来,看见新闻的接口比较少,先写这部分。
测试 GetData 接口。
啥玩意?!2012年的数据!那这个就不理他了。。。
接下来就是 hot(获取热门新闻列表)这个接口。
测试下:http://wcf.open.cnblogs.com/news/hot/10
工作良好,于是开始写这个接口的封装。
在 NewsService 中写上
public static async Task<IEnumerable<News>> HotAsync(int itemCount) { // TODO }
会发觉编译不通过(废话)。
于是新建一个News类先。修改方法。
编译!
不通过!!!
看错误列表:
不是4.5了么?怎么不能用 async?
解决方法:http://www.cnblogs.com/h82258652/p/4119118.html(注:本文为总结性文章,所以时间线的跳跃你们要跟上)
装好巨硬给我们的异步补丁包后就可以编译通过了。
接下来观察接口返回的xml文档,可以发现每一个entry节点就相当于一条新闻。
根据entry分析,完善News类:
1 using System; 2 3 namespace SoftwareKobo.CnblogsAPI.Model 4 { 5 /// <summary> 6 /// 新闻。 7 /// </summary> 8 public class News 9 { 10 /// <summary> 11 /// Id。 12 /// </summary> 13 public int Id 14 { 15 get; 16 internal set; 17 } 18 19 /// <summary> 20 /// 标题。 21 /// </summary> 22 public string Title 23 { 24 get; 25 internal set; 26 } 27 28 /// <summary> 29 /// 摘要。 30 /// </summary> 31 public string Summary 32 { 33 get; 34 internal set; 35 } 36 37 /// <summary> 38 /// 发表时间。 39 /// </summary> 40 public DateTime Published 41 { 42 get; 43 internal set; 44 } 45 46 /// <summary> 47 /// 更新时间。 48 /// </summary> 49 public DateTime Updated 50 { 51 get; 52 internal set; 53 } 54 55 /// <summary> 56 /// 新闻链接。 57 /// </summary> 58 public Uri Link 59 { 60 get; 61 internal set; 62 } 63 64 /// <summary> 65 /// 推荐数。 66 /// </summary> 67 public int Diggs 68 { 69 get; 70 internal set; 71 } 72 73 /// <summary> 74 /// 查看数。 75 /// </summary> 76 public int Views 77 { 78 get; 79 internal set; 80 } 81 82 /// <summary> 83 /// 评论数。 84 /// </summary> 85 public int Comments 86 { 87 get; 88 internal set; 89 } 90 91 /// <summary> 92 /// 主题。 93 /// </summary> 94 public string Topic 95 { 96 get; 97 internal set; 98 } 99 100 /// <summary> 101 /// 主题图标。 102 /// </summary> 103 public Uri TopicIcon 104 { 105 get; 106 internal set; 107 } 108 109 /// <summary> 110 /// 转载自。 111 /// </summary> 112 public string SourceName 113 { 114 get; 115 internal set; 116 } 117 } 118 }
PS:Q:为什么set方法都写为internal?A:为什么外部要修改?Q:好吧,你赢了。
接下来就开始写我们的 HotAsync 方法了。
HotAsync 方法有一个参数——itemCount,那么当然要进行验证。(不发送这些无谓的请求一方面可以不让用户等待、一方面减轻博客园的压力)
if (itemCount < 1) { throw new ArgumentOutOfRangeException(nameof(itemCount)); }
nameof,还没跟上时代节奏的小伙伴就赶紧跟上了,这里不解释。
接下来当然是拼接 url。
var url = string.Format(CultureInfo.InvariantCulture, HotUrlTemplate, itemCount); var uri = new Uri(url, UriKind.Absolute);
HotUrlTemplate 的定义:
private const string HotUrlTemplate = "http://wcf.open.cnblogs.com/news/hot/{0}";
常量最好不要出现在方法中这是好习惯哦。
准备WebRequest,并且调用GetResponse。
var request = WebRequest.Create(uri); using (var response = await request.GetResponseAsync()) { // TODO }
由于返回的是一个xml,所以直接使用 XDocument 加载(PS:我特讨厌XmlDocument那套API)
var document = XDocument.Load(response.GetResponseStream());
接下来就是将XDocument转换为News实体类的列表,这里我们写到别的方法去,因为下面几个服务接口可能也会用到。
新建NewsHelper类。
根据entry节点的结果,写出以下代码:
1 internal static class NewsHelper 2 { 3 internal static IEnumerable<News> Deserialize(XDocument document) 4 { 5 var root = document?.Root; 6 if (root == null) 7 { 8 return null; 9 } 10 11 var ns = root.GetDefaultNamespace(); 12 var news = from entry in root.Elements(ns + "entry") 13 where entry.HasElements 14 let temp = Deserialize(entry) 15 where temp != null 16 select temp; 17 return news; 18 } 19 20 internal static News Deserialize(XElement element) 21 { 22 if (element == null) 23 { 24 return null; 25 } 26 27 var ns = element.GetDefaultNamespace(); 28 var id = element.Element(ns + "id"); 29 var title = element.Element(ns + "title"); 30 var summary = element.Element(ns + "summary"); 31 var published = element.Element(ns + "published"); 32 var updated = element.Element(ns + "updated"); 33 var href = element.Element(ns + "link")?.Attribute("href"); 34 var diggs = element.Element(ns + "diggs"); 35 var views = element.Element(ns + "views"); 36 var comments = element.Element(ns + "comments"); 37 var topic = element.Element(ns + "topic"); 38 var topicIcon = element.Element(ns + "topicIcon"); 39 var sourceName = element.Element(ns + "sourceName"); 40 41 if (id == null 42 || title == null 43 || summary == null 44 || published == null 45 || updated == null 46 || href == null 47 || diggs == null 48 || views == null 49 || comments == null 50 || topic == null 51 || topicIcon == null 52 || sourceName == null) 53 { 54 return null; 55 } 56 57 return new News 58 { 59 Id = int.Parse(id.Value, CultureInfo.InvariantCulture), 60 Title = WebUtility.HtmlDecode(title.Value), 61 Summary = WebUtility.HtmlDecode(summary.Value), 62 Published = DateTime.Parse(published.Value, CultureInfo.InvariantCulture), 63 Updated = DateTime.Parse(updated.Value, CultureInfo.InvariantCulture), 64 Link = new Uri(href.Value, UriKind.Absolute), 65 Diggs = int.Parse(diggs.Value, CultureInfo.InvariantCulture), 66 Views = int.Parse(views.Value, CultureInfo.InvariantCulture), 67 Comments = int.Parse(comments.Value, CultureInfo.InvariantCulture), 68 Topic = topic.Value, 69 TopicIcon = topicIcon.IsEmpty ? null : new Uri(topicIcon.Value, UriKind.Absolute), 70 SourceName = sourceName.Value 71 }; 72 } 73 }
由于我们需要的是IEnumerable<News>,所以直接返回Linq的结果就行了,不用ToList或者啥的。
注意PCL中是没有HtmlDecode、HtmlEncode的,nuget上有一个PCL用的,可惜人家作者没更新了,版本过旧,引用不了,没办法,自己动手丰衣足食。
项目地址:https://github.com/h82258652/SoftwareKobo.Net.WebUtility
Nuget地址:https://www.nuget.org/packages/SoftwareKobo.Net.WebUtility/
也是自己随便写的,反正能处理一下常见的字符就算了。(=_=)(巨硬的源码我实在是看不懂。。还打算照抄的……)
接下来回到 HotAsync 补充最后一句:
return NewsHelper.Deserialize(document);
完事。
其他什么 Recent(最新的)、Recommend(推荐)如法炮制。(Recent有两个接口,选择分页那个好了,反正感觉博客园内部实现也是重载)
接下来看新闻内容这个接口:
写上 DetailAsync 方法:
public static async Task<NewsDetail> DetailAsync(int newsId)
当然也有建上NewsDetail类。
1 /// <summary> 2 /// 新闻内容。 3 /// </summary> 4 public class NewsDetail 5 { 6 /// <summary> 7 /// Id。 8 /// </summary> 9 public int Id 10 { 11 get; 12 internal set; 13 } 14 15 /// <summary> 16 /// 标题。 17 /// </summary> 18 public string Title 19 { 20 get; 21 internal set; 22 } 23 24 /// <summary> 25 /// 转载自。 26 /// </summary> 27 public string SourceName 28 { 29 get; 30 internal set; 31 } 32 33 /// <summary> 34 /// 发表时间。 35 /// </summary> 36 public DateTime SubmitDate 37 { 38 get; 39 internal set; 40 } 41 42 /// <summary> 43 /// 内容。 44 /// </summary> 45 public string Content 46 { 47 get; 48 internal set; 49 } 50 51 /// <summary> 52 /// 新闻中用到的图片的路径。 53 /// </summary> 54 public ReadOnlyCollection<Uri> ImageUrl 55 { 56 get; 57 internal set; 58 } 59 60 /// <summary> 61 /// 上一条新闻的 Id。 62 /// </summary> 63 public int? PrevNews 64 { 65 get; 66 internal set; 67 } 68 69 /// <summary> 70 /// 下一条新闻的 Id。 71 /// </summary> 72 public int? NextNews 73 { 74 get; 75 internal set; 76 } 77 78 /// <summary> 79 /// 评论数。 80 /// </summary> 81 public int CommentCount 82 { 83 get; 84 internal set; 85 } 86 }
注意:
1、ImageUrl 用了 ReadOnlyCollection,原因同上,外部没必要修改。
2、PreNews 和 NextNews 使用可空类型,因为不一定有上一条或下一条。(都最新了,还能有下一条么)
接下来也是写个Helper,将Xml的内容转换为对象列表。完事。
剩下来获取评论也是差不多。
博客方面的接口封装基本上也是这么搞,注意的是新闻的评论和博客文章的评论可以用同一个模型来表达。
都写完后,感觉获取新闻获取评论不方便啊,得先访问Id,再调NewsService。天生懒,没办法,写个扩展方法。
1 /// <summary> 2 /// 新闻扩展。 3 /// </summary> 4 public static class NewsExtension 5 { 6 /// <summary> 7 /// 获取新闻评论。 8 /// </summary> 9 /// <param name="news">新闻。</param> 10 /// <param name="pageIndex">第几页,从 1 开始。</param> 11 /// <param name="pageSize">每页条数。</param> 12 /// <returns>新闻评论。</returns> 13 /// <exception cref="ArgumentNullException">新闻为 null。</exception> 14 public static async Task<IEnumerable<Comment>> CommentAsync(this News news, int pageIndex, int pageSize) 15 { 16 if (news == null) 17 { 18 throw new ArgumentNullException(nameof(news)); 19 } 20 return await NewsService.CommentAsync(news.Id, pageIndex, pageSize); 21 } 22 23 /// <summary> 24 /// 获取新闻内容。 25 /// </summary> 26 /// <param name="news">新闻。</param> 27 /// <returns>新闻内容。</returns> 28 /// <exception cref="ArgumentNullException">新闻为 null。</exception> 29 public static async Task<NewsDetail> DetailAsync(this News news) 30 { 31 if (news == null) 32 { 33 throw new ArgumentNullException(nameof(news)); 34 } 35 return await NewsService.DetailAsync(news.Id); 36 } 37 }
Q:为什么不在News类里写?A:没什么,保持模型纯正而已。-_-|||也好管理。
完事了,Release编译,弄到nuget包里,发布。测试添加引用,没注释!!!
回到项目,修改项目属性,生成XML,勾上!!
把XML一同弄到nuget包里,再发布,测试,好了。
At The End:
项目地址:https://github.com/h82258652/SoftwareKobo.CnblogsAPI
Nuget地址:https://www.nuget.org/packages/SoftwareKobo.CnblogsAPI/
博主我(h82258652)的话:有什么建议欢迎在下面评论提,毕竟博主我也是菜鸟。
↑重点当然要大只字!