之前在开发博客园新闻客户端的时候,需要获取博客园的新闻数据,最早开发出来的版本使用的是手机版的 html 解释的方式。效果算是做出来了,但是感觉获取到的数据太冗余,于是便查了一下有没有相应的接口。皇天不负有心人,博客园果然开放了些接口供我们使用。
博客服务接口:http://wcf.open.VEVb.com/blog/help
新闻服务接口:http://wcf.open.VEVb.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.VEVb.com/news/hot/10
工作良好,于是开始写这个接口的封装。
在 NewsService 中写上
public static async Task<IEnumerable<News>> HotAsync(int itemCount){ // TODO}
会发觉编译不通过(废话)。
于是新建一个News类先。修改方法。
编译!
不通过!!!
看错误列表:
不是4.5了么?怎么不能用 async?
解决方法:http://www.VEVb.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 TopicIcon104 {105 get;106 internal set;107 }108 109 /// <summary>110 /// 转载自。111 /// </summary>112 public string SourceName113 {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.VEVb.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.HasElements14 let temp = Deserialize(entry)15 where temp != null16 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 == null42 || title == null43 || summary == null44 || published == null45 || updated == null46 || href == null47 || diggs == null48 || views == null49 || comments == null50 || topic == null51 || topicIcon == null52 || sourceName == null)53 {54 return null;55 }56 57 return new News58 {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.Value71 };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 Id10 {11 get;12 internal set;13 }14 15 /// <summary>16 /// 标题。17 /// </summary>18 public string Title19 {20 get;21 internal set;22 }23 24 /// <summary>25 /// 转载自。26 /// </summary>27 public string SourceName28 {29 get;30 internal set;31 }32 33 /// <summary>34 /// 发表时间。35 /// </summary>36 public DateTime SubmitDate37 {38 get;39 internal set;40 }41 42 /// <summary>43 /// 内容。44 /// </summary>45 public string Content46 {47 get;48 internal set;49 }50 51 /// <summary>52 /// 新闻中用到的图片的路径。53 /// </summary>54 public ReadOnlyCollection<Uri> ImageUrl55 {56 get;57 internal set;58 }59 60 /// <summary>61 /// 上一条新闻的 Id。62 /// </summary>63 public int? PrevNews64 {65 get;66 internal set;67 }68 69 /// <summary>70 /// 下一条新闻的 Id。71 /// </summary>72 public int? NextNews73 {74 get;75 internal set;76 }77 78 /// <summary>79 /// 评论数。80 /// </summary>81 public int CommentCount82 {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)的话:有什么建议欢迎在下面评论提,毕竟博主我也是菜鸟。
↑重点当然要大只字!
新闻热点
疑难解答