花了几天时间,消耗了九牛六虎之力,新浪微博大部分API已经封装,但有部分API实在太难封装。
说起这封装,我必须严重地、从人品和技术层面鄙视一下新浪的程序员,实在太菜了。估计菜鸟都被大企业吸收了,菜到连面向对象都不懂。建议新浪的菜菜们向淘宝学习下,人家淘宝还同时有xml和JSON两种数据格式。
同样的内容,返回的JSON对象居然会出现不同结构,更可恶的,像公共API中获取城市列表,国家区域代码列表的返回结果,实在让人不得不发笑。那些JSON用JS读起来都困难,更何况要进行封装呢。根本没法封装,因此在论坛上抱怨的人不少,可是新浪官方呢,置之不管,就当没看见一样,看来,大企业就是这点水平。
题外话不多说,回归主题。除了权限不够和无法封装的API外,其余的API都封装了。先上一张代码地图。
这图有点像蜘蛛网,非常有美感,可惜是静止的,如果会动的,一定很好看。
我不可能把代码一行一行地向大家讲述,因为这样做会相当恶心。现在菜菜们都整天拿开源来装逼,所以,为了迎合广大菜菜所说的所谓“趋势”,我也决定把我这份乱七八糟的代码向全人类公开。
下载地址:http://vdisk.weibo.com/s/z7iFc2gCCwC1b
接下来,就给大家说说原理和思路,这才是编程的关键。
1、不管是微博API还是其他的开放平台的API,都有N多共同点。首先,用户要用自己的帐号登录,然后由用户决定是否授权给我们的应用程序,如果用户已同意授权,我们会得到一个授权码(Code)。
2、拿到Code后,我们要用这个Code来换取一个accessToken,就相当于用户授予我们一把钥匙,我们再用这把钥匙去开启库房的门,然后取出一个临时令牌,就好比在汉代,大臣代表天子巡视诸侯国时,手里要持着天子的符节一样。有了Token,我们的应用才能进行一系列操作。
微博的API的每一次调用,其实就是一轮HTTP请求-响应的往返过程,说白了,就是一问一答,我们调用API是问,服务器完成相关处理后返回结果给我们,为答。我们也知道,HTTP发送数据,用得最多的两种方法是GET和POST,至于是GET还是POST,我们按照API文档的说明去干就行了。
而对于服务器返回的数据(JSON),我是通过数据协定的形式来进行封装的。比如:
一个表示用户信息的回复JSON如下:
{"id": 1404376560,"screen_name": "zaku","name": "zaku","PRovince": "11","city": "5","location": "北京 朝阳区","description": "人生五十年,乃如梦如幻;有生斯有死,壮士复何憾。","url": "http://blog.sina.com.cn/zaku","profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1","domain": "zaku","gender": "m","followers_count": 1204,"friends_count": 447,"statuses_count": 2908,"favourites_count": 0,"created_at": "Fri Aug 28 00:00:00 +0800 2009","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false,"status": {"created_at": "Tue May 24 18:04:53 +0800 2011","id": 11142488790,"text": "我的相机到了。","source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>","favorited":false,"truncated":false,"in_reply_to_status_id": "","in_reply_to_user_id": "","in_reply_to_screen_name": "","geo":null,"mid": "5610221544300749636","annotations": [],"reposts_count": 5,"comments_count": 8},"allow_all_comment":true,"avatar_large": "http://tp1.sinaimg.cn/1404376560/180/0/1","verified_reason": "","follow_me":false,"online_status": 0,"bi_followers_count": 215}
我们可以根据JSON中每个成员的名字,用代码封装成一个类。
///<summary>///用户实体///</summary>[DataContract]publicclassUserInfo
{///<summary>///用户UID///</summary>[DataMember(Name ="id")]publiclongID {get;set; }///<summary>///字符串型的用户UID///</summary>[DataMember(Name ="idstr")]publicstringIDStr {get;set; }///<summary>///用户昵称///</summary>[DataMember(Name ="screen_name")]publicstringScreenName {get;set; }///<summary>///友好显示名称///</summary>[DataMember(Name ="name")]publicstringName {get;set; }///<summary>///用户所在省级ID///</summary>[DataMember(Name ="province")]publicintProvince {get;set; }///<summary>///用户所在城市ID///</summary>[DataMember(Name ="city")]publicintCity {get;set; }///<summary>///用户所在地///</summary>[DataMember(Name ="location")]publicstringLocation {get;set; }///<summary>///用户个人描述///</summary>[DataMember(Name ="description")]publicstringDescription {get;set; }///<summary>///用户博客地址///</summary>[DataMember(Name ="url")]publicstringUrl {get;set; }///<summary>///用户头像地址(中图),50×50像素///</summary>[DataMember(Name ="profile_image_url")]publicstringProfileImageUrl {get;set; }///<summary>///用户的微博统一URL地址///</summary>[DataMember(Name ="profile_url")]publicstringProfileUrl {get;set; }///<summary>///用户的个性化域名///</summary>[DataMember(Name ="domain")]publicstringDomain {get;set; }///<summary>///用户的微号///</summary>[DataMember(Name ="weihao")]publicstringWeiHao {get;set; }///<summary>///性别,m:男、f:女、n:未知///</summary>[DataMember(Name ="gender")]publicstringGender {get;set; }///<summary>///粉丝数///</summary>[DataMember(Name ="followers_count")]publicintFollowersCount {get;set; }///<summary>///关注数///</summary>[DataMember(Name ="friends_count")]publicintFriendsCount {get;set; }///<summary>///微博数///</summary>[DataMember(Name ="statuses_count")]publicintStatusesCount {get;set; }///<summary>///收藏数///</summary>[DataMember(Name ="favourites_count")]publicintFavouritesCount {get;set; }///<summary>///用户创建(注册)时间///</summary>[DataMember(Name ="created_at")]publicstringCreatedAt {get;set; }///<summary>///暂未支持///</summary>[DataMember(Name ="following")]publicboolFollowing {get;set; }///<summary>///是否允许所有人给我发私信,true:是,false:否///</summary>[DataMember(Name ="allow_all_act_msg")]publicboolAllowAllActMsg {get;set; }///<summary>///是否允许标识用户的地理位置,true:是,false:否///</summary>[DataMember(Name ="geo_enabled")]publicboolGeoEnabled {get;set; }///<summary>///是否是微博认证用户,即加V用户,true:是,false:否///</summary>[DataMember(Name ="verified")]publicboolVerified {get;set; }///<summary>///暂未支持///</summary>[DataMember(Name ="verified_type")]publicintVerifiedType {get;set; }///<summary>///用户备注信息///</summary>[DataMember(Name ="remark")]publicstringRemark {get;set; }///<summary>///用户的最近一条微博信息///</summary>[DataMember(Name ="status")]publicStatus Status {get;set; }///<summary>///当对象数据未返回完整微博信息时,将填充该字段///</summary>[DataMember(Name ="status_id")]publiclongStatusID {get;set; }///<summary>///是否允许所有人对我的微博进行评论,true:是,false:否///</summary>[DataMember(Name ="allow_all_comment")]publicboolAllowAllComment {get;set; }///<summary>///用户头像地址(大图),180×180像素///</summary>[DataMember(Name ="avatar_large")]publicstringAvatarLarge {get;set; }///<summary>///用户头像地址(高清),高清头像原图///</summary>[DataMember(Name ="avatar_hd")]publicstringAvatarHd {get;set; }///<summary>///认证原因///</summary>[DataMember(Name ="verified_reason")]publicstringVerifiedReason {get;set; }///<summary>///该用户是否关注当前登录用户,true:是,false:否///</summary>[DataMember(Name ="follow_me")]publicboolFollowMe {get;set; }///<summary>///用户的在线状态,0:不在线、1:在线///</summary>[DataMember(Name ="online_status")]publicintOnlineStatus {get;set; }///<summary>///用户的互粉数///</summary>[DataMember(Name ="bi_followers_count")]publicintBiFollowersCount {get;set; }///<summary>///用户当前的语言版本,zh-cn:简体中文,zh-tw:繁体中文,en:英语///</summary>[DataMember(Name ="lang")]publicstringLang {get;set; }
}
其中,Status属性表示用户发表的最新一条微博,它由一个表示微博的对象构成。
{"created_at": "Tue May 24 18:04:53 +0800 2011","id": 11142488790,"text": "我的相机到了。","source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>","favorited":false,"truncated":false,"in_reply_to_status_id": "","in_reply_to_user_id": "","in_reply_to_screen_name": "","geo":null,"mid": "5610221544300749636","annotations": [],"reposts_count": 5,"comments_count": 8}
同样,我们也用一个数据协定类来封装它,表示一条微博的信息。
///<summary>///微博实体///</summary>[DataContract]publicclassStatus
{///<summary>///微博创建时间///</summary>[DataMember(Name ="created_at")]publicstringCreateAt {get;set; }///<summary>///微博ID///</summary>[DataMember(Name ="id")]publicInt64 ID {get;set; }///<summary>///微博MID///</summary>[DataMember(Name ="mid")]publicInt64 MID {set;get; }///<summary>///字符串型的微博ID///</summary>[DataMember(Name ="idstr")]publicstringIDStr {get;set; }///<summary>///微博信息内容///</summary>[DataMember(Name ="text")]publicstringText {get;set; }///<summary>///微博来源///</summary>[DataMember(Name ="source")]publicstringSource {get;set; }///<summary>///是否已收藏,true:是,false:否///</summary>[DataMember(Name ="favorited")]publicboolFavorited {get;set; }///<summary>///是否被截断,true:是,false:否///</summary>[DataMember(Name ="truncated")]publicboolTruncated {get;set; }///<summary>///(暂未支持)回复ID///</summary>[DataMember(Name ="in_reply_to_status_id")]publicstringInReplyToStatusID {get;set; }///<summary>///(暂未支持)回复人UID///</summary>[DataMember(Name ="in_reply_to_user_id")]publicstringInReplyToUserID {get;set; }///<summary>///(暂未支持)回复人昵称///</summary>[DataMember(Name ="in_reply_to_screen_name")]publicstringInReplyToScreenName {get;set; }///<summary>///缩略图片地址///</summary>[DataMember(Name ="thumbnail_pic")]publicstringThumbnailPic {get;set; }///<summary>///中等尺寸图片地址///</summary>[DataMember(Name ="bmiddle_pic")]publicstringBmiddlePic {get;set; }///<summary>///原始图片地址///</summary>[DataMember(Name ="original_pic")]publicstringOriginalPic {get;set; }///<summary>///地理信息///</summary>[DataMember(Name ="geo")]publicGeoInfo Geo {get;set; }///<summary>///微博作者的用户信息///</summary>[DataMember(Name ="user")]publicUserInfo User {get;set; }///<summary>///被转发的原微博信息字段,当该微博为转发微博时返回///</summary>[DataMember(Name ="retweeted_status")]publicStatus RetweetedStatus {get;set; }///<summary>///转发数///</summary>[DataMember(Name ="reposts_count")]publicintRepostsCount {get;set; }///<summary>///评论数///</summary>[DataMember(Name ="comments_count")]publicintCommentsCount {get;set; }///<summary>///表态数///</summary>[DataMember(Name ="attitudes_count")]publicintAttitudesCount {get;set; }///<summary>///暂未支持///</summary>[DataMember(Name ="mlevel")]publicintMlevel {get;set; }///<summary>///微博的可见性及指定可见分组信息。该object中type取值,0:普通微博,1:私密微博,3:指定分组微博,4:密友微博;list_id为分组的组号///</summary>[DataMember(Name ="visible")]publicdynamic Visible {get;set; }///<summary>///微博配图地址。多图时返回多图链接。无配图返回“[]”///</summary>[DataMember(Name ="pic_urls")]publicdynamic PicUrls {get;set; }///<summary>///微博流内的推广微博ID///</summary>[DataMember(Name ="ad")]publicdynamic Ad {get;set; }///<summary>///当不返回user字段时,此属性可填充UID///</summary>[DataMember(Name ="uid")]publiclongUid {get;set; }
}
我们在接收到服务器的回应后,直接对数据进行反序列化,就可以得到这些数据的封装实例,面向对象,操作起来更方便。这些被封装的基本类型,我都放到了Models目录下。为了实现反序列化,我定义了一个类,公开一个静态方法,以便从JSON数据生成对象实例。
publicclassJsonSerializeHelper
{publicstaticT ReadDataFromJson<T>(Stream inStream)
{
DataContractJsonSerializer s=newDataContractJsonSerializer(typeof(T));return(T)s.ReadObject(inStream);
}
}
因为我们要反序列化的类型是多种多样的,无法定论,因此,这里使用泛型参数较合适。具体反序列化为哪种类型的实例,在运行时决定。
由于每个API的调用都是一轮HTTP请求/回应,无论你调用哪个API都一样,这个行为是通过的,所以,我就统一进行提取,并使用.NET 4.5新增的HttpClient类来处理,这个类很强大,把一些不太好处理的HTTP请求都为我们封装好了,尤其是像form-data这种POST的数据,如MultiPart form data这些,尤其是向服务器上传文件时较容易处理,省去许多不必要的机械性工作,将人类从无必要的代码中解放出来,极大地提高生产率,这是.NET最牛X的地方。
internalstaticasyncTask<TResult> SendRequestWithMultipartFormDataAsync<TResult>(stringrelateUrl, IDictionary<string,object> parms,stringfilename)
{
Uri reqUri=newUri(API_BASE_RUI);
reqUri=newUri(reqUri, relateUrl);
TResult result=default(TResult);using(HttpClient client =newHttpClient())
{stringb ="---------------------"+ DateTime.Now.Ticks.ToString("x");
MultipartFormDataContent formData=newMultipartFormDataContent(b);foreach(varpairinparms)
{stringstr = pair.Valueasstring;if(str !=null)
{
StringContent stringContent=newStringContent(pair.Valueasstring);
formData.Add(stringContent, pair.Key);
}
Stream stream= pair.ValueasStream;if(stream !=null)
{
StreamContent streamContent=newStreamContent(stream);
formData.Add(streamContent, pair.Key, filename);
}
}varresponse =awaitclient.PostAsync(reqUri, formData);if(response.IsSuccessStatusCode)
{using(Stream backstream =awaitresponse.Content.ReadAsStreamAsync())
{
result= JsonSerializeHelper.ReadDataFromJson<TResult>(backstream);
}
}else{
ErrorData err=null;using(Stream errstream =awaitresponse.Content.ReadAsStreamAsync())
{
err= JsonSerializeHelper.ReadDataFromJson<ErrorData>(errstream);
}thrownewWeiboException(err);
}
}returnresult;
}internalstaticasyncTask<string> HttpGetStringAsync(stringrelateUrl)
{
Uri base_uri=newUri(API_BASE_RUI);
Uri request_uri=newUri(base_uri, relateUrl);stringresult ="";using(HttpClient client =newHttpClient())
{
result=awaitclient.GetStringAsync(request_uri);
}returnresult;
}internalstaticasyncTask<Stream> HttpGetStreamAsync(stringrelateUrl)
{
Uri base_uri=newUri(API_BASE_RUI);
base_uri=newUri(base_uri, relateUrl);
Stream streamres=null;using(HttpClient client =newHttpClient())
{varstreamtmp =awaitclient.GetStreamAsync(base_uri);
streamres=newMemoryStream();
streamtmp.CopyTo(streamres);
streamtmp.Dispose();
}returnstreamres;
}
上面仅仅列举了一两个方法,详细的代码大家可以下载源代码看。
在我发布的解决方案中,有一个项目名为WeiboTest,它是一个单元测试项目,是我在封装API过程用来测试调用而写的。
这个SDK可用在桌面程序和Web应用程序中,对于WP和Store App,我一开始是考虑建一个可移植的Portable项目的,但由于我仅在试水阶段,后来就没考虑移植了,其实绝大部分代码是可以通用的,但有一小部分不行,比如文件操作就不能通用,因为WP上的文件是通过独立存储来处理的,而不像桌面环境中那样使用物理文件来处理。等代码完善后,我会考虑将它改为一个通用类库。
下面就介绍一下我封装了哪些API。
一、登录授权部分我合为一体了,不使用新浪弹出的授权页面,而是直接模拟用户登录时POST的数据,直接获取Code,然后调用OAuth2的API换取Token。同时还公开用于取消授权的RevokeOAuth2Async。
二、微博模块。1、获取最近的公共微博列表,
2、获取好友/关注人/自己的最新微博列表。
3、获取与当前登录用户相互关注的用户的最新微博列表。
4、获取某条微博的转发列表。
5、获取@当前用户 的微博列表。
6、根据微博ID获取单条微博的详细信息。
7、批量获取微博的转发数和评论数。
8、微博ID和MID的相互获取。
9、获取微博官方表情列表。
10、转发微博。
11、发表微博 / 发表带图片的微博。
12、删除微博。
三、评论模块。1、获取某条微博的评论列表。
2、获取当前登录用户所发表的评论列表。
3、获取当前用户接收到的评论列表。
4、获取@当前用户的评论列表。
5、批量获取评论列表。
6、发表评论。
7、删除指定评论。
8、批量删除评论。
9、回复某条评论。
四、帐号模块。1、获取当前用户的隐私设置信息。是设置项,不是获取隐私,别想歪了。
2、获取学校列表,这个好像没什么用。
五、关系模块。1、获取当前登录用户的关注列表。
2、获取两个用户之间的共同关注人列表。比如我关注了A,而C也关注了A,因而我和C的共同关注人就是A。
3、获取用户的相互关注人列表。也就是有哪些人跟我互粉。
4、获取当前用户的粉丝列表。即哪些人关注了我。
5、获取用户的活跃粉丝列表。这个不知道干什么的,反正我测试的时候返回空。
6、获取当前登录用户关注列表中同时关注了某用户的列表。比如,我关注了A,获取我关注的用户中也关注了A的用户列表。
7、关注一位用户。
8、取消关注某位用户。
六、用户模块。1、根据用户ID获取用户信息。
2、批量获取用户的关注数、粉丝数、微博数。
3、通过个性域名获取用户信息以及最新发表的一条微博。
七、短链接模块。1、长链接转为短链接。
2、短链接转为长链接。
八、地理位置模块。1、根据IP地址返回地理信息。
2、根据具体地址返回地理坐标信息。
3、根据地理坐标(经度,纬度)返回地址信息。
这个SDK不算很完美,由于新浪的程序员比较菜,有些JSON数据结构相当不合理,暂时无法封装,等到哪天我想到解决方法后再补充。