成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

微博爬蟲“免登錄”技巧詳解及Java實(shí)現(xiàn)

mmy123456 / 2506人閱讀

摘要:現(xiàn)在的微博即便在不登錄的狀態(tài)下,依然可以看到很多微博信息流,而我們的落腳點(diǎn)就在這里。本文詳細(xì)介紹如何獲取相關(guān)的并重新封裝達(dá)到免登錄的目的,以支持微博上的各項(xiàng)數(shù)據(jù)抓取任務(wù)。

本文源地址:http://www.fullstackyang.com/...,轉(zhuǎn)發(fā)請(qǐng)注明該地址或segmentfault地址,謝謝!

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

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

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

本文詳細(xì)介紹如何獲取相關(guān)的Cookie并重新封裝Httpclient達(dá)到免登錄的目的,以支持微博上的各項(xiàng)數(shù)據(jù)抓取任務(wù)。下面就從微博首頁http://weibo.com開始。

二、準(zhǔn)備工作

準(zhǔn)備工作很簡單,一個(gè)現(xiàn)代瀏覽器(你知道我為什么會(huì)寫”現(xiàn)代”兩個(gè)字),以及httpclient(我用的版本是4.5.3)

跟登錄爬蟲一樣,免登錄爬蟲也是需要裝載Cookie。這里的Cookie是用來標(biāo)明游客身份,利用這個(gè)Cookie就可以在微博平臺(tái)中訪問那些允許訪問的內(nèi)容了。

這里我們可以使用瀏覽器的network工具來看一下,請(qǐng)求http://weibo.com之后服務(wù)器都返回哪些東西,當(dāng)然事先清空一下瀏覽器的緩存。

不出意外,應(yīng)該可以看到下圖中的內(nèi)容

第1次請(qǐng)求weibo.com的時(shí)候,其狀態(tài)為302重定向,也就是說這時(shí)并沒有真正地開始加載頁面,而最后一個(gè)請(qǐng)求weibo.com的狀態(tài)為200,表示了請(qǐng)求成功,對(duì)比兩次請(qǐng)求的header:

明顯地,中間的這些過程給客戶端加載了各種Cookie,從而使得可以順利訪問頁面,接下來我們逐個(gè)進(jìn)行分析。

三、抽絲剝繭

第2個(gè)請(qǐng)求是https://passport.weibo.com/vi...……,各位可以把這個(gè)url復(fù)制出來,用httpclient多帶帶訪問一下這個(gè)url,可以看到返回的是一個(gè)html頁面,里面有一大段Javascript腳本,另外頭部還引用一個(gè)JS文件mini_original.js,也就是第3個(gè)請(qǐng)求。腳本的功能比較多,就不一一敘述了,簡單來說就是微博訪問的入口控制,而值得我們注意的是其中的一個(gè)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);
    };

這里是為請(qǐng)求者賦予一個(gè)訪客身份,而控制跳轉(zhuǎn)的鏈接也是由一些參數(shù)拼接起來的,也就是上圖中第6個(gè)請(qǐng)求。所以下面的工作就是獲得這3個(gè)參數(shù):tid,w(where),c(conficence,從下文來看應(yīng)為confidence,大概是新浪工程師的手誤)。繼續(xù)閱讀源碼,可以看到該function是tid.get方法的回調(diào)函數(shù),而這個(gè)tid則是定義在那個(gè)mini_original.js中的一個(gè)對(duì)象,其部分源碼為:

    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);//注意這里,對(duì)應(yīng)上述的3個(gè)參數(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 + "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);
                    }
                });
            }
        },
……
//獲得參數(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個(gè)參數(shù)。在get方法中,當(dāng)cookie為空時(shí),tid會(huì)調(diào)用getTidFromServer,這時(shí)就產(chǎn)生了第5個(gè)請(qǐng)求https://passport.weibo.com/vi...,它需要兩個(gè)參數(shù)cb和fp,其參數(shù)值可以作為常量:

該請(qǐng)求的結(jié)果返回一串json

{
  "msg": "succ",
  "data": {
    "new_tid": false,
    "confidence": 95,
    "tid": "kIRvLolhrCR5iSCc80tWqDYmwBvlRVlnY2+yvCQ1VVA="
  },
  "retcode": 20000000
}

其中就包含了tid和confidence,這個(gè)confidence,我猜大概是推測客戶端是否真實(shí)的一個(gè)置信度,不一定出現(xiàn),根據(jù)window.gen_callback方法,不出現(xiàn)時(shí)默認(rèn)為100,另外當(dāng)new_tid為真時(shí)參數(shù)where等于3,否則等于2。

此時(shí)3個(gè)參數(shù)已經(jīng)全部獲得,現(xiàn)在就可以用httpclient發(fā)起上面第6個(gè)請(qǐng)求,返回得到另一串json:

{
  "msg": "succ",
  "data": {
    "sub": "_2AkMu428tf8NxqwJRmPAcxWzmZYh_zQjEieKYv572JRMxHRl-yT83qnMGtRCnhyR4ezQQZQrBRO3gVMwM5ZB2hQ..",
    "subp": "0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWU2MgYnITksS2awP.AX-DQ"
  },
  "retcode": 20000000
}

參考最后請(qǐng)求weibo.com的header,這里的sub和subp就是最終要獲取的cookie值。大家或許有一個(gè)小疑問,第一個(gè)Cookie怎么來的,沒用嗎?是的,這個(gè)Cookie是第一次訪問weibo.com產(chǎn)生的,經(jīng)過測試可以不用裝載。

最后我們用上面兩個(gè)Cookie裝載到HttpClient中請(qǐng)求一次weibo.com,就可以獲得完整的html頁面了,下面就是見證奇跡的時(shí)刻:











微博-隨時(shí)隨地發(fā)現(xiàn)新鮮事






……

如果之前有微博爬蟲開發(fā)經(jīng)驗(yàn)的小伙伴,看到這里,一定能想出來很多玩法了吧。

四、代碼實(shí)現(xiàn)

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

我把Cookie獲取的過程做成了一個(gè)靜態(tài)內(nèi)部類,其中需要發(fā)起2次請(qǐng)求,一次是genvisitor獲得3個(gè)參數(shù),另一次是incarnate獲得Cookie值;

如果Cookie獲取失敗,會(huì)調(diào)用HttpClientInstance.changeProxy來改變代理IP,然后重新獲取,直到獲取成功為止;

在使用時(shí),出現(xiàn)了IP被封或無法正常獲取頁面等異常情況,外部可以通過調(diào)用cookieReset方法,重新獲取一個(gè)新的Cookie。這里還是要聲明一下,科學(xué)地使用爬蟲,維護(hù)世界和平是程序員的基本素養(yǎng);

雖然加了一些鎖的控制,但是還未在高并發(fā)場景實(shí)測過,不能保證百分百線程安全,如使用下面的代碼,請(qǐng)根據(jù)需要自行修改,如有問題也請(qǐng)大神們及時(shí)指出,拜謝!

HttpClientInstance是我用單例模式重新封裝的httpclient,對(duì)于每個(gè)傳進(jìn)來的請(qǐng)求重新包裝了一層RequestConfig,并且使用了代理IP;

不是所有的微博頁面都可以抓取得到,但是博文,評(píng)論,轉(zhuǎn)發(fā)等基本的數(shù)據(jù)還是沒有問題的;

后續(xù)我也會(huì)把代碼push到github上,請(qǐng)大家支持,謝謝!

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;
 
/**
 * 微博免登陸請(qǐng)求客戶端
 *
 * @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方法,獲取微博平臺(tái)的其他頁面
     * @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;
        }
    }
 
     /**
     * 獲取訪問微博時(shí)必需的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 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 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();
        }
    }
}

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70483.html

相關(guān)文章

  • 首次公開,整理12年積累的博客收藏夾,零距離展示《收藏夾吃灰》系列博客

    摘要:時(shí)間永遠(yuǎn)都過得那么快,一晃從年注冊(cè),到現(xiàn)在已經(jīng)過去了年那些被我藏在收藏夾吃灰的文章,已經(jīng)太多了,是時(shí)候把他們整理一下了。那是因?yàn)槭詹貖A太亂,橡皮擦給設(shè)置私密了,不收拾不好看呀。 ...

    Harriet666 評(píng)論0 收藏0
  • 參考 - 收藏集 - 掘金

    摘要:譯年你不能錯(cuò)過的類庫后端掘金各位讀者好,這篇文章是在我看過的一篇介紹文后,整理出來的。上線后平穩(wěn)運(yùn)行我的后端書架后端掘金我的后端書架月前本書架主要針對(duì)后端開發(fā)與架構(gòu)。 【譯】2017 年你不能錯(cuò)過的 Java 類庫 - 后端 - 掘金各位讀者好, 這篇文章是在我看過 Andres Almiray 的一篇介紹文后,整理出來的。 因?yàn)閮?nèi)容非常好,我便將它整理成參考列表分享給大家, 同時(shí)附上...

    feng409 評(píng)論0 收藏0
  • 「碼個(gè)蛋」2017年200篇精選干貨集合

    摘要:讓你收獲滿滿碼個(gè)蛋從年月日推送第篇文章一年過去了已累積推文近篇文章,本文為年度精選,共計(jì)篇,按照類別整理便于讀者主題閱讀。本篇文章是今年的最后一篇技術(shù)文章,為了讓大家在家也能好好學(xué)習(xí),特此花了幾個(gè)小時(shí)整理了這些文章。 showImg(https://segmentfault.com/img/remote/1460000013241596); 讓你收獲滿滿! 碼個(gè)蛋從2017年02月20...

    wangtdgoodluck 評(píng)論0 收藏0
  • 2018先知白帽大會(huì) | 議題解讀

    摘要:摘要今年的先知白帽大會(huì),與會(huì)者將能夠親身感受到非常多有趣的技術(shù)議題,如在國際賽事中屢奪佳績的團(tuán)隊(duì),其隊(duì)長將親臨現(xiàn)場,分享穿針引線般的漏洞利用藝術(shù)。從數(shù)據(jù)視角探索安全威脅阿里云安全工程師議題解讀本議題討論了數(shù)據(jù)為安全人員思維方式帶來的變化。 摘要: 今年的先知白帽大會(huì),與會(huì)者將能夠親身感受到非常多有趣的技術(shù)議題,如HITCON在國際賽事中屢奪佳績的CTF團(tuán)隊(duì),其隊(duì)長Orange將親臨現(xiàn)場...

    Hydrogen 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<