微博爬蟲“免登錄”技巧詳解及Java實現(xiàn)
一、微博一定要登錄才能抓取?
目前,對于微博的爬蟲,大部分是基于模擬微博賬號登錄的方式實現(xiàn)的,這種方式如果真的運營起來,實際上是一件非常頭疼痛苦的事,你可能每天都過得提心吊膽,生怕新浪爸爸把你的那些賬號給封了,而且現(xiàn)在隨著實名制的落地,獲得賬號的渠道估計也會變得越來越少。
但是日子還得繼續(xù),在如此艱難的條件下,為了生存爬蟲們必須尋求進化。好在上帝關門的同時會隨手開窗,微博在其他諸如頭條,一點等這類新媒體平臺的沖擊之下,逐步放開了信息流的查看權限?,F(xiàn)在的微博即便在不登錄的狀態(tài)下,依然可以看到很多微博信息流,而我們的落腳點就在這里。
本文詳細介紹如何獲取相關的Cookie并重新封裝Httpclient達到免登錄的目的,以支持微博上的各項數(shù)據(jù)抓取任務。下面就從微博首頁http://weibo.com開始。
二、準備工作
準備工作很簡單,一個現(xiàn)代瀏覽器(你知道我為什么會寫”現(xiàn)代”兩個字),以及httpclient(我用的版本是4.5.3)
跟登錄爬蟲一樣,免登錄爬蟲也是需要裝載Cookie。這里的Cookie是用來標明游客身份,利用這個Cookie就可以在微博平臺中訪問那些允許訪問的內(nèi)容了。
這里我們可以使用瀏覽器的network工具來看一下,請求http://weibo.com之后服務器都返回哪些東西,當然事先清空一下瀏覽器的緩存。
不出意外,應該可以看到下圖中的內(nèi)容
第1次請求weibo.com的時候,其狀態(tài)為302重定向,也就是說這時并沒有真正地開始加載頁面,而最后一個請求weibo.com的狀態(tài)為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 + "//" + 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);
- };
這里是為請求者賦予一個訪客身份,而控制跳轉的鏈接也是由一些參數(shù)拼接起來的,也就是上圖中第6個請求。所以下面的工作就是獲得這3個參數(shù):tid,w(where),c(conficence,從下文來看應為confidence,大概是新浪工程師的手誤)。繼續(xù)閱讀源碼,可以看到該function是tid.get方法的回調(diào)函數(shù),而這個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個參數(shù)
- }
- },
- 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 + '//' + window.location.host + '/' + tid.postInterface, "cb=gen_callback&fp=" + encodeURIComponent(data), function (res) {
- if (res) {
- eval(res);
- }
- });
- });
- } else {
- util.postData(window.location.protocol + '//' + window.location.host + '/' + tid.postInterface, "cb=gen_callback", function (res) {
- if (res) {
- eval(res);
- }
- });
- }
- },
- ……
- //獲得參數(shù)
- 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()是真正執(zhí)行回調(diào)函數(shù)的地方,這里就能看到傳入的3個參數(shù)。在get方法中,當cookie為空時,tid會調(diào)用getTidFromServer,這時就產(chǎn)生了第5個請求https://passport.weibo.com/vi...,它需要兩個參數(shù)cb和fp,其參數(shù)值可以作為常量:
該請求的結果返回一串json
- {
- "msg": "succ",
- "data": {
- "new_tid": false,
- "confidence": 95,
- "tid": "kIRvLolhrCR5iSCc80tWqDYmwBvlRVlnY2+yvCQ1VVA="
- },
- "retcode": 20000000
- }
其中就包含了tid和confidence,這個confidence,我猜大概是推測客戶端是否真實的一個置信度,不一定出現(xiàn),根據(jù)window.gen_callback方法,不出現(xiàn)時默認為100,另外當new_tid為真時參數(shù)where等于3,否則等于2。
此時3個參數(shù)已經(jīng)全部獲得,現(xiàn)在就可以用httpclient發(fā)起上面第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產(chǎn)生的,經(jīng)過測試可以不用裝載。
最后我們用上面兩個Cookie裝載到HttpClient中請求一次weibo.com,就可以獲得完整的html頁面了,下面就是見證奇跡的時刻:
- <!doctype html>
- <html>
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
- <meta name="viewport" content="initial-scale=1,minimum-scale=1" />
- <meta content="隨時隨地發(fā)現(xiàn)新鮮事!微博帶你欣賞世界上每一個精彩瞬間,了解每一個幕后故事。分享你想表達的,讓全世界都能聽到你的心聲!" name="description" />
- <link rel="mask-icon" sizes="any" href="//img.t.sinajs.cn/t6/style/images/apple/wbfont.svg" color="black" />
- <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
- <script type="text/javascript">
- try{document.execCommand("BackgroundImageCache", false, true);}catch(e){}
- </script>
- <title>微博-隨時隨地發(fā)現(xiàn)新鮮事</title>
- <link href="//img.t.sinajs.cn/t6/style/css/module/base/frame.css?version=6c9bf6ab3b33391f" type="text/css" rel="stylesheet" charset="utf-8" />
- <link href="//img.t.sinajs.cn/t6/style/css/pages/growth/login_v5.css?version=6c9bf6ab3b33391f" type="text/css" rel="stylesheet" charset="utf-8">
- <link href="//img.t.sinajs.cn/t6/skin/default/skin.css?version=6c9bf6ab3b33391f" type="text/css" rel="stylesheet" id="skin_style" />
- <script type="text/javascript">
- var $CONFIG = {};
- $CONFIG['islogin'] = '0';
- $CONFIG['version'] = '6c9bf6ab3b33391f';
- $CONFIG['timeDiff'] = (new Date() - 1505746970000);
- $CONFIG['lang'] = 'zh-cn';
- $CONFIG['jsPath'] = '//js.t.sinajs.cn/t5/';
- $CONFIG['cssPath'] = '//img.t.sinajs.cn/t5/';
- $CONFIG['imgPath'] = '//img.t.sinajs.cn/t5/';
- $CONFIG['servertime'] = 1505746970;
- $CONFIG['location']='login';
- $CONFIG['bigpipe']='false';
- $CONFIG['bpType']='login';
- $CONFIG['mJsPath'] = ['//js{n}.t.sinajs.cn/t5/', 1, 2];
- $CONFIG['mCssPath'] = ['//img{n}.t.sinajs.cn/t5/', 1, 2];
- $CONFIG['redirect'] = '';
- $CONFIG['vid']='1008997495870';
- </script>
- <style>#js_style_css_module_global_WB_outframe{height:42px;}</style>
- </head>
- ……
如果之前有微博爬蟲開發(fā)經(jīng)驗的小伙伴,看到這里,一定能想出來很多玩法了吧。
四、代碼實現(xiàn)
下面附上我的源碼,通過上面的詳細介紹,應該已經(jīng)比較好理解,因此這里就簡單地說明一下:
- 我把Cookie獲取的過程做成了一個靜態(tài)內(nèi)部類,其中需要發(fā)起2次請求,一次是genvisitor獲得3個參數(shù),另一次是incarnate獲得Cookie值;
- 如果Cookie獲取失敗,會調(diào)用HttpClientInstance.changeProxy來改變代理IP,然后重新獲取,直到獲取成功為止;
- 在使用時,出現(xiàn)了IP被封或無法正常獲取頁面等異常情況,外部可以通過調(diào)用cookieReset方法,重新獲取一個新的Cookie。這里還是要聲明一下,科學地使用爬蟲,維護世界和平是程序員的基本素養(yǎng);
- 雖然加了一些鎖的控制,但是還未在高并發(fā)場景實測過,不能保證百分百線程安全,如使用下面的代碼,請根據(jù)需要自行修改,如有問題也請大神們及時指出,拜謝!
- HttpClientInstance是我用單例模式重新封裝的httpclient,對于每個傳進來的請求重新包裝了一層RequestConfig,并且使用了代理IP;
- 不是所有的微博頁面都可以抓取得到,但是博文,評論,轉發(fā)等基本的數(shù)據(jù)還是沒有問題的;
- 后續(xù)我也會把代碼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() {
- Map<String, String> map;
- 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<String, String> 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<String, String> map = Maps.newHashMap();
- map.put("SUB", s);
- map.put("SUBP", sp);
- return map;
- }
- private String postGenvisitor(String passporturl) {
- Map<String, String> headers = Maps.newHashMap();
- headers.put(HttpHeaders.ACCEPT, "*/*");
- headers.put(HttpHeaders.ORIGIN, "https://passport.weibo.com");
- headers.put(HttpHeaders.REFERER, passporturl);
- Map<String, String> 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<String, String> 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();
- }
- }
- }