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
  • 相关阅读:
    POJ2155 Matrix
    POJ3469 Dual Core CPU
    洛谷P1469找筷子
    CodeForces 97D. Robot in Basement
    UVa11542 Square
    清澄 A1485. Catch The Penguins 抓企鹅
    Bzoj2595: [Wc2008]游览计划
    HDU4085 Peach Blossom Spring
    CodeForces 333E. Summer Earnings
    洛谷P3389 【模板】高斯消元法
  • 原文地址:https://www.cnblogs.com/panda2/p/7600589.html
Copyright © 2011-2022 走看看