微信号:programmer_club

介绍:程序员第一自媒体,与你探讨码农人生路上遇到的各类泛技术话题,定期为你推荐码农人生思考、感悟以及启迪!

微博爬虫“免登录”技巧详解及 Java 实现

2017-10-03 20:30 程序员之家

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

目前,对于微博的爬虫,大部分是基于模拟微博账号登录的方式实现的,这种方式如果真的运营起来,实际上是一件非常头疼痛苦的事,你可能每天都过得提心吊胆,生怕新浪爸爸把你的那些账号给封了,而且现在随着实名制的落地,获得账号的渠道估计也会变得越来越少。

但是日子还得继续,在如此艰难的条件下,为了生存爬虫们必须寻求进化。好在上帝关门的同时会随手开窗,微博在其他诸如头条,一点等这类新媒体平台的冲击之下,逐步放开了信息流的查看权限。现在的微博即便在不登录的状态下,依然可以看到很多微博信息流,而我们的落脚点就在这里。

本文详细介绍如何获取相关的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:

 
           
  1. // 为用户赋予访客身份 。

  2.    var incarnate = function (tid, where, conficence) {

  3.        var gen_conf = "";

  4.        var from = "weibo";

  5.        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();

  6.        url.l(incarnate_intr);

  7.    };

这里是为请求者赋予一个访客身份,而控制跳转的链接也是由一些参数拼接起来的,也就是上图中第6个请求。所以下面的工作就是获得这3个参数:tid,w(where),c(conficence,从下文来看应为confidence,大概是新浪工程师的手误)。继续阅读源码,可以看到该function是tid.get方法的回调函数,而这个tid则是定义在那个mini_original.js中的一个对象,其部分源码为:

 
           
  1.    var tid = {

  2.        key: 'tid',

  3.        value: '',

  4.        recover: 0,

  5.        confidence: '',

  6.        postInterface: postUrl,

  7.        fpCollectInterface: sendUrl,

  8.        callbackStack: [],

  9.        init: function () {

  10.            tid.get();

  11.        },

  12.        runstack: function () {

  13.            var f;

  14.            while (f = tid.callbackStack.pop()) {

  15.                f(tid.value, tid.recover, tid.confidence);//注意这里,对应上述的3个参数

  16.            }

  17.        },

  18.        get: function (callback) {

  19.            callback = callback || function () {

  20.            };

  21.            tid.callbackStack.push(callback);

  22.            if (tid.value) {

  23.                return tid.runstack();

  24.            }

  25.            Store.DB.get(tid.key, function (v) {

  26.                if (!v) {

  27.                    tid.getTidFromServer();

  28.                } else {

  29.                    ……

  30.                }

  31.            });

  32.        },

  33.    ……

  34.    }

  35. ……

  36. getTidFromServer: function () {

  37.            tid.getTidFromServer = function () {

  38.            };

  39.            if (window.use_fp) {

  40.                getFp(function (data) {

  41.                    util.postData(window.location.protocol + '//' + window.location.host + '/' + tid.postInterface, "cb=gen_callback&fp=" + encodeURIComponent(data), function (res) {

  42.                        if (res) {

  43.                            eval(res);

  44.                        }

  45.                    });

  46.                });

  47.            } else {

  48.                util.postData(window.location.protocol + '//' + window.location.host + '/' + tid.postInterface, "cb=gen_callback", function (res) {

  49.                    if (res) {

  50.                        eval(res);

  51.                    }

  52.                });

  53.            }

  54.        },

  55. ……

  56. //获得参数

  57. window.gen_callback = function (fp) {

  58.        var value = false, confidence;

  59.        if (fp) {

  60.            if (fp.retcode == 20000000) {

  61.                confidence = typeof(fp.data.confidence) != 'undefined' ? '000' + fp.data.confidence : '100';

  62.                tid.recover = fp.data.new_tid ? 3 : 2;

  63.                tid.confidence = confidence = confidence.substring(confidence.length - 3);

  64.                value = fp.data.tid;

  65.                Store.DB.set(tid.key, value + '__' + confidence);

  66.            }

  67.        }

  68.        tid.value = value;

  69.        tid.runstack();

  70.    };

显然,tid.runstack()是真正执行回调函数的地方,这里就能看到传入的3个参数。在get方法中,当cookie为空时,tid会调用getTidFromServer,这时就产生了第5个请求https://passport.weibo.com/visitor/genvisitor,它需要两个参数cb和fp,其参数值可以作为常量:

该请求的结果返回一串json

 
           
  1. {

  2.  "msg": "succ",

  3.  "data": {

  4.    "new_tid": false,

  5.    "confidence": 95,

  6.    "tid": "kIRvLolhrCR5iSCc80tWqDYmwBvlRVlnY2+yvCQ1VVA="

  7.  },

  8.  "retcode": 20000000

  9. }

其中就包含了tid和confidence,这个confidence,我猜大概是推测客户端是否真实的一个置信度,不一定出现,根据window.gencallback方法,不出现时默认为100,另外当newtid为真时参数where等于3,否则等于2。

此时3个参数已经全部获得,现在就可以用httpclient发起上面第6个请求,返回得到另一串json:

 
           
  1. {

  2.  "msg": "succ",

  3.  "data": {

  4.    "sub": "_2AkMu428tf8NxqwJRmPAcxWzmZYh_zQjEieKYv572JRMxHRl-yT83qnMGtRCnhyR4ezQQZQrBRO3gVMwM5ZB2hQ..",

  5.    "subp": "0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWU2MgYnITksS2awP.AX-DQ"

  6.  },

  7.  "retcode": 20000000

  8. }

参考最后请求weibo.com的header,这里的sub和subp就是最终要获取的cookie值。大家或许有一个小疑问,第一个Cookie怎么来的,没用吗?是的,这个Cookie是第一次访问weibo.com产生的,经过测试可以不用装载。

最后我们用上面两个Cookie装载到HttpClient中请求一次weibo.com,就可以获得完整的html页面了,下面就是见证奇迹的时刻:

 
           
  1. <!doctype html>

  2. <html>

  3. <head>

  4. <meta charset="utf-8">

  5. <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

  6. <meta name="viewport" content="initial-scale=1,minimum-scale=1" />

  7. <meta content="随时随地发现新鲜事!微博带你欣赏世界上每一个精彩瞬间,了解每一个幕后故事。分享你想表达的,让全世界都能听到你的心声!" name="description" />

  8. <link rel="mask-icon" sizes="any" href="//img.t.sinajs.cn/t6/style/images/apple/wbfont.svg" color="black" />

  9. <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />

  10. <script type="text/javascript">

  11. try{document.execCommand("BackgroundImageCache", false, true);}catch(e){}

  12. </script>

  13. <title>微博-随时随地发现新鲜事</title>

  14. <link href="//img.t.sinajs.cn/t6/style/css/module/base/frame.css?version=6c9bf6ab3b33391f" type="text/css" rel="stylesheet" charset="utf-8" />

  15. <link href="//img.t.sinajs.cn/t6/style/css/pages/growth/login_v5.css?version=6c9bf6ab3b33391f" type="text/css" rel="stylesheet" charset="utf-8">

  16. <link href="//img.t.sinajs.cn/t6/skin/default/skin.css?version=6c9bf6ab3b33391f" type="text/css" rel="stylesheet" id="skin_style" />

  17. <script type="text/javascript">

  18. var $CONFIG = {};

  19. $CONFIG['islogin'] = '0';

  20. $CONFIG['version'] = '6c9bf6ab3b33391f';

  21. $CONFIG['timeDiff'] = (new Date() - 1505746970000);

  22. $CONFIG['lang'] = 'zh-cn';

  23. $CONFIG['jsPath'] = '//js.t.sinajs.cn/t5/';

  24. $CONFIG['cssPath'] = '//img.t.sinajs.cn/t5/';

  25. $CONFIG['imgPath'] = '//img.t.sinajs.cn/t5/';

  26. $CONFIG['servertime'] = 1505746970;

  27. $CONFIG['location']='login';

  28. $CONFIG['bigpipe']='false';

  29. $CONFIG['bpType']='login';

  30. $CONFIG['mJsPath'] = ['//js{n}.t.sinajs.cn/t5/', 1, 2];

  31. $CONFIG['mCssPath'] = ['//img{n}.t.sinajs.cn/t5/', 1, 2];

  32. $CONFIG['redirect'] = '';

  33. $CONFIG['vid']='1008997495870';

  34. </script>

  35. <style>#js_style_css_module_global_WB_outframe{height:42px;}</style>

  36. </head>

  37. ……

如果之前有微博爬虫开发经验的小伙伴,看到这里,一定能想出来很多玩法了吧。

四、代码实现

下面附上我的源码,通过上面的详细介绍,应该已经比较好理解,因此这里就简单地说明一下:

  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上,请大家支持,谢谢!

 
           
  1. import com.fullstackyang.httpclient.HttpClientInstance;

  2. import com.fullstackyang.httpclient.HttpRequestUtils;

  3. import com.google.common.base.Strings;

  4. import com.google.common.collect.Maps;

  5. import com.google.common.net.HttpHeaders;

  6. import lombok.NoArgsConstructor;

  7. import lombok.extern.slf4j.Slf4j;

  8. import org.apache.commons.lang3.StringUtils;

  9. import org.apache.http.client.config.CookieSpecs;

  10. import org.apache.http.client.config.RequestConfig;

  11. import org.apache.http.client.methods.HttpGet;

  12. import org.apache.http.client.methods.HttpPost;

  13. import org.json.JSONObject;

  14. import java.io.UnsupportedEncodingException;

  15. import java.math.BigDecimal;

  16. import java.net.URLEncoder;

  17. import java.util.Map;

  18. import java.util.concurrent.locks.Lock;

  19. import java.util.concurrent.locks.ReentrantLock;

  20. /**

  21. * 微博免登陆请求客户端

  22. *

  23. * @author fullstackyang

  24. */

  25. @Slf4j

  26. public class WeiboClient {

  27.    private static CookieFetcher cookieFetcher = new CookieFetcher();

  28.    private volatile String cookie;

  29.    public WeiboClient() {

  30.        this.cookie = cookieFetcher.getCookie();

  31.    }

  32.    private static Lock lock = new ReentrantLock();

  33.    public void cookieReset() {

  34.        if (lock.tryLock()) {

  35.            try {

  36.                HttpClientInstance.instance().changeProxy();

  37.                this.cookie = cookieFetcher.getCookie();

  38.                log.info("cookie :" + cookie);

  39.            } finally {

  40.                lock.unlock();

  41.            }

  42.        }

  43.    }

  44.    /**

  45.     * get方法,获取微博平台的其他页面

  46.     * @param url

  47.     * @return

  48.     */

  49.    public String get(String url) {

  50.        if (Strings.isNullOrEmpty(url))

  51.            return "";

  52.        while (true) {

  53.            HttpGet httpGet = new HttpGet(url);

  54.            httpGet.addHeader(HttpHeaders.COOKIE, cookie);

  55.            httpGet.addHeader(HttpHeaders.HOST, "weibo.com");

  56.            httpGet.addHeader("Upgrade-Insecure-Requests", "1");

  57.            httpGet.setConfig(RequestConfig.custom().setSocketTimeout(3000)

  58.                    .setConnectTimeout(3000).setConnectionRequestTimeout(3000).build());

  59.            String html = HttpClientInstance.instance().tryExecute(httpGet, null, null);

  60.            if (html == null)

  61.                cookieReset();

  62.            else return html;

  63.        }

  64.    }

  65.     /**

  66.     * 获取访问微博时必需的Cookie

  67.     */

  68.    @NoArgsConstructor

  69.    static class CookieFetcher {

  70.        static final String PASSPORT_URL = "https://passport.weibo.com/visitor/visitor?entry=miniblog&a=enter&url=http://weibo.com/?category=2"

  71.                + "&domain=.weibo.com&ua=php-sso_sdk_client-0.6.23";

  72.        static final String GEN_VISITOR_URL = "https://passport.weibo.com/visitor/genvisitor";

  73.        static final String VISITOR_URL = "https://passport.weibo.com/visitor/visitor?a=incarnate";

  74.        private String getCookie() {

  75.            Map<String, String> map;

  76.            while (true) {

  77.                map = getCookieParam();

  78.                if (map.containsKey("SUB") && map.containsKey("SUBP") &&

  79.                        StringUtils.isNoneEmpty(map.get("SUB"), map.get("SUBP")))

  80.                    break;

  81.                HttpClientInstance.instance().changeProxy();

  82.            }

  83.            return " YF-Page-G0=" + "; _s_tentry=-; SUB=" + map.get("SUB") + "; SUBP=" + map.get("SUBP");

  84.        }

  85.        private Map<String, String> getCookieParam() {

  86.            String time = System.currentTimeMillis() + "";

  87.            time = time.substring(0, 9) + "." + time.substring(9, 13);

  88.            String passporturl = PASSPORT_URL + "&_rand=" + time;

  89.            String tid = "";

  90.            String c = "";

  91.            String w = "";

  92.            {

  93.                String str = postGenvisitor(passporturl);

  94.                if (str.contains(""retcode":20000000")) {

  95.                    JSONObject jsonObject = new JSONObject(str).getJSONObject("data");

  96.                    tid = jsonObject.optString("tid");

  97.                    try {

  98.                        tid = URLEncoder.encode(tid, "utf-8");

  99.                    } catch (UnsupportedEncodingException e) {

  100.                    }

  101.                    c = jsonObject.has("confidence") ? "000" + jsonObject.getInt("confidence") : "100";

  102.                    w = jsonObject.optBoolean("new_tid") ? "3" : "2";

  103.                }

  104.            }

  105.            String s = "";

  106.            String sp = "";

  107.            {

  108.                if (StringUtils.isNoneEmpty(tid, w, c)) {

  109.                    String str = getVisitor(tid, w, c, passporturl);

  110.                    str = str.substring(str.indexOf("(") + 1, str.indexOf(")"));

  111.                    if (str.contains(""retcode":20000000")) {

  112.                        System.out.println(new JSONObject(str).toString(2));

  113.                        JSONObject jsonObject = new JSONObject(str).getJSONObject("data");

  114.                        s = jsonObject.getString("sub");

  115.                        sp = jsonObject.getString("subp");

  116.                    }

  117.                }

  118.            }

  119.            Map<String, String> map = Maps.newHashMap();

  120.            map.put("SUB", s);

  121.            map.put("SUBP", sp);

  122.            return map;

  123.        }

  124.        private String postGenvisitor(String passporturl) {

  125.            Map<String, String> headers = Maps.newHashMap();

  126.            headers.put(HttpHeaders.ACCEPT, "*/*");

  127.            headers.put(HttpHeaders.ORIGIN, "https://passport.weibo.com");

  128.            headers.put(HttpHeaders.REFERER, passporturl);

  129.            Map<String, String> params = Maps.newHashMap();

  130.            params.put("cb", "gen_callback");

  131.            params.put("fp", fp());

  132.            HttpPost httpPost = HttpRequestUtils.createHttpPost(GEN_VISITOR_URL, headers, params);

  133.            String str = HttpClientInstance.instance().execute(httpPost, null);

  134.            return str.substring(str.indexOf("(") + 1, str.lastIndexOf(""));

  135.        }

  136.        private String getVisitor(String tid, String w, String c, String passporturl) {

  137.            String url = VISITOR_URL + "&t=" + tid + "&w=" + "&c=" + c.substring(c.length() - 3)

  138.                    + "&gc=&cb=cross_domain&from=weibo&_rand=0." + rand();

  139.            Map<String, String> headers = Maps.newHashMap();

  140.            headers.put(HttpHeaders.ACCEPT, "*/*");

  141.            headers.put(HttpHeaders.HOST, "passport.weibo.com");

  142.            headers.put(HttpHeaders.COOKIE, "tid=" + tid + "__0" + c);

  143.            headers.put(HttpHeaders.REFERER, passporturl);

  144.            HttpGet httpGet = HttpRequestUtils.createHttpGet(url, headers);

  145.            httpGet.setConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build());

  146.            return HttpClientInstance.instance().execute(httpGet, null);

  147.        }

  148.        private static String rand() {

  149.            return new BigDecimal(Math.floor(Math.random() * 10000000000000000L)).toString();

  150.        }

  151.        private static String fp() {

  152.            JSONObject jsonObject = new JSONObject();

  153.            jsonObject.put("os", "1");

  154.            jsonObject.put("browser", "Chrome59,0,3071,115");

  155.            jsonObject.put("fonts", "undefined");

  156.            jsonObject.put("screenInfo", "1680*1050*24");

  157.            jsonObject.put("plugins",

  158.                    "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");

  159.            return jsonObject.toString();

  160.        }

  161.    }

  162. }

编辑 | 码哥

图片源于网络,版权归原作者所有

 
程序员之家 更多文章 欧洲刮起性爱机器人风:用户体验称比真人舒服 2017互联网月饼哪家强?腾讯、阿里、百度、网易等21家中秋月饼盘点 印度diss中国?真正的威胁到底是什么? 抢网易云音乐饭碗?B站正式上线音乐播放器功能 马云又双叒叕搞事情啦!淘宝怎么活!
猜您喜欢 你真的会收拾行李吗? 深度剖析开源分布式监控CAT SAS or R:谁更适合你? 面对“混合云”的管理难题,“超云”的出现将给我们带来什么? 《2015MSC移动安全挑战赛》15万奖金等你赢!