摘要:現在的微博即便在不登錄的狀態下,依然可以看到很多微博信息流,而我們的落腳點就在這里。本文詳細介紹如何獲取相關的并重新封裝達到免登錄的目的,以支持微博上的各項數據抓取任務。
一、微博一定要登錄才能抓取?本文源地址:http://www.fullstackyang.com/...,轉發請注明該地址或segmentfault地址,謝謝!
目前,對于微博的爬蟲,大部分是基于模擬微博賬號登錄的方式實現的,這種方式如果真的運營起來,實際上是一件非常頭疼痛苦的事,你可能每天都過得提心吊膽,生怕新浪爸爸把你的那些賬號給封了,而且現在隨著實名制的落地,獲得賬號的渠道估計也會變得越來越少。
但是日子還得繼續,在如此艱難的條件下,為了生存爬蟲們必須尋求進化。好在上帝關門的同時會隨手開窗,微博在其他諸如頭條,一點等這類新媒體平臺的沖擊之下,逐步放開了信息流的查看權限。現在的微博即便在不登錄的狀態下,依然可以看到很多微博信息流,而我們的落腳點就在這里。
本文詳細介紹如何獲取相關的Cookie并重新封裝Httpclient達到免登錄的目的,以支持微博上的各項數據抓取任務。下面就從微博首頁http://weibo.com開始。
二、準備工作準備工作很簡單,一個現代瀏覽器(你知道我為什么會寫”現代”兩個字),以及httpclient(我用的版本是4.5.3)
跟登錄爬蟲一樣,免登錄爬蟲也是需要裝載Cookie。這里的Cookie是用來標明游客身份,利用這個Cookie就可以在微博平臺中訪問那些允許訪問的內容了。
這里我們可以使用瀏覽器的network工具來看一下,請求http://weibo.com之后服務器都返回哪些東西,當然事先清空一下瀏覽器的緩存。
不出意外,應該可以看到下圖中的內容
第1次請求weibo.com的時候,其狀態為302重定向,也就是說這時并沒有真正地開始加載頁面,而最后一個請求weibo.com的狀態為200,表示了請求成功,對比兩次請求的header:
明顯地,中間的這些過程給客戶端加載了各種Cookie,從而使得可以順利訪問頁面,接下來我們逐個進行分析。
三、抽絲剝繭第2個請求是https://passport.weibo.com/vi...……,各位可以把這個url復制出來,用httpclient多帶帶訪問一下這個url,可以看到返回的是一個html頁面,里面有一大段Javascript腳本,另外頭部還引用一個JS文件mini_original.js,也就是第3個請求。腳本的功能比較多,就不一一敘述了,簡單來說就是微博訪問的入口控制,而值得我們注意的是其中的一個function:
// 為用戶賦予訪客身份 。 var incarnate = function (tid, where, conficence) { var gen_conf = ""; var from = "weibo"; var incarnate_intr = window.location.protocol + "http://" + window.location.host + "/visitor/visitor?a=incarnate&t=" + encodeURIComponent(tid) + "&w=" + encodeURIComponent(where) + "&c=" + encodeURIComponent(conficence) + "&gc=" + encodeURIComponent(gen_conf) + "&cb=cross_domain&from=" + from + "&_rand=" + Math.random(); url.l(incarnate_intr); };
這里是為請求者賦予一個訪客身份,而控制跳轉的鏈接也是由一些參數拼接起來的,也就是上圖中第6個請求。所以下面的工作就是獲得這3個參數:tid,w(where),c(conficence,從下文來看應為confidence,大概是新浪工程師的手誤)。繼續閱讀源碼,可以看到該function是tid.get方法的回調函數,而這個tid則是定義在那個mini_original.js中的一個對象,其部分源碼為:
var tid = { key: "tid", value: "", recover: 0, confidence: "", postInterface: postUrl, fpCollectInterface: sendUrl, callbackStack: [], init: function () { tid.get(); }, runstack: function () { var f; while (f = tid.callbackStack.pop()) { f(tid.value, tid.recover, tid.confidence);//注意這里,對應上述的3個參數 } }, get: function (callback) { callback = callback || function () { }; tid.callbackStack.push(callback); if (tid.value) { return tid.runstack(); } Store.DB.get(tid.key, function (v) { if (!v) { tid.getTidFromServer(); } else { …… } }); }, …… } …… getTidFromServer: function () { tid.getTidFromServer = function () { }; if (window.use_fp) { getFp(function (data) { util.postData(window.location.protocol + "http://" + window.location.host + "/" + tid.postInterface, "cb=gen_callback&fp=" + encodeURIComponent(data), function (res) { if (res) { eval(res); } }); }); } else { util.postData(window.location.protocol + "http://" + window.location.host + "/" + tid.postInterface, "cb=gen_callback", function (res) { if (res) { eval(res); } }); } }, …… //獲得參數 window.gen_callback = function (fp) { var value = false, confidence; if (fp) { if (fp.retcode == 20000000) { confidence = typeof(fp.data.confidence) != "undefined" ? "000" + fp.data.confidence : "100"; tid.recover = fp.data.new_tid ? 3 : 2; tid.confidence = confidence = confidence.substring(confidence.length - 3); value = fp.data.tid; Store.DB.set(tid.key, value + "__" + confidence); } } tid.value = value; tid.runstack(); };
顯然,tid.runstack()是真正執行回調函數的地方,這里就能看到傳入的3個參數。在get方法中,當cookie為空時,tid會調用getTidFromServer,這時就產生了第5個請求https://passport.weibo.com/vi...,它需要兩個參數cb和fp,其參數值可以作為常量:
該請求的結果返回一串json
{ "msg": "succ", "data": { "new_tid": false, "confidence": 95, "tid": "kIRvLolhrCR5iSCc80tWqDYmwBvlRVlnY2+yvCQ1VVA=" }, "retcode": 20000000 }
其中就包含了tid和confidence,這個confidence,我猜大概是推測客戶端是否真實的一個置信度,不一定出現,根據window.gen_callback方法,不出現時默認為100,另外當new_tid為真時參數where等于3,否則等于2。
此時3個參數已經全部獲得,現在就可以用httpclient發起上面第6個請求,返回得到另一串json:
{ "msg": "succ", "data": { "sub": "_2AkMu428tf8NxqwJRmPAcxWzmZYh_zQjEieKYv572JRMxHRl-yT83qnMGtRCnhyR4ezQQZQrBRO3gVMwM5ZB2hQ..", "subp": "0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWU2MgYnITksS2awP.AX-DQ" }, "retcode": 20000000 }
參考最后請求weibo.com的header,這里的sub和subp就是最終要獲取的cookie值。大家或許有一個小疑問,第一個Cookie怎么來的,沒用嗎?是的,這個Cookie是第一次訪問weibo.com產生的,經過測試可以不用裝載。
最后我們用上面兩個Cookie裝載到HttpClient中請求一次weibo.com,就可以獲得完整的html頁面了,下面就是見證奇跡的時刻:
微博-隨時隨地發現新鮮事 ……
如果之前有微博爬蟲開發經驗的小伙伴,看到這里,一定能想出來很多玩法了吧。
四、代碼實現下面附上我的源碼,通過上面的詳細介紹,應該已經比較好理解,因此這里就簡單地說明一下:
我把Cookie獲取的過程做成了一個靜態內部類,其中需要發起2次請求,一次是genvisitor獲得3個參數,另一次是incarnate獲得Cookie值;
如果Cookie獲取失敗,會調用HttpClientInstance.changeProxy來改變代理IP,然后重新獲取,直到獲取成功為止;
在使用時,出現了IP被封或無法正常獲取頁面等異常情況,外部可以通過調用cookieReset方法,重新獲取一個新的Cookie。這里還是要聲明一下,科學地使用爬蟲,維護世界和平是程序員的基本素養;
雖然加了一些鎖的控制,但是還未在高并發場景實測過,不能保證百分百線程安全,如使用下面的代碼,請根據需要自行修改,如有問題也請大神們及時指出,拜謝!
HttpClientInstance是我用單例模式重新封裝的httpclient,對于每個傳進來的請求重新包裝了一層RequestConfig,并且使用了代理IP;
不是所有的微博頁面都可以抓取得到,但是博文,評論,轉發等基本的數據還是沒有問題的;
后續我也會把代碼push到github上,請大家支持,謝謝!
import com.fullstackyang.httpclient.HttpClientInstance; import com.fullstackyang.httpclient.HttpRequestUtils; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.common.net.HttpHeaders; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 微博免登陸請求客戶端 * * @author fullstackyang */ @Slf4j public class WeiboClient { private static CookieFetcher cookieFetcher = new CookieFetcher(); private volatile String cookie; public WeiboClient() { this.cookie = cookieFetcher.getCookie(); } private static Lock lock = new ReentrantLock(); public void cookieReset() { if (lock.tryLock()) { try { HttpClientInstance.instance().changeProxy(); this.cookie = cookieFetcher.getCookie(); log.info("cookie :" + cookie); } finally { lock.unlock(); } } } /** * get方法,獲取微博平臺的其他頁面 * @param url * @return */ public String get(String url) { if (Strings.isNullOrEmpty(url)) return ""; while (true) { HttpGet httpGet = new HttpGet(url); httpGet.addHeader(HttpHeaders.COOKIE, cookie); httpGet.addHeader(HttpHeaders.HOST, "weibo.com"); httpGet.addHeader("Upgrade-Insecure-Requests", "1"); httpGet.setConfig(RequestConfig.custom().setSocketTimeout(3000) .setConnectTimeout(3000).setConnectionRequestTimeout(3000).build()); String html = HttpClientInstance.instance().tryExecute(httpGet, null, null); if (html == null) cookieReset(); else return html; } } /** * 獲取訪問微博時必需的Cookie */ @NoArgsConstructor static class CookieFetcher { static final String PASSPORT_URL = "https://passport.weibo.com/visitor/visitor?entry=miniblog&a=enter&url=http://weibo.com/?category=2" + "&domain=.weibo.com&ua=php-sso_sdk_client-0.6.23"; static final String GEN_VISITOR_URL = "https://passport.weibo.com/visitor/genvisitor"; static final String VISITOR_URL = "https://passport.weibo.com/visitor/visitor?a=incarnate"; private String getCookie() { Mapmap; while (true) { map = getCookieParam(); if (map.containsKey("SUB") && map.containsKey("SUBP") && StringUtils.isNoneEmpty(map.get("SUB"), map.get("SUBP"))) break; HttpClientInstance.instance().changeProxy(); } return " YF-Page-G0=" + "; _s_tentry=-; SUB=" + map.get("SUB") + "; SUBP=" + map.get("SUBP"); } private Map getCookieParam() { String time = System.currentTimeMillis() + ""; time = time.substring(0, 9) + "." + time.substring(9, 13); String passporturl = PASSPORT_URL + "&_rand=" + time; String tid = ""; String c = ""; String w = ""; { String str = postGenvisitor(passporturl); if (str.contains(""retcode":20000000")) { JSONObject jsonObject = new JSONObject(str).getJSONObject("data"); tid = jsonObject.optString("tid"); try { tid = URLEncoder.encode(tid, "utf-8"); } catch (UnsupportedEncodingException e) { } c = jsonObject.has("confidence") ? "000" + jsonObject.getInt("confidence") : "100"; w = jsonObject.optBoolean("new_tid") ? "3" : "2"; } } String s = ""; String sp = ""; { if (StringUtils.isNoneEmpty(tid, w, c)) { String str = getVisitor(tid, w, c, passporturl); str = str.substring(str.indexOf("(") + 1, str.indexOf(")")); if (str.contains(""retcode":20000000")) { System.out.println(new JSONObject(str).toString(2)); JSONObject jsonObject = new JSONObject(str).getJSONObject("data"); s = jsonObject.getString("sub"); sp = jsonObject.getString("subp"); } } } Map map = Maps.newHashMap(); map.put("SUB", s); map.put("SUBP", sp); return map; } private String postGenvisitor(String passporturl) { Map headers = Maps.newHashMap(); headers.put(HttpHeaders.ACCEPT, "*/*"); headers.put(HttpHeaders.ORIGIN, "https://passport.weibo.com"); headers.put(HttpHeaders.REFERER, passporturl); Map params = Maps.newHashMap(); params.put("cb", "gen_callback"); params.put("fp", fp()); HttpPost httpPost = HttpRequestUtils.createHttpPost(GEN_VISITOR_URL, headers, params); String str = HttpClientInstance.instance().execute(httpPost, null); return str.substring(str.indexOf("(") + 1, str.lastIndexOf("")); } private String getVisitor(String tid, String w, String c, String passporturl) { String url = VISITOR_URL + "&t=" + tid + "&w=" + "&c=" + c.substring(c.length() - 3) + "&gc=&cb=cross_domain&from=weibo&_rand=0." + rand(); Map headers = Maps.newHashMap(); headers.put(HttpHeaders.ACCEPT, "*/*"); headers.put(HttpHeaders.HOST, "passport.weibo.com"); headers.put(HttpHeaders.COOKIE, "tid=" + tid + "__0" + c); headers.put(HttpHeaders.REFERER, passporturl); HttpGet httpGet = HttpRequestUtils.createHttpGet(url, headers); httpGet.setConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()); return HttpClientInstance.instance().execute(httpGet, null); } private static String rand() { return new BigDecimal(Math.floor(Math.random() * 10000000000000000L)).toString(); } private static String fp() { JSONObject jsonObject = new JSONObject(); jsonObject.put("os", "1"); jsonObject.put("browser", "Chrome59,0,3071,115"); jsonObject.put("fonts", "undefined"); jsonObject.put("screenInfo", "1680*1050*24"); jsonObject.put("plugins", "Enables Widevine licenses for playback of HTML audio/video content. (version: 1.4.8.984)::widevinecdmadapter.dll::Widevine Content Decryption Module|Shockwave Flash 26.0 r0::pepflashplayer.dll::Shockwave Flash|::mhjfbmdgcfjbbpaeojofohoefgiehjai::Chrome PDF Viewer|::internal-nacl-plugin::Native Client|Portable Document Format::internal-pdf-viewer::Chrome PDF Viewer"); return jsonObject.toString(); } } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70483.html
摘要:時間永遠都過得那么快,一晃從年注冊,到現在已經過去了年那些被我藏在收藏夾吃灰的文章,已經太多了,是時候把他們整理一下了。那是因為收藏夾太亂,橡皮擦給設置私密了,不收拾不好看呀。 ...
摘要:譯年你不能錯過的類庫后端掘金各位讀者好,這篇文章是在我看過的一篇介紹文后,整理出來的。上線后平穩運行我的后端書架后端掘金我的后端書架月前本書架主要針對后端開發與架構。 【譯】2017 年你不能錯過的 Java 類庫 - 后端 - 掘金各位讀者好, 這篇文章是在我看過 Andres Almiray 的一篇介紹文后,整理出來的。 因為內容非常好,我便將它整理成參考列表分享給大家, 同時附上...
摘要:讓你收獲滿滿碼個蛋從年月日推送第篇文章一年過去了已累積推文近篇文章,本文為年度精選,共計篇,按照類別整理便于讀者主題閱讀。本篇文章是今年的最后一篇技術文章,為了讓大家在家也能好好學習,特此花了幾個小時整理了這些文章。 showImg(https://segmentfault.com/img/remote/1460000013241596); 讓你收獲滿滿! 碼個蛋從2017年02月20...
摘要:摘要今年的先知白帽大會,與會者將能夠親身感受到非常多有趣的技術議題,如在國際賽事中屢奪佳績的團隊,其隊長將親臨現場,分享穿針引線般的漏洞利用藝術。從數據視角探索安全威脅阿里云安全工程師議題解讀本議題討論了數據為安全人員思維方式帶來的變化。 摘要: 今年的先知白帽大會,與會者將能夠親身感受到非常多有趣的技術議題,如HITCON在國際賽事中屢奪佳績的CTF團隊,其隊長Orange將親臨現場...
閱讀 1618·2021-09-08 10:42
閱讀 3604·2021-08-11 10:23
閱讀 3959·2019-08-30 14:10
閱讀 2732·2019-08-29 17:29
閱讀 3090·2019-08-29 12:50
閱讀 637·2019-08-26 13:36
閱讀 3456·2019-08-26 11:59
閱讀 1487·2019-08-23 16:23