花了幾天時間,消耗了九牛六虎之力,新浪微博大部分API已經封裝,但有部分API實在太難封裝。
說起這封裝,我必須嚴重地、從人品和技術層面鄙視一下新浪的程序員,實在太菜了。估計菜鳥都被大企業吸收了,菜到連面向對象都不懂。建議新浪的菜菜們向淘寶學習下,人家淘寶還同時有XML和JSON兩種數據格式。
同樣的內容,返回的JSON對象居然會出現不同結構,更可惡的,像公共API中獲取城市列表,國家區域代碼列表的返回結果,實在讓人不得不發笑。那些JSON用JS讀起來都困難,更何況要進行封裝呢。根本沒法封裝,因此在論壇上抱怨的人不少,可是新浪官方呢,置之不管,就當沒看見一樣,看來,大企業就是這點水平。
題外話不多說,回歸主題。除了權限不夠和無法封裝的API外,其余的API都封裝了。先上一張代碼地圖。
這圖有點像蜘蛛網,非常有美感,可惜是靜止的,如果會動的,一定很好看。
我不可能把代碼一行一行地向大家講述,因為這樣做會相當惡心。現在菜菜們都整天拿開源來裝逼,所以,為了迎合廣大菜菜所說的所謂“趨勢”,我也決定把我這份亂七八糟的代碼向全人類公開。
下載地址:http://vdisk.weibo.com/s/z7iFc2gCCwC1b
接下來,就給大家說說原理和思路,這才是編程的關鍵。
1、不管是微博API還是其他的開放平台的API,都有N多共同點。首先,用戶要用自己的帳號登錄,然后由用戶決定是否授權給我們的應用程序,如果用戶已同意授權,我們會得到一個授權碼(Code)。
2、拿到Code后,我們要用這個Code來換取一個Access Token,就相當於用戶授予我們一把鑰匙,我們再用這把鑰匙去開啟庫房的門,然后取出一個臨時令牌,就好比在漢代,大臣代表天子巡視諸侯國時,手里要持着天子的符節一樣。有了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] public class UserInfo { /// <summary> /// 用戶UID /// </summary> [DataMember(Name = "id")] public long ID { get; set; } /// <summary> /// 字符串型的用戶UID /// </summary> [DataMember(Name = "idstr")] public string IDStr { get; set; } /// <summary> /// 用戶昵稱 /// </summary> [DataMember(Name = "screen_name")] public string ScreenName { get; set; } /// <summary> /// 友好顯示名稱 /// </summary> [DataMember(Name = "name")] public string Name { get; set; } /// <summary> /// 用戶所在省級ID /// </summary> [DataMember(Name = "province")] public int Province { get; set; } /// <summary> /// 用戶所在城市ID /// </summary> [DataMember(Name = "city")] public int City { get; set; } /// <summary> /// 用戶所在地 /// </summary> [DataMember(Name = "location")] public string Location { get; set; } /// <summary> /// 用戶個人描述 /// </summary> [DataMember(Name = "description")] public string Description { get; set; } /// <summary> /// 用戶博客地址 /// </summary> [DataMember(Name = "url")] public string Url { get; set; } /// <summary> /// 用戶頭像地址(中圖),50×50像素 /// </summary> [DataMember(Name = "profile_image_url")] public string ProfileImageUrl { get; set; } /// <summary> /// 用戶的微博統一URL地址 /// </summary> [DataMember(Name = "profile_url")] public string ProfileUrl { get; set; } /// <summary> /// 用戶的個性化域名 /// </summary> [DataMember(Name = "domain")] public string Domain { get; set; } /// <summary> /// 用戶的微號 /// </summary> [DataMember(Name = "weihao")] public string WeiHao { get; set; } /// <summary> /// 性別,m:男、f:女、n:未知 /// </summary> [DataMember(Name = "gender")] public string Gender { get; set; } /// <summary> /// 粉絲數 /// </summary> [DataMember(Name = "followers_count")] public int FollowersCount { get; set; } /// <summary> /// 關注數 /// </summary> [DataMember(Name = "friends_count")] public int FriendsCount { get; set; } /// <summary> /// 微博數 /// </summary> [DataMember(Name = "statuses_count")] public int StatusesCount { get; set; } /// <summary> /// 收藏數 /// </summary> [DataMember(Name = "favourites_count")] public int FavouritesCount { get; set; } /// <summary> /// 用戶創建(注冊)時間 /// </summary> [DataMember(Name = "created_at")] public string CreatedAt { get; set; } /// <summary> /// 暫未支持 /// </summary> [DataMember(Name = "following")] public bool Following { get; set; } /// <summary> /// 是否允許所有人給我發私信,true:是,false:否 /// </summary> [DataMember(Name = "allow_all_act_msg")] public bool AllowAllActMsg { get; set; } /// <summary> /// 是否允許標識用戶的地理位置,true:是,false:否 /// </summary> [DataMember(Name = "geo_enabled")] public bool GeoEnabled { get; set; } /// <summary> /// 是否是微博認證用戶,即加V用戶,true:是,false:否 /// </summary> [DataMember(Name = "verified")] public bool Verified { get; set; } /// <summary> /// 暫未支持 /// </summary> [DataMember(Name = "verified_type")] public int VerifiedType { get; set; } /// <summary> /// 用戶備注信息 /// </summary> [DataMember(Name = "remark")] public string Remark { get; set; } /// <summary> /// 用戶的最近一條微博信息 /// </summary> [DataMember(Name = "status")] public Status Status { get; set; } /// <summary> /// 當對象數據未返回完整微博信息時,將填充該字段 /// </summary> [DataMember(Name = "status_id")] public long StatusID { get; set; } /// <summary> /// 是否允許所有人對我的微博進行評論,true:是,false:否 /// </summary> [DataMember(Name = "allow_all_comment")] public bool AllowAllComment { get; set; } /// <summary> /// 用戶頭像地址(大圖),180×180像素 /// </summary> [DataMember(Name = "avatar_large")] public string AvatarLarge { get; set; } /// <summary> /// 用戶頭像地址(高清),高清頭像原圖 /// </summary> [DataMember(Name = "avatar_hd")] public string AvatarHd { get; set; } /// <summary> /// 認證原因 /// </summary> [DataMember(Name = "verified_reason")] public string VerifiedReason { get; set; } /// <summary> /// 該用戶是否關注當前登錄用戶,true:是,false:否 /// </summary> [DataMember(Name = "follow_me")] public bool FollowMe { get; set; } /// <summary> /// 用戶的在線狀態,0:不在線、1:在線 /// </summary> [DataMember(Name = "online_status")] public int OnlineStatus { get; set; } /// <summary> /// 用戶的互粉數 /// </summary> [DataMember(Name = "bi_followers_count")] public int BiFollowersCount { get; set; } /// <summary> /// 用戶當前的語言版本,zh-cn:簡體中文,zh-tw:繁體中文,en:英語 /// </summary> [DataMember(Name = "lang")] public string Lang { 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] public class Status { /// <summary> /// 微博創建時間 /// </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> /// 微博信息內容 /// </summary> [DataMember(Name = "text")] public string Text { get; set; } /// <summary> /// 微博來源 /// </summary> [DataMember(Name = "source")] public string Source { get; set; } /// <summary> /// 是否已收藏,true:是,false:否 /// </summary> [DataMember(Name = "favorited")] public bool Favorited { get; set; } /// <summary> /// 是否被截斷,true:是,false:否 /// </summary> [DataMember(Name = "truncated")] public bool Truncated { get; set; } /// <summary> /// (暫未支持)回復ID /// </summary> [DataMember(Name = "in_reply_to_status_id")] public string InReplyToStatusID { get; set; } /// <summary> /// (暫未支持)回復人UID /// </summary> [DataMember(Name = "in_reply_to_user_id")] public string InReplyToUserID { get; set; } /// <summary> /// (暫未支持)回復人昵稱 /// </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> /// 微博作者的用戶信息 /// </summary> [DataMember(Name = "user")] public UserInfo User { get; set; } /// <summary> /// 被轉發的原微博信息字段,當該微博為轉發微博時返回 /// </summary> [DataMember(Name = "retweeted_status")] public Status RetweetedStatus { get; set; } /// <summary> /// 轉發數 /// </summary> [DataMember(Name = "reposts_count")] public int RepostsCount { get; set; } /// <summary> /// 評論數 /// </summary> [DataMember(Name = "comments_count")] public int CommentsCount { get; set; } /// <summary> /// 表態數 /// </summary> [DataMember(Name = "attitudes_count")] public int AttitudesCount { get; set; } /// <summary> /// 暫未支持 /// </summary> [DataMember(Name = "mlevel")] public int Mlevel { get; set; } /// <summary> /// 微博的可見性及指定可見分組信息。該object中type取值,0:普通微博,1:私密微博,3:指定分組微博,4:密友微博;list_id為分組的組號 /// </summary> [DataMember(Name = "visible")] public dynamic Visible { get; set; } /// <summary> /// 微博配圖地址。多圖時返回多圖鏈接。無配圖返回“[]” /// </summary> [DataMember(Name = "pic_urls")] public dynamic PicUrls { get; set; } /// <summary> /// 微博流內的推廣微博ID /// </summary> [DataMember(Name = "ad")] public dynamic Ad { get; set; } /// <summary> /// 當不返回user字段時,此屬性可填充UID /// </summary> [DataMember(Name = "uid")] public long Uid { get; set; } }
我們在接收到服務器的回應后,直接對數據進行反序列化,就可以得到這些數據的封裝實例,面向對象,操作起來更方便。這些被封裝的基本類型,我都放到了Models目錄下。為了實現反序列化,我定義了一個類,公開一個靜態方法,以便從JSON數據生成對象實例。
public class JsonSerializeHelper { public static T ReadDataFromJson<T>(Stream inStream) { DataContractJsonSerializer s = new DataContractJsonSerializer(typeof(T)); return (T)s.ReadObject(inStream); } }
因為我們要反序列化的類型是多種多樣的,無法定論,因此,這里使用泛型參數較合適。具體反序列化為哪種類型的實例,在運行時決定。
由於每個API的調用都是一輪HTTP請求/回應,無論你調用哪個API都一樣,這個行為是通過的,所以,我就統一進行提取,並使用.NET 4.5新增的HttpClient類來處理,這個類很強大,把一些不太好處理的HTTP請求都為我們封裝好了,尤其是像form-data這種POST的數據,如MultiPart form data這些,尤其是向服務器上傳文件時較容易處理,省去許多不必要的機械性工作,將人類從無必要的代碼中解放出來,極大地提高生產率,這是.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; }
上面僅僅列舉了一兩個方法,詳細的代碼大家可以下載源代碼看。
在我發布的解決方案中,有一個項目名為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數據結構相當不合理,暫時無法封裝,等到哪天我想到解決方法后再補充。