微博爬蟲“免登入”技巧詳解及 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.com或ifxvn.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:// 為使用者賦予訪客身份 。
var incarnate = function (tid, where, conficence) {
var gen_conf = "";
var from = "weibo";
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();
url.l(incarnate_intr);
};
我的部落格:CODE大全:www.codedq.net;業餘草:www.xttblog.com;愛分享:www.ndislwf.com或ifxvn.com。這裡是為請求者賦予一個訪客身份,而控制跳轉的連結也是由一些引數拼接起來的,也就是上圖中第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();
};
我的部落格:CODE大全:www.codedq.net;業餘草:www.xttblog.com;愛分享:www.ndislwf.com或ifxvn.com。
該請求的結果返回一串json
{
"msg": "succ",
"data": {
"new_tid": false,
"confidence": 95,
"tid": "kIRvLolhrCR5iSCc80tWqDYmwBvlRVlnY2+yvCQ1VVA="
},
"retcode": 20000000
}
我的部落格:CODE大全:www.codedq.net;業餘草:www.xttblog.com;愛分享:www.ndislwf.com或ifxvn.com。其中就包含了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
}
我的部落格:CODE大全:www.codedq.net;業餘草:www.xttblog.com;愛分享:www.ndislwf.com或ifxvn.com。參考最後請求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" href="//img.t.sinajs.cn/t6/style/images/apple/wbfont.svg" 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 href="//img.t.sinajs.cn/t6/style/css/module/base/frame.css?version=6c9bf6ab3b33391f"
type="text/css" rel="stylesheet" charset="utf-8" />
<link href="//img.t.sinajs.cn/t6/style/css/pages/growth/login_v5.css?version=6c9bf6ab3b33391f"
type="text/css" rel="stylesheet" charset="utf-8">
<link href="//img.t.sinajs.cn/t6/skin/default/skin.css?version=6c9bf6ab3b33391f" 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>
……
我的部落格:CODE大全:www.codedq.net;業餘草:www.xttblog.com;愛分享:www.ndislwf.com或ifxvn.com。
四、程式碼實現
下面附上我的原始碼,通過上面的詳細介紹,應該已經比較好理解,因此這裡就簡單地說明一下:- 我把Cookie獲取的過程做成了一個靜態內部類,其中需要發起2次請求,一次是genvisitor獲得3個引數,另一次是incarnate獲得Cookie值;
- 如果Cookie獲取失敗,會呼叫HttpClientInstance.changeProxy來改變代理IP,然後重新獲取,直到獲取成功為止;
- 在使用時,出現了IP被封或無法正常獲取頁面等異常情況,外部可以通過呼叫cookieReset方法,重新獲取一個新的Cookie。這裡還是要宣告一下,科學地使用爬蟲,維護世界和平是程式設計師的基本素養;
- 雖然加了一些鎖的控制,但是還未在高併發場景實測過,不能保證百分百執行緒安全,如使用下面的程式碼,請根據需要自行修改,如有問題也請大神們及時指出,拜謝!
- HttpClientInstance是我用單例模式重新封裝的httpclient,對於每個傳進來的請求重新包裝了一層RequestConfig,並且使用了代理IP;
- 不是所有的微博頁面都可以抓取得到,但是博文,評論,轉發等基本的資料還是沒有問題的;
- 後續我也會把程式碼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();
}
}
}
我的部落格:CODE大全:www.codedq.net;業餘草:www.xttblog.com;愛分享:www.ndislwf.com或ifxvn.com。相關文章
- 微博爬蟲 java實現爬蟲Java
- 爬蟲模擬登入—OAUTH的詳解爬蟲OAuth
- Python實現微博爬蟲,爬取新浪微博Python爬蟲
- 01、部落格爬蟲爬蟲
- [python爬蟲] 正規表示式使用技巧及爬取個人部落格例項Python爬蟲
- 為爬蟲獲取登入cookies: 使用Charles和requests模擬微博登入爬蟲Cookie
- 分享5個爬蟲專業部落格網站爬蟲網站
- [雪峰磁針石部落格]python爬蟲cookbook1爬蟲入門Python爬蟲
- 業餘草分享大量IT資料免費領!
- 業餘草,Java新人入職——配置環境及安裝開發工具(總結)Java
- 黑馬部落格——詳細步驟(二)專案功能的實現之登入功能
- SSH免密登入詳解
- python爬蟲爬取csdn部落格專家所有部落格內容Python爬蟲
- 部落格園記錄:汽車引數爬蟲爬蟲
- python 爬蟲——登入知乎Python爬蟲
- SSH 實現免密登入
- Python3 大型網路爬蟲實戰 002 --- scrapy 爬蟲專案的建立及爬蟲的建立 --- 例項:爬取百度標題和CSDN部落格Python爬蟲
- 介紹Linux小技巧,如何實現免密碼登入Linux密碼
- 【爬蟲工具】下載部落格轉成Markdown的形式爬蟲
- 每天一個爬蟲-learnku我的部落格列表爬蟲
- 單點登入的原理、實現、及技術方案比較詳解
- 爬蟲實戰(一):爬取微博使用者資訊爬蟲
- 【爬蟲】利用Python爬蟲爬取小麥苗itpub部落格的所有文章的連線地址並寫入Excel中(2)爬蟲PythonExcel
- 關於接入微博登入的程式碼實現
- Python 網路爬蟲入門詳解Python爬蟲
- 【爬蟲】利用Python爬蟲爬取小麥苗itpub部落格的所有文章的連線地址(1)爬蟲Python
- 業餘草尚矽谷Java視訊教程_SpringBoot視訊教程免費下載JavaSpring Boot
- Python爬蟲入門,8個常用爬蟲技巧盤點Python爬蟲
- node爬蟲進階之——登入爬蟲
- 業餘草談設計模式設計模式
- 部落格園登入請求分析
- 爬蟲進階:反反爬蟲技巧爬蟲
- 02-個人部落格筆記-登入註冊介面的實現筆記
- 部落格後臺登入,使用者,說說等功能實現
- 爬蟲實戰(二):Selenium 模擬登入並爬取資訊爬蟲
- 為爬蟲獲取登入cookies:登入的恩恩怨怨爬蟲Cookie
- 爬蟲系列 | 6、詳解爬蟲中BeautifulSoup4的用法爬蟲
- 爬蟲實戰(三):微博使用者資訊分析爬蟲