zoukankan      html  css  js  c++  java
  • 微博爬虫“免登录”技巧详解及 Java 实现(业余草的博客)

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

    目前,对于微博的爬虫,大部分是基于模拟微博账号登录的方式实现的,这种方式如果真的运营起来,实际上是一件非常头疼痛苦的事,你可能每天都过得提心吊胆,生怕新浪爸爸把你的那些账号给封了,而且现在随着实名制的落地,获得账号的渠道估计也会变得越来越少。
    但是日子还得继续,在如此艰难的条件下,为了生存爬虫们必须寻求进化。好在上帝关门的同时会随手开窗,微博在其他诸如头条,一点等这类新媒体平台的冲击之下,逐步放开了信息流的查看权限。现在的微博即便在不登录的状态下,依然可以看到很多微博信息流,而我们的落脚点就在这里。
    本文详细介绍如何获取相关的Cookie并重新封装Httpclient达到免登录的目的,以支持微博上的各项数据抓取任务。下面就从微博首页http://weibo.com开始。

    二、准备工作

    准备工作很简单,一个现代浏览器(你知道我为什么会写”现代”两个字),以及httpclient(我用的版本是4.5.3)
    跟登录爬虫一样,免登录爬虫也是需要装载Cookie。这里的Cookie是用来标明游客身份,利用这个Cookie就可以在微博平台中访问那些允许访问的内容了。

    这里我们可以使用浏览器的network工具来看一下,请求http://weibo.com之后服务器都返回哪些东西,当然事先清空一下浏览器的缓存。

    我的博客:CODE大全www.codedq.net业余草www.xttblog.com爱分享www.ndislwf.comifxvn.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:

    [javascript] view plain copy
     
    1. // 为用户赋予访客身份 。   
    2. var incarnate = function (tid, where, conficence) {   
    3.     var gen_conf = "";   
    4.     var from = "weibo";   
    5.     var incarnate_intr = window.location.protocol   
    6.     + "//" + window.location.host + "/visitor/visitor?a=incarnate&t="   
    7.     + encodeURIComponent(tid) + "&w=" + encodeURIComponent(where)   
    8.     + "&c=" + encodeURIComponent(conficence) + "&gc="   
    9.     + encodeURIComponent(gen_conf) + "&cb=cross_domain&from="   
    10.     + from + "&_rand=" + Math.random();   
    11.     url.l(incarnate_intr);   
    12. };    

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

    [javascript] view plain copy
     
    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 + '//'   
    42.                 + window.location.host + '/' + tid.postInterface, "cb=gen_callback&fp="  
    43.                 + encodeURIComponent(data), function (res) {   
    44.                     if (res) {   
    45.                         eval(res);   
    46.                     }   
    47.                 });   
    48.             });   
    49.         } else {   
    50.             util.postData(window.location.protocol + '//'   
    51.             + window.location.host + '/'   
    52.             + tid.postInterface, "cb=gen_callback", function (res) {   
    53.                 if (res) {   
    54.                     eval(res);   
    55.                 }   
    56.             });   
    57.         }   
    58.     },   
    59. ……   
    60. //获得参数   
    61. window.gen_callback = function (fp) {   
    62.     var value = false, confidence;   
    63.     if (fp) {   
    64.         if (fp.retcode == 20000000) {   
    65.             confidence = typeof(fp.data.confidence) != 'undefined' ? '000'   
    66.             + fp.data.confidence : '100';   
    67.             tid.recover = fp.data.new_tid ? 3 : 2;   
    68.             tid.confidence = confidence = confidence.substring(confidence.length - 3);   
    69.             value = fp.data.tid;   
    70.             Store.DB.set(tid.key, value + '__' + confidence);   
    71.         }   
    72.     }   
    73.     tid.value = value;   
    74.     tid.runstack();   
    75. };    

    我的博客:CODE大全www.codedq.net业余草www.xttblog.com爱分享www.ndislwf.comifxvn.com

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

    业余草的博客

    该请求的结果返回一串json

    [plain] view plain copy
     
    1. {   
    2.   "msg": "succ",   
    3.   "data": {   
    4.     "new_tid": false,   
    5.     "confidence": 95,   
    6.     "tid": "kIRvLolhrCR5iSCc80tWqDYmwBvlRVlnY2+yvCQ1VVA="   
    7.   },   
    8.   "retcode": 20000000   
    9. }  

    我的博客:CODE大全www.codedq.net业余草www.xttblog.com爱分享www.ndislwf.comifxvn.com
    其中就包含了tid和confidence,这个confidence,我猜大概是推测客户端是否真实的一个置信度,不一定出现,根据window.gen_callback方法,不出现时默认为100,另外当new_tid为真时参数where等于3,否则等于2。
    此时3个参数已经全部获得,现在就可以用httpclient发起上面第6个请求,返回得到另一串json:

    [plain] view plain copy
     
    1. {   
    2.   "msg": "succ",   
    3.   "data": {   
    4.     "sub": "_2AkMu428tf8NxqwJRmPAcxWzmZYh_zQjEieKYv572JRMxHRl-yT83qnMGtRCnhyR4ezQQZQrBRO3gVMwM5ZB2hQ..",   
    5.     "subp": "0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWU2MgYnITksS2awP.AX-DQ"   
    6.   },   
    7.   "retcode": 20000000   
    8. }    

    我的博客:CODE大全www.codedq.net业余草www.xttblog.com爱分享www.ndislwf.comifxvn.com
    参考最后请求weibo.com的header,这里的sub和subp就是最终要获取的cookie值。大家或许有一个小疑问,第一个Cookie怎么来的,没用吗?是的,这个Cookie是第一次访问weibo.com产生的,经过测试可以不用装载。

    业余草的博客

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

    [html] view plain copy
     
    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="随时随地发现新鲜事!微博带你欣赏世界上每一个精彩瞬间,了解每一个幕后故事。  
    8. 分享你想表达的,让全世界都能听到你的心声!" name="description" />   
    9. <link rel="mask-icon" sizes="any" href="//img.t.sinajs.cn/t6/style/images/apple/wbfont.svg" color="black" />   
    10. <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />   
    11. <script type="text/javascript">   
    12. try{document.execCommand("BackgroundImageCache", false, true);}catch(e){}   
    13. </script>   
    14. <title>微博-随时随地发现新鲜事</title>   
    15. <link href="//img.t.sinajs.cn/t6/style/css/module/base/frame.css?version=6c9bf6ab3b33391f"  
    16.  type="text/css" rel="stylesheet" charset="utf-8" />   
    17. <link href="//img.t.sinajs.cn/t6/style/css/pages/growth/login_v5.css?version=6c9bf6ab3b33391f"   
    18. type="text/css" rel="stylesheet" charset="utf-8">   
    19. <link href="//img.t.sinajs.cn/t6/skin/default/skin.css?version=6c9bf6ab3b33391f" type="text/css"   
    20. rel="stylesheet" id="skin_style" />   
    21. <script type="text/javascript">   
    22. var $CONFIG = {};   
    23. $CONFIG['islogin'] = '0';   
    24. $CONFIG['version'] = '6c9bf6ab3b33391f';   
    25. $CONFIG['timeDiff'] = (new Date() - 1505746970000);   
    26. $CONFIG['lang'] = 'zh-cn';   
    27. $CONFIG['jsPath'] = '//js.t.sinajs.cn/t5/';   
    28. $CONFIG['cssPath'] = '//img.t.sinajs.cn/t5/';   
    29. $CONFIG['imgPath'] = '//img.t.sinajs.cn/t5/';   
    30. $CONFIG['servertime'] = 1505746970;   
    31. $CONFIG['location']='login';   
    32. $CONFIG['bigpipe']='false';   
    33. $CONFIG['bpType']='login';   
    34. $CONFIG['mJsPath'] = ['//js{n}.t.sinajs.cn/t5/', 1, 2];   
    35. $CONFIG['mCssPath'] = ['//img{n}.t.sinajs.cn/t5/', 1, 2];   
    36. $CONFIG['redirect'] = '';   
    37. $CONFIG['vid']='1008997495870';   
    38. </script>   
    39. <style>#js_style_css_module_global_WB_outframe{height:42px;}</style>   
    40. </head>   
    41. ……    

    我的博客:CODE大全www.codedq.net业余草www.xttblog.com爱分享www.ndislwf.comifxvn.com

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

    四、代码实现

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

    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上,请大家支持,谢谢! 
    [java] view plain copy
     
    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.     
    15. import java.io.UnsupportedEncodingException;   
    16. import java.math.BigDecimal;   
    17. import java.net.URLEncoder;   
    18. import java.util.Map;   
    19. import java.util.concurrent.locks.Lock;   
    20. import java.util.concurrent.locks.ReentrantLock;   
    21.     
    22. /**  
    23.  * 微博免登陆请求客户端  
    24.  *  
    25.  * @author fullstackyang  
    26.  */   
    27. @Slf4j   
    28. public class WeiboClient {   
    29.     
    30.     private static CookieFetcher cookieFetcher = new CookieFetcher();   
    31.     
    32.     private volatile String cookie;   
    33.     
    34.     public WeiboClient() {   
    35.         this.cookie = cookieFetcher.getCookie();   
    36.     }   
    37.     
    38.     private static Lock lock = new ReentrantLock();   
    39.     
    40.     public void cookieReset() {   
    41.         if (lock.tryLock()) {   
    42.             try {   
    43.                 HttpClientInstance.instance().changeProxy();   
    44.                 this.cookie = cookieFetcher.getCookie();   
    45.                 log.info("cookie :" + cookie);   
    46.             } finally {   
    47.                 lock.unlock();   
    48.             }   
    49.         }   
    50.     }   
    51.     
    52.     /**  
    53.      * get方法,获取微博平台的其他页面  
    54.      * @param url  
    55.      * @return  
    56.      */   
    57.     public String get(String url) {   
    58.         if (Strings.isNullOrEmpty(url))   
    59.             return "";   
    60.     
    61.         while (true) {   
    62.             HttpGet httpGet = new HttpGet(url);   
    63.             httpGet.addHeader(HttpHeaders.COOKIE, cookie);   
    64.             httpGet.addHeader(HttpHeaders.HOST, "weibo.com");   
    65.             httpGet.addHeader("Upgrade-Insecure-Requests", "1");   
    66.     
    67.             httpGet.setConfig(RequestConfig.custom().setSocketTimeout(3000)   
    68.                     .setConnectTimeout(3000).setConnectionRequestTimeout(3000).build());   
    69.             String html = HttpClientInstance.instance().tryExecute(httpGet, null, null);   
    70.             if (html == null)   
    71.                 cookieReset();   
    72.             else return html;   
    73.         }   
    74.     }   
    75.     
    76.      /**  
    77.      * 获取访问微博时必需的Cookie  
    78.      */   
    79.     @NoArgsConstructor   
    80.     static class CookieFetcher {   
    81.     
    82.         static final String PASSPORT_URL = "https://passport.weibo.com/visitor/visitor?entry="  
    83.                 +"miniblog&a=enter&url=http://weibo.com/?category=2"   
    84.                 + "&domain=.weibo.com&ua=php-sso_sdk_client-0.6.23";   
    85.     
    86.         static final String GEN_VISITOR_URL = "https://passport.weibo.com/visitor/genvisitor";   
    87.     
    88.         static final String VISITOR_URL = "https://passport.weibo.com/visitor/visitor?a=incarnate";   
    89.     
    90.         private String getCookie() {   
    91.             Map<String, String> map;   
    92.             while (true) {   
    93.                 map = getCookieParam();   
    94.                 if (map.containsKey("SUB") && map.containsKey("SUBP") &&   
    95.                         StringUtils.isNoneEmpty(map.get("SUB"), map.get("SUBP")))   
    96.                     break;   
    97.                 HttpClientInstance.instance().changeProxy();   
    98.             }   
    99.             return " YF-Page-G0=" + "; _s_tentry=-; SUB=" + map.get("SUB") + "; SUBP=" + map.get("SUBP");   
    100.         }   
    101.     
    102.         private Map<String, String> getCookieParam() {   
    103.             String time = System.currentTimeMillis() + "";   
    104.             time = time.substring(0, 9) + "." + time.substring(9, 13);   
    105.             String passporturl = PASSPORT_URL + "&_rand=" + time;   
    106.     
    107.             String tid = "";   
    108.             String c = "";   
    109.             String w = "";   
    110.             {   
    111.                 String str = postGenvisitor(passporturl);   
    112.                 if (str.contains(""retcode":20000000")) {   
    113.                     JSONObject jsonObject = new JSONObject(str).getJSONObject("data");   
    114.                     tid = jsonObject.optString("tid");   
    115.                     try {   
    116.                         tid = URLEncoder.encode(tid, "utf-8");   
    117.                     } catch (UnsupportedEncodingException e) {   
    118.                     }   
    119.                     c = jsonObject.has("confidence") ? "000" + jsonObject.getInt("confidence") : "100";   
    120.                     w = jsonObject.optBoolean("new_tid") ? "3" : "2";   
    121.                 }   
    122.             }   
    123.             String s = "";   
    124.             String sp = "";   
    125.             {   
    126.                 if (StringUtils.isNoneEmpty(tid, w, c)) {   
    127.                     String str = getVisitor(tid, w, c, passporturl);   
    128.                     str = str.substring(str.indexOf("(") + 1, str.indexOf(")"));   
    129.                     if (str.contains(""retcode":20000000")) {   
    130.                         System.out.println(new JSONObject(str).toString(2));   
    131.                         JSONObject jsonObject = new JSONObject(str).getJSONObject("data");   
    132.                         s = jsonObject.getString("sub");   
    133.                         sp = jsonObject.getString("subp");   
    134.                     }   
    135.     
    136.                 }   
    137.             }   
    138.             Map<String, String> map = Maps.newHashMap();   
    139.             map.put("SUB", s);   
    140.             map.put("SUBP", sp);   
    141.             return map;   
    142.         }   
    143.     
    144.         private String postGenvisitor(String passporturl) {   
    145.     
    146.             Map<String, String> headers = Maps.newHashMap();   
    147.             headers.put(HttpHeaders.ACCEPT, "*/*");   
    148.             headers.put(HttpHeaders.ORIGIN, "https://passport.weibo.com");   
    149.             headers.put(HttpHeaders.REFERER, passporturl);   
    150.     
    151.             Map<String, String> params = Maps.newHashMap();   
    152.             params.put("cb", "gen_callback");   
    153.             params.put("fp", fp());   
    154.     
    155.             HttpPost httpPost = HttpRequestUtils.createHttpPost(GEN_VISITOR_URL, headers, params);   
    156.     
    157.             String str = HttpClientInstance.instance().execute(httpPost, null);   
    158.             return str.substring(str.indexOf("(") + 1, str.lastIndexOf(""));   
    159.         }   
    160.     
    161.         private String getVisitor(String tid, String w, String c, String passporturl) {   
    162.             String url = VISITOR_URL + "&t=" + tid + "&w=" + "&c=" + c.substring(c.length() - 3)   
    163.                     + "&gc=&cb=cross_domain&from=weibo&_rand=0." + rand();   
    164.     
    165.             Map<String, String> headers = Maps.newHashMap();   
    166.             headers.put(HttpHeaders.ACCEPT, "*/*");   
    167.             headers.put(HttpHeaders.HOST, "passport.weibo.com");   
    168.             headers.put(HttpHeaders.COOKIE, "tid=" + tid + "__0" + c);   
    169.             headers.put(HttpHeaders.REFERER, passporturl);   
    170.     
    171.             HttpGet httpGet = HttpRequestUtils.createHttpGet(url, headers);   
    172.             httpGet.setConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build());   
    173.             return HttpClientInstance.instance().execute(httpGet, null);   
    174.         }   
    175.     
    176.         private static String rand() {   
    177.             return new BigDecimal(Math.floor(Math.random() * 10000000000000000L)).toString();   
    178.         }   
    179.     
    180.         private static String fp() {   
    181.             JSONObject jsonObject = new JSONObject();   
    182.             jsonObject.put("os", "1");   
    183.             jsonObject.put("browser", "Chrome59,0,3071,115");   
    184.             jsonObject.put("fonts", "undefined");   
    185.             jsonObject.put("screenInfo", "1680*1050*24");   
    186.             jsonObject.put("plugins",   
    187.                     "Enables Widevine licenses for playback of HTML audio/video content. "  
    188.                     +"(version: 1.4.8.984)::widevinecdmadapter.dll::Widevine Content "  
    189.                     +"Decryption Module|Shockwave Flash 26.0 r0::pepflashplayer.dll::Shockwave"  
    190.                     +" Flash|::mhjfbmdgcfjbbpaeojofohoefgiehjai::Chrome PDF "  
    191.                     +"Viewer|::internal-nacl-plugin::Native Client|Portable Document"  
    192.                     +" Format::internal-pdf-viewer::Chrome PDF Viewer");   
    193.             return jsonObject.toString();   
    194.         }   
    195.     }   
    196. }    
    我的博客:CODE大全www.codedq.net业余草www.xttblog.com爱分享www.ndislwf.comifxvn.com
  • 相关阅读:
    QFramework 使用指南 2020(二):下载与版本介绍
    QFramework 使用指南 2020 (一): 概述
    Unity 游戏框架搭建 2018 (二) 单例的模板与最佳实践
    Unity 游戏框架搭建 2018 (一) 架构、框架与 QFramework 简介
    Unity 游戏框架搭建 2017 (二十三) 重构小工具 Platform
    Unity 游戏框架搭建 2017 (二十二) 简易引用计数器
    Unity 游戏框架搭建 2017 (二十一) 使用对象池时的一些细节
    你确定你会写 Dockerfile 吗?
    小白学 Python 爬虫(8):网页基础
    老司机大型车祸现场
  • 原文地址:https://www.cnblogs.com/panda2/p/7600589.html
Copyright © 2011-2022 走看看