花了幾天時(shí)間,消耗了九牛六虎之力,新浪微博大部分API已經(jīng)封裝,但有部分API實(shí)在太難封裝。
說(shuō)起這封裝,我必須嚴(yán)重地、從人品和技術(shù)層面鄙視一下新浪的程序員,實(shí)在太菜了。估計(jì)菜鳥(niǎo)都被大企業(yè)吸收了,菜到連面向?qū)ο蠖疾欢=ㄗh新浪的菜菜們向淘寶學(xué)習(xí)下,人家淘寶還同時(shí)有xml和JSON兩種數(shù)據(jù)格式。
同樣的內(nèi)容,返回的JSON對(duì)象居然會(huì)出現(xiàn)不同結(jié)構(gòu),更可惡的,像公共API中獲取城市列表,國(guó)家區(qū)域代碼列表的返回結(jié)果,實(shí)在讓人不得不發(fā)笑。那些JSON用JS讀起來(lái)都困難,更何況要進(jìn)行封裝呢。根本沒(méi)法封裝,因此在論壇上抱怨的人不少,可是新浪官方呢,置之不管,就當(dāng)沒(méi)看見(jiàn)一樣,看來(lái),大企業(yè)就是這點(diǎn)水平。
題外話(huà)不多說(shuō),回歸主題。除了權(quán)限不夠和無(wú)法封裝的API外,其余的API都封裝了。先上一張代碼地圖。

這圖有點(diǎn)像蜘蛛網(wǎng),非常有美感,可惜是靜止的,如果會(huì)動(dòng)的,一定很好看。
我不可能把代碼一行一行地向大家講述,因?yàn)檫@樣做會(huì)相當(dāng)惡心。現(xiàn)在菜菜們都整天拿開(kāi)源來(lái)裝逼,所以,為了迎合廣大菜菜所說(shuō)的所謂“趨勢(shì)”,我也決定把我這份亂七八糟的代碼向全人類(lèi)公開(kāi)。
下載地址:http://vdisk.weibo.com/s/z7iFc2gCCwC1b
接下來(lái),就給大家說(shuō)說(shuō)原理和思路,這才是編程的關(guān)鍵。
1、不管是微博API還是其他的開(kāi)放平臺(tái)的API,都有N多共同點(diǎn)。首先,用戶(hù)要用自己的帳號(hào)登錄,然后由用戶(hù)決定是否授權(quán)給我們的應(yīng)用程序,如果用戶(hù)已同意授權(quán),我們會(huì)得到一個(gè)授權(quán)碼(Code)。
2、拿到Code后,我們要用這個(gè)Code來(lái)?yè)Q取一個(gè)access Token,就相當(dāng)于用戶(hù)授予我們一把鑰匙,我們?cè)儆眠@把鑰匙去開(kāi)啟庫(kù)房的門(mén),然后取出一個(gè)臨時(shí)令牌,就好比在漢代,大臣代表天子巡視諸侯國(guó)時(shí),手里要持著天子的符節(jié)一樣。有了Token,我們的應(yīng)用才能進(jìn)行一系列操作。
微博的API的每一次調(diào)用,其實(shí)就是一輪HTTP請(qǐng)求-響應(yīng)的往返過(guò)程,說(shuō)白了,就是一問(wèn)一答,我們調(diào)用API是問(wèn),服務(wù)器完成相關(guān)處理后返回結(jié)果給我們,為答。我們也知道,HTTP發(fā)送數(shù)據(jù),用得最多的兩種方法是GET和POST,至于是GET還是POST,我們按照API文檔的說(shuō)明去干就行了。
而對(duì)于服務(wù)器返回的數(shù)據(jù)(JSON),我是通過(guò)數(shù)據(jù)協(xié)定的形式來(lái)進(jìn)行封裝的。比如:
一個(gè)表示用戶(hù)信息的回復(fù)JSON如下:
{ "id": 1404376560, "screen_name": "zaku", "name": "zaku", ", "city": "5", "location": "北京 朝陽(yáng)區(qū)", "descr, "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": "我的相機(jī)到了。", "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}
我們可以根據(jù)JSON中每個(gè)成員的名字,用代碼封裝成一個(gè)類(lèi)。
/// <summary> /// 用戶(hù)實(shí)體 /// </summary> [DataContract] public class UserInfo { /// <summary> /// 用戶(hù)UID /// </summary> [DataMember(Name = "id")] public long ID { get; set; } /// <summary> /// 字符串型的用戶(hù)UID /// </summary> [DataMember(Name = "idstr")] public string IDStr { get; set; } /// <summary> /// 用戶(hù)昵稱(chēng) /// </summary> [DataMember(Name = "screen_name")] public string ScreenName { get; set; } /// <summary> /// 友好顯示名稱(chēng) /// </summary> [DataMember(Name = "name")] public string Name { get; set; } /// <summary> /// 用戶(hù)所在省級(jí)ID /// </summary> [DataMember(Name = "province")] public int Province { get; set; } /// <summary> /// 用戶(hù)所在城市ID /// </summary> [DataMember(Name = "city")] public int City { get; set; } /// <summary> /// 用戶(hù)所在地 /// </summary> [DataMember(Name = "location")] public string Location { get; set; } /// <summary> /// 用戶(hù)個(gè)人描述 /// </summary> [DataMember(Name = "description")] public string Description { get; set; } /// <summary> /// 用戶(hù)博客地址 /// </summary> [DataMember(Name = "url")] public string Url { get; set; } /// <summary> /// 用戶(hù)頭像地址(中圖),50×50像素 /// </summary> [DataMember(Name = "profile_image_url")] public string ProfileImageUrl { get; set; } /// <summary> /// 用戶(hù)的微博統(tǒng)一URL地址 /// </summary> [DataMember(Name = "profile_url")] public string ProfileUrl { get; set; } /// <summary> /// 用戶(hù)的個(gè)性化域名 /// </summary> [DataMember(Name = "domain")] public string Domain { get; set; } /// <summary> /// 用戶(hù)的微號(hào) /// </summary> [DataMember(Name = "weihao")] public string WeiHao { get; set; } /// <summary> /// 性別,m:男、f:女、n:未知 /// </summary> [DataMember(Name = "gender")] public string Gender { get; set; } /// <summary> /// 粉絲數(shù) /// </summary> [DataMember(Name = "followers_count")] public int FollowersCount { get; set; } /// <summary> /// 關(guān)注數(shù) /// </summary> [DataMember(Name = "friends_count")] public int FriendsCount { get; set; } /// <summary> /// 微博數(shù) /// </summary> [DataMember(Name = "statuses_count")] public int StatusesCount { get; set; } /// <summary> /// 收藏?cái)?shù) /// </summary> [DataMember(Name = "favourites_count")] public int FavouritesCount { get; set; } /// <summary> /// 用戶(hù)創(chuàng)建(注冊(cè))時(shí)間 /// </summary> [DataMember(Name = "created_at")] public string CreatedAt { get; set; } /// <summary> /// 暫未支持 /// </summary> [DataMember(Name = "following")] public bool Following { get; set; } /// <summary> /// 是否允許所有人給我發(fā)私信,true:是,false:否 /// </summary> [DataMember(Name = "allow_all_act_msg")] public bool AllowAllActMsg { get; set; } /// <summary> /// 是否允許標(biāo)識(shí)用戶(hù)的地理位置,true:是,false:否 /// </summary> [DataMember(Name = "geo_enabled")] public bool GeoEnabled { get; set; } /// <summary> /// 是否是微博認(rèn)證用戶(hù),即加V用戶(hù),true:是,false:否 /// </summary> [DataMember(Name = "verified")] public bool Verified { get; set; } /// <summary> /// 暫未支持 /// </summary> [DataMember(Name = "verified_type")] public int VerifiedType { get; set; } /// <summary> /// 用戶(hù)備注信息 /// </summary> [DataMember(Name = "remark")] public string Remark { get; set; } /// <summary> /// 用戶(hù)的最近一條微博信息 /// </summary> [DataMember(Name = "status")] public Status Status { get; set; } /// <summary> /// 當(dāng)對(duì)象數(shù)據(jù)未返回完整微博信息時(shí),將填充該字段 /// </summary> [DataMember(Name = "status_id")] public long StatusID { get; set; } /// <summary> /// 是否允許所有人對(duì)我的微博進(jìn)行評(píng)論,true:是,false:否 /// </summary> [DataMember(Name = "allow_all_comment")] public bool AllowAllComment { get; set; } /// <summary> /// 用戶(hù)頭像地址(大圖),180×180像素 /// </summary> [DataMember(Name = "avatar_large")] public string AvatarLarge { get; set; } /// <summary> /// 用戶(hù)頭像地址(高清),高清頭像原圖 /// </summary> [DataMember(Name = "avatar_hd")] public string AvatarHd { get; set; } /// <summary> /// 認(rèn)證原因 /// </summary> [DataMember(Name = "verified_reason")] public string VerifiedReason { get; set; } /// <summary> /// 該用戶(hù)是否關(guān)注當(dāng)前登錄用戶(hù),true:是,false:否 /// </summary> [DataMember(Name = "follow_me")] public bool FollowMe { get; set; } /// <summary> /// 用戶(hù)的在線(xiàn)狀態(tài),0:不在線(xiàn)、1:在線(xiàn) /// </summary> [DataMember(Name = "online_status")] public int OnlineStatus { get; set; } /// <summary> /// 用戶(hù)的互粉數(shù) /// </summary> [DataMember(Name = "bi_followers_count")] public int BiFollowersCount { get; set; } /// <summary> /// 用戶(hù)當(dāng)前的語(yǔ)言版本,zh-cn:簡(jiǎn)體中文,zh-tw:繁體中文,en:英語(yǔ) /// </summary> [DataMember(Name = "lang")] public string Lang { get; set; } }
其中,Status屬性表示用戶(hù)發(fā)表的最新一條微博,它由一個(gè)表示微博的對(duì)象構(gòu)成。
{ "created_at": "Tue May 24 18:04:53 +0800 2011", "id": 11142488790, "text": "我的相機(jī)到了。", "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 }
同樣,我們也用一個(gè)數(shù)據(jù)協(xié)定類(lèi)來(lái)封裝它,表示一條微博的信息。
/// <summary> /// 微博實(shí)體 /// </summary> [DataContract] public class Status { /// <summary> /// 微博創(chuàng)建時(shí)間 /// </summary> [DataMember(Name = "created_at")] public string CreateAt { get; set; } /// <summary> /// 微博ID /// </summary> [DataMember(Name = "id")] public Int64 ID { get; set; } /// <summary> /// 微博MID /// </summary> [DataMember(Name = "mid")] public Int64 MID { set; get; } /// <summary> /// 字符串型的微博ID /// </summary> [DataMember(Name = "idstr")] public string IDStr { get; set; } /// <summary> /// 微博信息內(nèi)容 /// </summary> [DataMember(Name = "text")] public string Text { get; set; } /// <summary> /// 微博來(lái)源 /// </summary> [DataMember(Name = "source")] public string Source { get; set; } /// <summary> /// 是否已收藏,true:是,false:否 /// </summary> [DataMember(Name = "favorited")] public bool Favorited { get; set; } /// <summary> /// 是否被截?cái)啵瑃rue:是,false:否 /// </summary> [DataMember(Name = "truncated")] public bool Truncated { get; set; } /// <summary> /// (暫未支持)回復(fù)ID /// </summary> [DataMember(Name = "in_reply_to_status_id")] public string InReplyToStatusID { get; set; } /// <summary> /// (暫未支持)回復(fù)人UID /// </summary> [DataMember(Name = "in_reply_to_user_id")] public string InReplyToUserID { get; set; } /// <summary> /// (暫未支持)回復(fù)人昵稱(chēng) /// </summary> [DataMember(Name = "in_reply_to_screen_name")] public string InReplyToScreenName { get; set; } /// <summary> /// 縮略圖片地址 /// </summary> [DataMember(Name = "thumbnail_pic")] public string ThumbnailPic { get; set; } /// <summary> /// 中等尺寸圖片地址 /// </summary> [DataMember(Name = "bmiddle_pic")] public string BmiddlePic { get; set; } /// <summary> /// 原始圖片地址 /// </summary> [DataMember(Name = "original_pic")] public string OriginalPic { get; set; } /// <summary> /// 地理信息 /// </summary> [DataMember(Name = "geo")] public GeoInfo Geo { get; set; } /// <summary> /// 微博作者的用戶(hù)信息 /// </summary> [DataMember(Name = "user")] public UserInfo User { get; set; } /// <summary> /// 被轉(zhuǎn)發(fā)的原微博信息字段,當(dāng)該微博為轉(zhuǎn)發(fā)微博時(shí)返回 /// </summary> [DataMember(Name = "retweeted_status")] public Status RetweetedStatus { get; set; } /// <summary> /// 轉(zhuǎn)發(fā)數(shù) /// </summary> [DataMember(Name = "reposts_count")] public int RepostsCount { get; set; } /// <summary> /// 評(píng)論數(shù) /// </summary> [DataMember(Name = "comments_count")] public int CommentsCount { get; set; } /// <summary> /// 表態(tài)數(shù) /// </summary> [DataMember(Name = "attitudes_count")] public int AttitudesCount { get; set; } /// <summary> /// 暫未支持 /// </summary> [DataMember(Name = "mlevel")] public int Mlevel { get; set; } /// <summary> /// 微博的可見(jiàn)性及指定可見(jiàn)分組信息。該object中type取值,0:普通微博,1:私密微博,3:指定分組微博,4:密友微博;list_id為分組的組號(hào) /// </summary> [DataMember(Name = "visible")] public dynamic Visible { get; set; } /// <summary> /// 微博配圖地址。多圖時(shí)返回多圖鏈接。無(wú)配圖返回“[]” /// </summary> [DataMember(Name = "pic_urls")] public dynamic PicUrls { get; set; } /// <summary> /// 微博流內(nèi)的推廣微博ID /// </summary> [DataMember(Name = "ad")] public dynamic Ad { get; set; } /// <summary> /// 當(dāng)不返回user字段時(shí),此屬性可填充UID /// </summary> [DataMember(Name = "uid")] public long Uid { get; set; } }
我們?cè)诮邮盏椒?wù)器的回應(yīng)后,直接對(duì)數(shù)據(jù)進(jìn)行反序列化,就可以得到這些數(shù)據(jù)的封裝實(shí)例,面向?qū)ο螅僮髌饋?lái)更方便。這些被封裝的基本類(lèi)型,我都放到了Models目錄下。為了實(shí)現(xiàn)反序列化,我定義了一個(gè)類(lèi),公開(kāi)一個(gè)靜態(tài)方法,以便從JSON數(shù)據(jù)生成對(duì)象實(shí)例。
public class JsonSerializeHelper { public static T ReadDataFromJson<T>(Stream inStream) { DataContractJsonSerializer s = new DataContractJsonSerializer(typeof(T)); return (T)s.ReadObject(inStream); } }
因?yàn)槲覀円葱蛄谢念?lèi)型是多種多樣的,無(wú)法定論,因此,這里使用泛型參數(shù)較合適。具體反序列化為哪種類(lèi)型的實(shí)例,在運(yùn)行時(shí)決定。
由于每個(gè)API的調(diào)用都是一輪HTTP請(qǐng)求/回應(yīng),無(wú)論你調(diào)用哪個(gè)API都一樣,這個(gè)行為是通過(guò)的,所以,我就統(tǒng)一進(jìn)行提取,并使用.NET 4.5新增的HttpClient類(lèi)來(lái)處理,這個(gè)類(lèi)很強(qiáng)大,把一些不太好處理的HTTP請(qǐng)求都為我們封裝好了,尤其是像form-data這種POST的數(shù)據(jù),如MultiPart form data這些,尤其是向服務(wù)器上傳文件時(shí)較容易處理,省去許多不必要的機(jī)械性工作,將人類(lèi)從無(wú)必要的代碼中解放出來(lái),極大地提高生產(chǎn)率,這是.NET最牛X的地方。
internal static async Task<TResult> SendRequestWithMultipartFormDataAsync<TResult>(string relateUrl, IDictionary<string, object> parms, string filename) { Uri reqUri = new Uri(API_BASE_RUI); reqUri = new Uri(reqUri, relateUrl); TResult result = default(TResult); using (HttpClient client = new HttpClient()) { string b = "---------------------" + DateTime.Now.Ticks.ToString("x"); MultipartFormDataContent formData = new MultipartFormDataContent(b); foreach (var pair in parms) { string str = pair.Value as string; if (str != null) { StringContent stringContent = new StringContent(pair.Value as string); formData.Add(stringContent, pair.Key); } Stream stream = pair.Value as Stream; if (stream != null) { StreamContent streamContent = new StreamContent(stream); formData.Add(streamContent, pair.Key, filename); } } var response = await client.PostAsync(reqUri, formData); if (response.IsSuccessStatusCode) { using (Stream backstream = await response.Content.ReadAsStreamAsync()) { result = JsonSerializeHelper.ReadDataFromJson<TResult>(backstream); } } else { ErrorData err = null; using (Stream errstream = await response.Content.ReadAsStreamAsync()) { err = JsonSerializeHelper.ReadDataFromJson<ErrorData>(errstream); } throw new WeiboException(err); } } return result; } internal static async Task<string> HttpGetStringAsync(string relateUrl) { Uri base_uri = new Uri(API_BASE_RUI); Uri request_uri = new Uri(base_uri, relateUrl); string result = ""; using (HttpClient client = new HttpClient()) { result = await client.GetStringAsync(request_uri); } return result; } internal static async Task<Stream> HttpGetStreamAsync(string relateUrl) { Uri base_uri = new Uri(API_BASE_RUI); base_uri = new Uri(base_uri, relateUrl); Stream streamres = null; using (HttpClient client = new HttpClient()) { var streamtmp = await client.GetStreamAsync(base_uri); streamres = new MemoryStream(); streamtmp.CopyTo(streamres); streamtmp.Dispose(); } return streamres; }
上面僅僅列舉了一兩個(gè)方法,詳細(xì)的代碼大家可以下載源代碼看。
在我發(fā)布的解決方案中,有一個(gè)項(xiàng)目名為WeiboTest,它是一個(gè)單元測(cè)試項(xiàng)目,是我在封裝API過(guò)程用來(lái)測(cè)試調(diào)用而寫(xiě)的。
這個(gè)SDK可用在桌面程序和Web應(yīng)用程序中,對(duì)于WP和Store App,我一開(kāi)始是考慮建一個(gè)可移植的Portable項(xiàng)目的,但由于我僅在試水階段,后來(lái)就沒(méi)考慮移植了,其實(shí)絕大部分代碼是可以通用的,但有一小部分不行,比如文件操作就不能通用,因?yàn)閃P上的文件是通過(guò)獨(dú)立存儲(chǔ)來(lái)處理的,而不像桌面環(huán)境中那樣使用物理文件來(lái)處理。等代碼完善后,我會(huì)考慮將它改為一個(gè)通用類(lèi)庫(kù)。
下面就介紹一下我封裝了哪些API。
不使用新浪彈出的授權(quán)頁(yè)面,而是直接模擬用戶(hù)登錄時(shí)POST的數(shù)據(jù),直接獲取Code,然后調(diào)用OAuth2的API換取Token。同時(shí)還公開(kāi)用于取消授權(quán)的RevokeOAuth2Async。
1、獲取最近的公共微博列表,
2、獲取好友/關(guān)注人/自己的最新微博列表。
3、獲取與當(dāng)前登錄用戶(hù)相互關(guān)注的用戶(hù)的最新微博列表。
4、獲取某條微博的轉(zhuǎn)發(fā)列表。
5、獲取@當(dāng)前用戶(hù) 的微博列表。
6、根據(jù)微博ID獲取單條微博的詳細(xì)信息。
7、批量獲取微博的轉(zhuǎn)發(fā)數(shù)和評(píng)論數(shù)。
8、微博ID和MID的相互獲取。
9、獲取微博官方表情列表。
10、轉(zhuǎn)發(fā)微博。
11、發(fā)表微博 / 發(fā)表帶圖片的微博。
12、刪除微博。
1、獲取某條微博的評(píng)論列表。
2、獲取當(dāng)前登錄用戶(hù)所發(fā)表的評(píng)論列表。
3、獲取當(dāng)前用戶(hù)接收到的評(píng)論列表。
4、獲取@當(dāng)前用戶(hù)的評(píng)論列表。
5、批量獲取評(píng)論列表。
6、發(fā)表評(píng)論。
7、刪除指定評(píng)論。
8、批量刪除評(píng)論。
9、回復(fù)某條評(píng)論。
1、獲取當(dāng)前用戶(hù)的隱私設(shè)置信息。是設(shè)置項(xiàng),不是獲取隱私,別想歪了。
2、獲取學(xué)校列表,這個(gè)好像沒(méi)什么用。
1、獲取當(dāng)前登錄用戶(hù)的關(guān)注列表。
2、獲取兩個(gè)用戶(hù)之間的共同關(guān)注人列表。比如我關(guān)注了A,而C也關(guān)注了A,因而我和C的共同關(guān)注人就是A。
3、獲取用戶(hù)的相互關(guān)注人列表。也就是有哪些人跟我互粉。
4、獲取當(dāng)前用戶(hù)的粉絲列表。即哪些人關(guān)注了我。
5、獲取用戶(hù)的活躍粉絲列表。這個(gè)不知道干什么的,反正我測(cè)試的時(shí)候返回空。
6、獲取當(dāng)前登錄用戶(hù)關(guān)注列表中同時(shí)關(guān)注了某用戶(hù)的列表。比如,我關(guān)注了A,獲取我關(guān)注的用戶(hù)中也關(guān)注了A的用戶(hù)列表。
7、關(guān)注一位用戶(hù)。
8、取消關(guān)注某位用戶(hù)。
1、根據(jù)用戶(hù)ID獲取用戶(hù)信息。
2、批量獲取用戶(hù)的關(guān)注數(shù)、粉絲數(shù)、微博數(shù)。
3、通過(guò)個(gè)性域名獲取用戶(hù)信息以及最新發(fā)表的一條微博。
1、長(zhǎng)鏈接轉(zhuǎn)為短鏈接。
2、短鏈接轉(zhuǎn)為長(zhǎng)鏈接。
1、根據(jù)IP地址返回地理信息。
2、根據(jù)具體地址返回地理坐標(biāo)信息。
3、根據(jù)地理坐標(biāo)(經(jīng)度,緯度)返回地址信息。
這個(gè)SDK不算很完美,由于新浪的程序員比較菜,有些JSON數(shù)據(jù)結(jié)構(gòu)相當(dāng)不合理,暫時(shí)無(wú)法封裝,等到哪天我想到解決方法后再補(bǔ)充。
|
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注