微博爬蟲“免登錄”技巧詳解及Java實現

本文源地址:http://www.fullstackyang.com/archives/434.html,轉發請注明該地址或簡書地址,謝謝!

一、微博一定要登錄才能抓取?

目前,對于微博的爬蟲,大部分是基于模擬微博賬號登錄的方式實現的,這種方式如果真的運營起來,實際上是一件非常頭疼痛苦的事,你可能每天都過得提心吊膽,生怕新浪爸爸把你的那些賬號給封了,而且現在隨著實名制的落地,獲得賬號的渠道估計也會變得越來越少。

但是日子還得繼續,在如此艱難的條件下,為了生存爬蟲們必須尋求進化。好在上帝關門的同時會隨手開窗,微博在其他諸如頭條,一點等這類新媒體平臺的沖擊之下,逐步放開了信息流的查看權限。現在的微博即便在不登錄的狀態下,依然可以看到很多微博信息流,而我們的落腳點就在這里。

本文詳細介紹如何獲取相關的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/visitor……,各位可以把這個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 + '//' + 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);
                    }
                });
            }
        },
……
//獲得參數
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/visitor/genvisitor,它需要兩個參數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頁面了,下面就是見證奇跡的時刻:

<!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="隨時隨地發現新鮮事!微博帶你欣賞世界上每一個精彩瞬間,了解每一個幕后故事。分享你想表達的,讓全世界都能聽到你的心聲!" name="description" />
<link rel="mask-icon" sizes="any"  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>微博-隨時隨地發現新鮮事</title>
<link  type="text/css" rel="stylesheet" charset="utf-8" />
<link  type="text/css" rel="stylesheet" charset="utf-8">
<link  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>
……

如果之前有微博爬蟲開發經驗的小伙伴,看到這里,一定能想出來很多玩法了吧。

四、代碼實現

下面附上我的源碼,通過上面的詳細介紹,應該已經比較好理解,因此這里就簡單地說明一下:

  1. 我把Cookie獲取的過程做成了一個靜態內部類,其中需要發起2次請求,一次是genvisitor獲得3個參數,另一次是incarnate獲得Cookie值;
  2. 如果Cookie獲取失敗,會調用HttpClientInstance.changeProxy來改變代理IP,然后重新獲取,直到獲取成功為止;
  3. 在使用時,出現了IP被封或無法正常獲取頁面等異常情況,外部可以通過調用cookieReset方法,重新獲取一個新的Cookie。這里還是要聲明一下,科學地使用爬蟲,維護世界和平是程序員的基本素養;
  4. 雖然加了一些鎖的控制,但是還未在高并發場景實測過,不能保證百分百線程安全,如使用下面的代碼,請根據需要自行修改,如有問題也請大神們及時指出,拜謝!
  5. HttpClientInstance是我用單例模式重新封裝的httpclient,對于每個傳進來的請求重新包裝了一層RequestConfig,并且使用了代理IP;
  6. 不是所有的微博頁面都可以抓取得到,但是博文,評論,轉發等基本的數據還是沒有問題的;
  7. 后續我也會把代碼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();
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 想獲取某人發的所有微博信息,發現新浪微博的API里存在局限性,不僅需要申請高級接口,而且還要用戶授權才能獲取他發的...
    darrenfantasy閱讀 5,581評論 0 3
  • 33款可用來抓數據的開源爬蟲軟件工具 要玩大數據,沒有數據怎么玩?這里推薦一些33款開源爬蟲軟件給大家。 爬蟲,即...
    visiontry閱讀 7,368評論 1 99
  • 要玩大數據,沒有數據怎么玩?這里推薦一些33款開源爬蟲軟件給大家。 爬蟲,即網絡爬蟲,是一種自動獲取網頁內容的程序...
    評評分分閱讀 8,011評論 2 121
  • 你爬了嗎? 要玩大數據,沒有數據怎么玩?這里推薦一些33款開源爬蟲軟件給大家。 爬蟲,即網絡爬蟲,是一種自動獲取網...
    Albert新榮閱讀 2,239評論 0 8