【爬蟲(chóng)成長(zhǎng)之路】(八)【大眾點(diǎn)評(píng)】APP爬蟲(chóng)

本系列文章共十篇:

【爬蟲(chóng)成長(zhǎng)之路】(一)爬蟲(chóng)系列文章導(dǎo)讀
【爬蟲(chóng)成長(zhǎng)之路】(二)各篇需要用到的庫(kù)和工具
【爬蟲(chóng)成長(zhǎng)之路】(三)【大眾點(diǎn)評(píng)】selenium爬蟲(chóng)
【爬蟲(chóng)成長(zhǎng)之路】(四)【大眾點(diǎn)評(píng)】selenium登錄+requests爬取數(shù)據(jù)
【爬蟲(chóng)成長(zhǎng)之路】(五)【大眾點(diǎn)評(píng)】瀏覽器掃碼登錄+油猴直接爬取數(shù)據(jù)
【爬蟲(chóng)成長(zhǎng)之路】(六)【大眾點(diǎn)評(píng)】mitmproxy修改HttpOnly字段獲取完整cookie+requests請(qǐng)求數(shù)據(jù)
【爬蟲(chóng)成長(zhǎng)之路】(七)【大眾點(diǎn)評(píng)】PC微信小程序+requests爬取數(shù)據(jù)
【爬蟲(chóng)成長(zhǎng)之路】(八)【大眾點(diǎn)評(píng)】安卓APP爬蟲(chóng)

本章標(biāo)題是安卓APP爬蟲(chóng)竞滓,說(shuō)實(shí)話对雪,如果爬蟲(chóng)的攻防對(duì)抗升級(jí)到了APP層面笋婿,這差不多是爬蟲(chóng)的最高形態(tài)了,之所以會(huì)升級(jí)到APP層面佳励,如果有把前面的文章中的實(shí)驗(yàn)都做一遍,就不難發(fā)現(xiàn),雖然爬蟲(chóng)能爬取數(shù)據(jù)熊尉,但是爬不了多少數(shù)據(jù)就會(huì)被封膏潮,所以如果需要更多的數(shù)據(jù)锻狗,就不得不轉(zhuǎn)移到APP這個(gè)層面來(lái)。APP能夠爬取數(shù)據(jù)的前提是目標(biāo)APP不強(qiáng)制要求登錄焕参,如果強(qiáng)制要求登錄轻纪,那APP端也爬不動(dòng)。幸運(yùn)的是大眾點(diǎn)評(píng)APP并沒(méi)有強(qiáng)制要求登錄叠纷,所以我們可以從APP端入手刻帚。

本文需要用到的工具:FiddlerIDA涩嚣,JADX-gui崇众、fridaobjection航厚、已root安卓手機(jī)或安卓模擬器顷歌、大眾點(diǎn)評(píng)APP v10.41.15...
本文需要用到的庫(kù):requests...

這里對(duì)這幾個(gè)工具作個(gè)簡(jiǎn)單的介紹:

  1. Fiddler:HTTP/HTTPS抓包軟件,可重放請(qǐng)求幔睬,也可修改請(qǐng)求和響應(yīng)眯漩;
  2. IDA:反編譯工具,可以將二進(jìn)制文件反編譯成匯編或偽代碼麻顶,還可以動(dòng)態(tài)調(diào)試赦抖,功能十分強(qiáng)大舱卡;
  3. JADX-gui:可以將APK直接反編譯成JAVA代碼,絕大部分都能還原回來(lái)队萤,最重要的是可以Go to Definition灼狰,還可以查找引用,這個(gè)非常好用浮禾;
  4. frida:一款可以使用JS進(jìn)行HOOK的全平臺(tái)的框架交胚,使用非常簡(jiǎn)單,無(wú)需配置盈电;
  5. objection:基于frida開(kāi)發(fā)的使用命令行進(jìn)行HOOK的工具蝴簇,不用寫(xiě)代碼就能完成HOOK工作,十分好用匆帚。

如果frida和objection不會(huì)使用的同學(xué)熬词,可以參考下官方文檔和下面的這些文章:

  1. Frida 安裝和使用
  2. FRIDA系列文章
  3. 實(shí)用FRIDA進(jìn)階:內(nèi)存漫游、hook anywhere吸重、抓包
  4. frida入門總結(jié)
  5. 一篇文章帶你領(lǐng)悟Frida的精髓(基于安卓8.1)
  6. 雷電模擬器安裝frida-server教程
  7. Frida Android hook
  8. objection 常用方法
  9. 記一次APP加密通信后的分析過(guò)程
  10. Frida構(gòu)造Java函數(shù)所需的Map<String, List<String>>參數(shù)

一互拾、需求分析

這一篇總共需要爬取兩個(gè)頁(yè)面的數(shù)據(jù),分別是:

  1. 某用戶的評(píng)論詳情頁(yè)面

二嚎幸、獲取目標(biāo)頁(yè)面的URL

由于是需要從APP入手颜矿,第一步還是抓包,對(duì)普通應(yīng)用程序來(lái)說(shuō)嫉晶,只需要在手機(jī)上安裝好證書(shū)骑疆,連上電腦的WIFI,就能在PC端抓到手機(jī)的包替废,但是在抓大眾點(diǎn)評(píng)的時(shí)候你會(huì)發(fā)現(xiàn)可以抓到一些提交日志信息相關(guān)的包箍铭,實(shí)際上有數(shù)據(jù)的包一個(gè)也抓不到,這是什么原因呢椎镣?

面對(duì)PC抓不到手機(jī)的包诈火,在手機(jī)和PC配置都沒(méi)有出錯(cuò)的前提下,一般有以下這兩種情況:

  1. APP檢測(cè)到了使用代理了状答,直接拒絕工作冷守,此時(shí)APP上不會(huì)更新任何數(shù)據(jù);
  2. APP所采用的不是HTTP/HTTPS通信協(xié)議,導(dǎo)致Fiddler抓不到包,但是使用wireshark可以看到明顯是有產(chǎn)生數(shù)據(jù)包的死宣。

具體是哪一種造锅,在使用過(guò)程中,我們可以看到大眾點(diǎn)評(píng)的APP明明有新的評(píng)論數(shù)據(jù)刷新出來(lái),但是Fiddler里面確是一條數(shù)據(jù)都沒(méi)有风喇,而使用wireshark的時(shí)候卻可以明顯看到有數(shù)據(jù)包產(chǎn)生怪与,這說(shuō)明大眾點(diǎn)評(píng)APP使用的不是HTTP/HTTPS協(xié)議堪唐,而是使用了TCP協(xié)議或者自定義的協(xié)議巡语。在逆向大眾點(diǎn)評(píng)的APP之前,我們先去search一下淮菠,在 美團(tuán)點(diǎn)評(píng)移動(dòng)網(wǎng)絡(luò)優(yōu)化實(shí)踐看到了這張圖男公,這也印證了大眾點(diǎn)評(píng)并不是在使用HTTP通信。從實(shí)戰(zhàn)報(bào)告中我們也可以知道合陵,他們提供了3種方案用于完成通信枢赔。

美團(tuán)完整的網(wǎng)絡(luò)通道拓?fù)鋱D

美團(tuán)技術(shù)團(tuán)隊(duì):圖中網(wǎng)絡(luò)通道SDK包含了三大通信通道:

  1. CIP通道:CIP通道就是上文中提到的自建代理長(zhǎng)連通道。CIP是China Internet Plus的縮寫(xiě)拥知,為美團(tuán)點(diǎn)評(píng)集團(tuán)的注冊(cè)英文名稱踏拜。App中絕大部分的請(qǐng)求通過(guò)CIP通道中的TCP子通道與長(zhǎng)連服務(wù)器(CIP Connection Server)通信,長(zhǎng)連服務(wù)器將收到的請(qǐng)求代理轉(zhuǎn)發(fā)到業(yè)務(wù)服務(wù)器(API Server)低剔。由于TCP子通道在一些極端情況下可能會(huì)無(wú)法工作速梗,我們?cè)贑IP通道中額外部署了UDP子通道和HTTP子通道,其中HTTP子通道通過(guò)公網(wǎng)繞過(guò)長(zhǎng)連服務(wù)器與業(yè)務(wù)服務(wù)器進(jìn)行直接請(qǐng)求襟齿。CIP通道的平均端到端成功率目前已達(dá)99.7%姻锁,耗時(shí)平均在350毫秒左右。
  2. WNS通道:出于災(zāi)備的需要猜欺,騰訊的WNS目前仍被包含在網(wǎng)絡(luò)通道SDK中位隶。當(dāng)極端情況發(fā)生,CIP通道不可用時(shí)替梨,WNS通道還可以作為備用的長(zhǎng)連替代方案钓试。
  3. HTTP通道:此處的HTTP通道是在公網(wǎng)直接請(qǐng)求API Server的網(wǎng)絡(luò)通道。出于長(zhǎng)連通道重要性的考慮副瀑,上傳和下載大數(shù)據(jù)包的請(qǐng)求如果放在長(zhǎng)連上進(jìn)行都有可能導(dǎo)致長(zhǎng)連通道的擁堵,因此我們將CDN訪問(wèn)恋谭、文件上傳和頻繁的日志上報(bào)等放在公網(wǎng)利用HTTP短連進(jìn)行請(qǐng)求糠睡,同時(shí)也減輕代理長(zhǎng)連服務(wù)器的負(fù)擔(dān)。

到此為止我們有兩種方法可以完成我們的爬蟲(chóng):

  1. 使用TCP通道進(jìn)行爬取疚颊,需要逆向APP狈孔;
  2. 使用HTTP通道;

但是在TCP通道可用的情況下材义,APP是不會(huì)采用HTTP通信的(上文提到均抽,HTTP通常只用來(lái)上傳日志和下載圖片),有數(shù)據(jù)的內(nèi)容不會(huì)通過(guò)HTTP進(jìn)行傳輸其掂,所以我們可以想辦法阻塞掉TCP通道油挥。一開(kāi)始我選用的方案是在wireshark中查看目標(biāo)服務(wù)器的IP,然后在windows的防火墻里封禁相應(yīng)的IP,在嘗試多次之后深寥,方向他的IP實(shí)在是太多了攘乒。。惋鹅。大概禁了七八個(gè)吧则酝,然后發(fā)現(xiàn)APP上的數(shù)據(jù)不更新了,可能HTTP也是走的這些通道吧闰集,或者根本就沒(méi)降級(jí)到HTTP沽讹。。武鲁。這個(gè)我不確定妥泉,沒(méi)仔細(xì)看,有興趣的同學(xué)可以自行驗(yàn)證洞坑。

那如果需要抓到HTTP的包盲链,就必須對(duì)APP進(jìn)行逆向了,這里我使用的是JADX-gui這款軟件迟杂,電腦內(nèi)存要大些才好刽沾,真的是有多少內(nèi)存就吃多少,最少也留個(gè)3G的空閑內(nèi)存吧排拷。 這里怎么逆向APP我就不展開(kāi)講解了侧漓,網(wǎng)上有蠻多的教程,如果不會(huì)的話自行去百度一下吧监氢。這個(gè)說(shuō)個(gè)比較重要的思路布蔗,如果老版本的APP可以使用,那就盡量從老版本的APP開(kāi)始入手浪腐,這樣遇到各種加密和混淆的概率低一些纵揍,也方便入手。

在逆向完成后议街,能夠看到j(luò)adx-gui反編譯后的JAVA代碼了泽谨,現(xiàn)在要做的就是根據(jù)一些關(guān)鍵字去定位到相應(yīng)的代碼,比如我們知道點(diǎn)評(píng)的域名信息是**.dianping.com/**特漩,就可以從中搜索了吧雹,定位代碼和理清其邏輯關(guān)系,這一步是這篇教程中最費(fèi)時(shí)的一步涂身。這里具體如何操作雄卷,我就不演示了,反正挺費(fèi)時(shí)間的蛤售,需要有耐心丁鹉。

下面給幾個(gè)關(guān)鍵的步驟妒潭,以便讀者可以快速的獲取到相關(guān)的接口:
我們知道傳輸?shù)臄?shù)據(jù)都是經(jīng)過(guò)加密了的,所以我們可以從加密函數(shù)入手鳄炉,利用frida來(lái) HOOK相應(yīng)的接口即可杜耙,

// 源程序包路徑:com.dianping.nvnetwork.tunnel.tool.c
  private static Key a(byte[] bArr) throws Exception {
        Object[] objArr = {bArr};
        ChangeQuickRedirect changeQuickRedirect = a;
        if (PatchProxy.isSupport(objArr, null, changeQuickRedirect, true, "862c9f994092c26eb84b9d83c437a3ac", RobustBitConfig.DEFAULT_VALUE)) {
            return (Key) PatchProxy.accessDispatch(objArr, null, changeQuickRedirect, true, "862c9f994092c26eb84b9d83c437a3ac");
        }
        return SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(bArr));
    }

    public static byte[] a(byte[] bArr, byte[] bArr2) throws Exception {
        Object[] objArr = {bArr, bArr2};
        ChangeQuickRedirect changeQuickRedirect = a;
        if (PatchProxy.isSupport(objArr, null, changeQuickRedirect, true, "f7a471aa6ce2c3ab09d975bac8d088d0", RobustBitConfig.DEFAULT_VALUE)) {
            return (byte[]) PatchProxy.accessDispatch(objArr, null, changeQuickRedirect, true, "f7a471aa6ce2c3ab09d975bac8d088d0");
        }
        Key a2 = a(bArr2);
        Cipher instance = Cipher.getInstance("DES");
        instance.init(2, a2);
        return instance.doFinal(bArr);
    }

    // 通過(guò)對(duì)所有的加密函數(shù)進(jìn)行HOOK,最終發(fā)現(xiàn)所有的URL相關(guān)加密都會(huì)經(jīng)過(guò)這個(gè)函數(shù)拂盯,因此HOOK這個(gè)函數(shù)
    public static byte[] b(byte[] bArr, byte[] bArr2) throws Exception {
        Object[] objArr = {bArr, bArr2};
        ChangeQuickRedirect changeQuickRedirect = a;
        if (PatchProxy.isSupport(objArr, null, changeQuickRedirect, true, "791dd5351de3cfac5e41752ae0c020dc", RobustBitConfig.DEFAULT_VALUE)) {
            return (byte[]) PatchProxy.accessDispatch(objArr, null, changeQuickRedirect, true, "791dd5351de3cfac5e41752ae0c020dc");
        }
        Key a2 = a(bArr2);
        Cipher instance = Cipher.getInstance("DES");
        instance.init(1, a2);
        return instance.doFinal(bArr);
    }

//frida js HOOK 代碼
function bin2string(array){
    var result = "";
    for(var i = 0; i < array.length-1; ++i){
        result+= (String.fromCharCode(array[i]));
    }
    return result;
}

function main(){
    Java.perform(function x() {

        var c = Java.use("com.dianping.nvnetwork.tunnel.tool.c");

        c.b.overload("[B","[B").implementation = function(bArr, bArr2){
            var result = this.b(bArr, bArr2);
            var str = bin2string(bArr);
            if(str.includes("pragma-unionid") && str.includes("pragma-dpid") && str.includes("mainid")){
                console.log(str);// 打印傳入待加密的參數(shù)
            }
            return result;
        }
    }
}

setImmediate(main)

通過(guò)HOOK DES加密函數(shù)佑女,最終找到相關(guān)接口及Header信息,Header及其他被加密的信息如下:

{
        "m": "GET",
        "h": {
            "pragma-device": "4***3",//IMEI
            "network-type": "wifi",
            "pragma-os": "MApi 1.4 (com.dianping.v1 10.36.3 om_sd_** NXT-DL00; Android 8.0)",//類似于UserAgent
            "pragma-uuid": "11139***2",//UUID可用UUID生成算法生成
            "pragma-unionid": "158bca***83",// 需要向服務(wù)器請(qǐng)求獲得
            "User-Agent": "MApi 1.4 (com.dianping.v1 10.36.3 om_sd_** NXT-DL00; Android 8.0)",
            "pragma-dpid": "158bca***83",// 需要向服務(wù)器請(qǐng)求獲得
            "picasso": "no-js",
            "M-SHARK-TRACEID": "11158b***a"http://本地生成
        },
        "u": "http://***", //  目標(biāo)URL
        "i": "11***4"http:// 請(qǐng)求序號(hào)
    }

其實(shí)到這里雖然拿到了這些重點(diǎn)參數(shù)谈竿,但是此時(shí)仍然是建立了TCP連接而不是HTTP連接团驱,所以,直接用這些數(shù)據(jù)去請(qǐng)求的話必然是失敗的空凸,這里就要迫使APP降級(jí)采用HTTP連接了嚎花,這里參考了github的一位大佬的代碼,我找了好一會(huì)沒(méi)找到入口在哪呀洲。紊选。。

//frida JS代碼道逗,繞過(guò)CIP和WNS代理兵罢,直接走HTTP通道
var nvnetwork_g = Java.use("com.dianping.nvnetwork.g");
nvnetwork_g.g.overload().implementation = function(){
    console.log("----------------------------- Hook g()---------------------------");
    return 3;
}

這樣就可以使用Fiddler抓包了,最終結(jié)果如下滓窍。

評(píng)論詳情URL接口:

# 評(píng)論詳情URL接口:
http://mapi.dianping.com/mapi/note/getfeedcontent.bin?***

三卖词、請(qǐng)求頭Header、URL參數(shù)解析

評(píng)論URL參數(shù)

這里有兩個(gè)重點(diǎn)參數(shù)吏夯,分別是mainidcx此蜈,這個(gè)cx和小程序里的cx是不一樣的,這點(diǎn)需要注意一下噪生。

序號(hào) 名稱 說(shuō)明
1 mainid *** 評(píng)論ID
2 feedtype 1 評(píng)論類型
3 lng *** 經(jīng)度
4 lat *** 緯度
5 displaypattern 2 顯示模式裆赵,固定
6 bubblepagetype null -,固定
7 cx *** 加密生成的參數(shù)杠园,帶了時(shí)間戳顾瞪,重點(diǎn)參數(shù)
8 pagecityid 1 -,固定
9 optimus_partner 76 -抛蚁,固定
10 optimus_risk_level 71 風(fēng)險(xiǎn)等級(jí),固定
11 optimus_code 10 -惕橙,固定
12 picsize ... 一些分辨率信息瞧甩,可適當(dāng)調(diào)整

Header 參數(shù)
看上面的注釋就好了,就不重復(fù)說(shuō)明了弥鹦,說(shuō)下重點(diǎn)參數(shù)肚逸。
在Header里有幾個(gè)重點(diǎn)參數(shù)爷辙,分別是:pragma-devicepragma-os朦促、pragma-uuid膝晾、pragma-unionidpragma-dpid务冕、M-SHARK-TRACEID血当、i

序號(hào) 名稱 說(shuō)明
1 pragma-device 4***3 設(shè)備的IMEI
2 pragma-os MApi 1.4 (com.dianping.v1 10.36.3 om_sd_** NXT-DL00; Android 8.0) 類似于UA
3 pragma-uuid 11139***2 本地生成
4 pragma-unionid 158bca***83 需要提交請(qǐng)求換取
5 pragma-dpid 158bca***83 pragma-unionid
6 M-SHARK-TRACEID 11158b***a 本地算法生成禀忆,需要看源碼
7 i 11***4 請(qǐng)求序號(hào)臊旭,依此遞增

四、請(qǐng)求頭Header箩退、URL參數(shù)構(gòu)造

首先構(gòu)造Header里的參數(shù)离熏,pragma-deviceIMEI,這個(gè)比較容易構(gòu)造戴涝,pragma-os類似于UserAgent滋戳,也比較好構(gòu)造,pragma-uuid是用UUID算法生成的啥刻,剩下的pragma-unionidpragma-dpid其實(shí)可以一致奸鸯,或者pragma-dpid可以留空,那最關(guān)鍵的就是獲取到pragma-unionidM-SHARK-TRACEID了郑什,那如何構(gòu)造pragma-unionidM-SHARK-TRACEID呢府喳?這就需要看源碼了,經(jīng)過(guò)一番定位查找蘑拯,結(jié)果如下钝满。

unionid參數(shù)生成,JAVA源碼:

// 源程序獲取unionid函數(shù)申窘,包路徑:com.meituan.android.common.unionid.oneid.OneIdHelper
private static void getOneIdByNetwork(final DeviceInfo deviceInfo, final OneIdNetworkHandler oneIdNetworkHandler, final List<IOneIdCallback> list, String str, final String str2) {
    Object[] objArr = {deviceInfo, oneIdNetworkHandler, list, str, str2};
    ChangeQuickRedirect changeQuickRedirect2 = changeQuickRedirect;
    if (PatchProxy.isSupport(objArr, null, changeQuickRedirect2, true, "24218b6da0953568d0f2ba37b8d38f2d", RobustBitConfig.DEFAULT_VALUE)) {
        PatchProxy.accessDispatch(objArr, null, changeQuickRedirect2, true, "24218b6da0953568d0f2ba37b8d38f2d");
    } else if (deviceInfo == null || oneIdNetworkHandler == null) {
        Log.e(TAG, "getoneIdByNetwork: one of the parameters is null");
    } else {
        _oneid_request(deviceInfo, oneIdNetworkHandler, list, str, str2, 1);
        try {
            MonitorManager.addEvent(deviceInfo.stat, "oaid", 0, true);
            OaidManager.getInstance().getOaid(sContext, new OaidCallback2() {
                /* class com.meituan.android.common.unionid.oneid.OneIdHelper.AnonymousClass1 */
                public static ChangeQuickRedirect changeQuickRedirect;

                @Override // com.meituan.android.common.unionid.oneid.oaid.OaidCallback
                public void onSuccuss(boolean z, String str, boolean z2) {
                }
...

public static void _oneid_request(DeviceInfo deviceInfo, OneIdNetworkHandler oneIdNetworkHandler, List<IOneIdCallback> list, String str, String str2, int i) {
    String request = OneIdNetworkHandler.request(sContext, str, deviceInfo, str2, i);
    if (!TextUtils.isEmpty(request)) {
        if (!TextUtils.isEmpty(lastOneid) && !lastOneid.equals(request)) {
            JSONObject jSONObject = new JSONObject();
            try {
                jSONObject.put("req", deviceInfo.toString());
                jSONObject.put("url", str);
                jSONObject.put("new", request);
                jSONObject.put("old", lastOneid);
                LogMonitor.watch(LogMonitor.ONEID_CHANGE_TAG, "", jSONObject);
            } catch (Exception e) {
                c.a(e);
                e.printStackTrace();
            }
        }
...

unionid 參數(shù)生成弯蚜,python 版本:

import time
import random
import uuid
import json
import requests


class UnionidHelper:
    '''
    UnionidHelper 是用來(lái)生成獲取unionid所需參數(shù)的
    [注意]初始化一次只能獲取一次里面的參數(shù),否則參數(shù)將是重復(fù)的剃法,無(wú)法生成新的unionid
    '''

    def __init__(self, device_info=None, brand=None,model=None,app_source=None,app_version=None,imei1=None,imei2=None,
                androidId=None,osName=None,os_version=None,serialNumber=None,bluetoothMac=None,wifiMac=None):
        '''
        device_info 字典中含有其他參數(shù)時(shí)碎捺,則其他參數(shù)可以不用傳入
        '''
        self.brand = brand if 'brand' not in device_info else device_info['brand']
        self.model = model if 'model' not in device_info else device_info['model']
        self.app_source = app_source if 'app_source' not in device_info else device_info['app_source']  # 應(yīng)用來(lái)源(不帶前綴om_sd_)
        self.app_version = app_version if 'app_version' not in device_info else device_info['app_version']
        self.imei1 = imei1 if 'imei1' not in device_info else device_info['imei1']
        self.imei2 = imei2 if 'imei2' not in device_info else device_info['imei2']
        self.androidId = androidId if 'androidId' not in device_info else device_info['androidId'] # len = 16
        self.osName = osName if 'osName' not in device_info else device_info['osName'] # 手機(jī)中的版本號(hào)
        self.os_version = os_version if 'os_version' not in device_info else device_info['os_version']  # android version
        self.serialNumber = serialNumber if 'serialNumber' not in device_info else device_info['serialNumber']
        self.bluetoothMac = bluetoothMac if 'bluetoothMac' not in device_info else device_info['bluetoothMac']
        self.wifiMac = wifiMac if 'wifiMac' not in device_info else device_info['wifiMac']

        self.localid = LocalId()
        
        self.url_unionid_register = 'http://api-unionid.meituan.com/unionid/android/register'

        self.header = {
            "Accept-Charset": "UTF-8",
            "uuidRequestId": self.localid.gen_localId(),
            "uuidSessionId": self.localid.gen_localId(),
            "retrofit_exec_time": str(int(time.time()*1000)),
            "Accept-Encoding": "gzip",
            "Content-Type": "application/json;charset=UTF-8",
            "Content-Length": '0',
            "User-Agent": f"Dalvik/2.1.0 (Linux; U; Android {os_version}; {self.model} Build/{osName})",  # 待修改
            "Host": "api-unionid.meituan.com",
            "Connection": "Keep-Alive"
        }

        self.logInfo = {
            "processName": "com.dianping.v1",
            "events": [
            {
                "markKey": "buCallStart",
                "markValue": 121,
                "incrementalId": 0,
                "opName": 0,
                "threadName": "Aurora#2",
                "timestamp": int(time.time()*1000),
                "uptimeMillis": 37244436+random.randint(-5,10)
            },
            {
                "markKey": "dpid",
                "markValue": 130,
                "incrementalId": 1,
                "opName": 0,
                "threadName": "Aurora#2",
                "timestamp": int(time.time()*1000),
                "uptimeMillis": 37245536+random.randint(-5,10)
            },
            ...
            ],
            "rtt": {
            "sessionId": ""
            }
        }

        self.data = {
            "appInfo": {
                "app": "com.dianping.v1",
                "version": self.app_source,
                "appName": "dianping_nova",
                "sdkVersion": "1.16.11",
                "userId": "",
                "downloadSource": f"om_sd_{self.app_source}"     # 待修改
            },
            "idInfo": {
                "localId": self.localid.gen_localId(),
                "unionId": "",
                "uuid": "",
                "dpid": "",
                "requiredId": str(random.randint(1,2))       # 1:registerOrUpdate, 2:startDpid, 4:registerOrUpdateUuid
            },
            "logInfo": json.dumps(self.logInfo),
            "environmentInfo": {
                "platform": "android",
                "osName": self.osName,    # 待修改
                "osVersion": self.os_version,
                "clientType": "6"
            },
            "deviceInfo": {
                "keyDeviceInfo": {
                "imei1": self.imei1,
                "imei2": self.imei2,
                "meid": "",
                "androidId": self.androidId,  # 6db88d1534ef1089
                "oaid": "",
                "appid": {
                    "share": "",    # raw: AndroidMuMu$6db88d1534ef1089
                    "local": {
                    "oldid": f"{self.brand}{self.model}${androidId}",  # raw: AndroidMuMu$6db88d1534ef1089
                    "newid": ""
                    }
                }
                },
                "secondaryDeviceInfo": {
                "serialNumber": self.serialNumber,   # ZX1G42CPJD
                "bluetoothMac": self.bluetoothMac.lower(),
                "wifiMac": self.wifiMac.lower(),
                "simulateId": "",
                "uuid": uuid.uuid4().__str__()
                },
                "brandInfo": {
                "brand": self.brand,
                "deviceModel": self.model
                }
            },
            "communicationInfo": {
                "jntj": "",
                "jddje": "",
                "nop": "unknown"
            },
            "mark": json.dumps({
                "dpid": 9,
                "unionId": 9,
                "appid_share": 131,
                "appid_local": 130
            })
        }


if __name__ == '__main__':
    unionid_helper = UnionidHelper()

    session = requests.session()
    session.headers.clear()
    session.headers.update(unionid_helper.header)
    res = session.post(url=unionid_helper.url_unionid_register, data=json.dumps(unionid_helper.data))

    res_data = json.loads(res.text)
    code = res_data['code']
    if code == 0:
        print(res_data['data']['unionId'])
    print(res.content)

M-SHARK-TRACEID參數(shù)生成算法, python版本:

def gen_M_SHARK_TRACEID(pragma_unionid):
    '''
    M-SHARK-TRACEID : 113888...6253a7a3161510...360d09cda
    結(jié)構(gòu):11 388...62 53a7a3 161510...360 d09cda
         [fix] [unionid] [uuid(pre-6bit)] [timestamp(ms)] [uuid(pre-6bit)]
    '''
    prefix = '11'

    uuid_pre = uuid.uuid4().__str__()[:6]
    uuid_end = uuid.uuid4().__str__()[:6]
    timestamp = str(int(time.time()*1000))
    return prefix + pragma_unionid + uuid_pre + timestamp + uuid_end

localId 生成算法(unionid 獲取時(shí)需要這個(gè)參數(shù))贷洲,JAVA源碼:

// 包路徑:com.meituan.android.common.unionid.oneid.util.TempIDGenerator
public class TempIDGenerator {
    public static String generate() {
        SecureRandom secureRandom = new SecureRandom();
        byte[] bArr = new byte[50];
        byte[] bArr2 = new byte[24];
        byte[] bArr3 = new byte[24];
        secureRandom.nextBytes(bArr2);
        secureRandom.nextBytes(bArr3);
        for (int i = 0; i < bArr2.length; i++) {
            bArr2[i] = (byte) (bArr2[i] & 15);
            bArr3[i] = (byte) (bArr3[i] & 15);
        }
        System.arraycopy(bArr2, 0, bArr, 0, bArr2.length);
        System.arraycopy(bArr3, 0, bArr, 26, bArr3.length);
        handleBytes(bArr2);
        handleBytes(bArr3);
        byte checker = getChecker(bArr2);
        byte checker2 = getChecker(bArr3);
        bArr[24] = checker;
        bArr[25] = checker2;
        return byteArrayToHexString(bArr);
    }

    private static void handleBytes(byte[] bArr) {
        for (int i = 0; i < bArr.length; i += 2) {
            bArr[i] = (byte) (bArr[i] * 2);
            while (bArr[i] >= 10) {
                bArr[i] = (byte) ((bArr[i] % 10) + ((bArr[i] / 10) % 10));
            }
        }
    }

    private static byte getChecker(byte[] bArr) {
        int i = 0;
        for (byte b : bArr) {
            i += b;
        }
        byte b2 = (byte) (10 - ((byte) (i % 10)));
        if (b2 == 10) {
            return 0;
        }
        return b2;
    }

    private static String byteArrayToHexString(byte[] bArr) {
        StringBuffer stringBuffer = new StringBuffer(bArr.length);
        for (byte b : bArr) {
            stringBuffer.append(Integer.toHexString(b));
        }
        return new String(stringBuffer);
    }
}

localId生成算法(unionid 獲取時(shí)需要這個(gè)參數(shù))收厨,python版本:

import time
import random
import uuid
import json
import requests


class LocalId:
    '''
    大眾點(diǎn)評(píng)localId生成算法(對(duì)應(yīng):TempIDGenerator.generate())
    '''
    def __init__(self):
        self.bArr2 = None  # [random.randint(0,15) for i in range(24)]
        self.bArr3 = None  # [random.randint(0,15) for i in range(24)]
        self.bArr = None  # self.bArr2 + [0,0]+ self.bArr3

    def handleBytes(self, bArr):
        '''
        func:將偶數(shù)位的數(shù)先乘2再變成小于10的數(shù)
        '''
        for i, v in enumerate(bArr):
            if i % 2 != 0:
                continue
            bArr[i] *= 2
            while bArr[i] >= 10:
                bArr[i] = bArr[i] % 10 + bArr[i] // 10 % 10
        return bArr

    def getChecker(self, bArr):
        _sum = sum(bArr)
        b2 = 10 - _sum % 10
        return b2 if b2 != 10 else 0

    def gen_localId(self):
        self.bArr2 = [random.randint(0,15) for i in range(24)]
        self.bArr3 = [random.randint(0,15) for i in range(24)]
        self.bArr = self.bArr2 + [0,0]+ self.bArr3

        self.bArr2 = self.handleBytes(self.bArr2)
        self.bArr3 = self.handleBytes(self.bArr3)
        check2 = self.getChecker(self.bArr2)
        check3 = self.getChecker(self.bArr3)

        self.bArr[24] = check2
        self.bArr[25] = check3

        bArr_str = ''.join('{:1x}'.format(x) for x in self.bArr)
        return bArr_str

cx 生成算法,JAVA源碼:
省略了...

cx 生成算法优构,python版本:

由于需要用到的參數(shù)過(guò)多诵叁,這里就不貼出來(lái)了了,太影響閱讀體驗(yàn)了钦椭,就給個(gè)大致思路吧:

加密過(guò)程: 待加密字符串(str) -> 編碼(byte) -> des加密(byte) -> base64編碼(byte) -> url編碼(str) -> 密文(str)
解密過(guò)程: 密文(str) -> url解碼(str) -> base64解碼(byte) -> des解密(byte) -> 解碼成字符串(str)

生成cx時(shí)用到的DES加解密算法

from pyDes import des, CBC, PAD_PKCS5
import base64
import urllib.parse
import json
import uuid
import time


class DES_Encrypt:

    def __init__(self):
        # 秘鑰
        self.KEY = 'k***'

    def des_encrypt_byte(self, content):
        """
        DES 加密
        :param s: 原始字符串
        :return: 加密后字符串拧额,byte
        """
        secret_key = self.KEY  # 密碼
        iv = secret_key  # 偏移
        # secret_key:加密密鑰碑诉,CBC:加密模式,iv:偏移, padmode:填充
        des_obj = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
        # 返回為字節(jié)
        secret_bytes = des_obj.encrypt(content, padmode=PAD_PKCS5)
        # 返回為16進(jìn)制
        # return binascii.b2a_hex(secret_bytes)
        return secret_bytes

    def des_descrypt_byte(self, content):
        """
        DES 解密
        :param s: 加密后的字符串侥锦,16進(jìn)制
        :return:  解密后的字符串
        """
        secret_key = self.KEY
        iv = secret_key
        des_obj = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
        decrypt_str = des_obj.decrypt(content, padmode=PAD_PKCS5)
        return decrypt_str

到這里进栽,發(fā)起請(qǐng)求的全部重點(diǎn)參數(shù)其含義以及如何生成的就都清楚了,其中的重點(diǎn)參數(shù):pragma-unionid恭垦、pragma-dpid快毛、M-SHARK-TRACEIDcx署照、localId的生成算法都給出來(lái)了祸泪,現(xiàn)在就可以去發(fā)起請(qǐng)求了。

五建芙、response響應(yīng)解析

按照以往的WEB爬蟲(chóng)來(lái)說(shuō)没隘,能夠成功發(fā)起請(qǐng)求,爬蟲(chóng)基本上就算完成了禁荸,但是對(duì)于APP爬蟲(chóng)右蒲,尤其是這種做了大量加密的APP來(lái)說(shuō),其返回的response必然也會(huì)進(jìn)行相應(yīng)的加密赶熟,這里的解密也參考了另一位github大佬的代碼瑰妄,點(diǎn)評(píng)APP把解密的算法放到了so庫(kù)里面,所以這時(shí)候借助JADX-gui就行不通了映砖,我們先來(lái)看下JADX-gui中解出來(lái)的解密算法:

// 包路徑:com.dianping.util.NativeHelper
public class NativeHelper {
    public static final boolean a;

    private static native boolean a();

    public static native boolean nd(byte[] bArr, byte[] bArr2, byte[] bArr3, byte[] bArr4);

    public static native byte[] ndug(byte[] bArr, byte[] bArr2, byte[] bArr3);

    public static native boolean ne(byte[] bArr, byte[] bArr2, byte[] bArr3, byte[] bArr4);

    public static native byte[] nug(byte[] bArr);

    static {
        boolean z;
        b.a("1af3893fc7dcb0905311776314368f4d");
        try {
            if (!aa.a("nh", NativeHelper.class)) {
                System.loadLibrary(b.b("nh"));
            }
            z = a();
        } catch (Throwable th) {
            c.a(th);
            ab.c("failed to load native helper");
            z = false;
        }
        a = z;
    }
}

里面沒(méi)有算法的具體實(shí)現(xiàn)间坐,說(shuō)明其具體實(shí)現(xiàn)在so層,這時(shí)候就要用IDA來(lái)查看其使用的是什么算法了邑退。

在APP解壓出來(lái)的文件中竹宋,找到libnh.so這個(gè)文件(從System.loadLibrary(b.b("nh"))這里可以知道),查看其導(dǎo)出函數(shù)地技,再一頓操作轉(zhuǎn)成可讀性稍好的C代碼蜈七,這里明顯能從函數(shù)名看出來(lái)使用了AES/CBC加密,

ne加密函數(shù)
ndug解密函數(shù)

關(guān)于AES加解密莫矗,直接調(diào)庫(kù)就好了飒硅;關(guān)于AES的秘鑰和偏移,可以從JADX-gui反編譯后的文件中找到作谚,這里的思路是根據(jù)加解密函數(shù)去查找三娩,看哪里調(diào)用了這個(gè)函數(shù),傳入的參數(shù)是哪里來(lái)的妹懒,就能找到了尽棕。

到這里,很大一部分工作就完成了彬伦,如果實(shí)際測(cè)試一下可以發(fā)現(xiàn)滔悉,解密后的數(shù)據(jù)仍然不是我們需要的數(shù)據(jù),還需要進(jìn)一步進(jìn)行處理单绑,如果源碼追蹤是仔細(xì)一點(diǎn)回官,可以看到這一步就是做了一個(gè)變量名的映射,然而重新建立這個(gè)映射關(guān)系還是需要花點(diǎn)時(shí)間的搂橙,源程序的部分映射關(guān)系如下:

//包路徑:com.dianping.model.AdLog
...
@SerializedName("feedback")
public String a;
@SerializedName("impUrl")
public String b;
@SerializedName("clickUrl")
public String c;
@SerializedName("thirdpartyMonitorImpUrls")
public String[] d;
@SerializedName("thirdpartyMonitorClickUrls")
public String[] e;
@SerializedName("ext")
public String f;

...
public void writeToParcel(Parcel parcel, int i) {
    parcel.writeInt(2633);
    parcel.writeInt(this.isPresent ? 1 : 0);
    parcel.writeInt(35360);
    parcel.writeString(this.f);
    parcel.writeInt(31004);
    parcel.writeStringArray(this.e);
    parcel.writeInt(53501);
    parcel.writeStringArray(this.d);
    parcel.writeInt(3264);
    parcel.writeString(this.c);
    parcel.writeInt(43874);
    parcel.writeString(this.b);
    parcel.writeInt(7952);
    parcel.writeString(this.a);
    parcel.writeInt(-1);
}

重建映射關(guān)系后歉提,就能解析出完成的JSON數(shù)據(jù)了,下面給出一個(gè)簡(jiǎn)化版本区转。

注:

下面response解碼部分本來(lái)是不打算放出來(lái)的苔巨,一是原創(chuàng)不是我,二是影響閱讀體驗(yàn)废离,但問(wèn)的同學(xué)是在太多了侄泽,所以考慮下,還是放出來(lái)了蜻韭。

def decrypt_aes(key, iv, content):
    generator = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
    decrypt = generator.decrypt(content)
    return decrypt

def decode_aes(content, model=None):
    aes_data = decrypt_aes(key=key, iv=iv, content=content)
    ungzip_data = gzip.decompress(aes_data)
    return ungzip_data

# 請(qǐng)求數(shù)據(jù)并做AES解密悼尾,再做變量名的重映射
res = session.get(url_user_comment, proxies=None, timeout=20)
body = decode_aes(res.content)
if res.status_code == 200:
    res_data = json.dumps(decode_model(body), indent=4)
    res_data = json.loads(res_data)

像下面這種model文件是有相互依賴關(guān)系的,有時(shí)解析某個(gè)model內(nèi)的數(shù)據(jù)時(shí)會(huì)依賴其他model的數(shù)據(jù)肖方,這里最好就全部解析一遍闺魏。如果需要爬取的內(nèi)容較多,也需要編寫(xiě)多個(gè)類似于下面的model文件俯画,可以用程序來(lái)處理析桥,將javamodel轉(zhuǎn)為pythonmodel

# encoding: utf-8

from model import BaseModel, add_model

@add_model(0x9bc3)
class AdLog(BaseModel):
    
    field_map = {'a': 'feedback', 'b': 'impUrl', 'c': 'clickUrl', 'd': 'thirdpartyMonitorImpUrls', 'e': 'thirdpartyMonitorClickUrls', 'f': 'ext'}

    def j_flag_2633(self):
        """
        0xa49 -> :sswitch_0
        :return:
        """
        self.result[self.field_map.get('isPresent', 'isPresent')] = self.archive_d_b()
    def j_flag_35360(self):
        """
        0x8a20 -> :sswitch_1
        :return:
        """
        self.result[self.field_map.get('f', 'f')] = self.archive_d_g()
    def j_flag_31004(self):
        """
        0x791c -> :sswitch_2
        :return:
        """
        self.result[self.field_map.get('e', 'e')] = self.archive_d_n()
    def j_flag_53501(self):
        """
        0xd0fd -> :sswitch_3
        :return:
        """
        self.result[self.field_map.get('d', 'd')] = self.archive_d_n()
    def j_flag_3264(self):
        """
        0xcc0 -> :sswitch_4
        :return:
        """
        self.result[self.field_map.get('c', 'c')] = self.archive_d_g()
    def j_flag_43874(self):
        """
        0xab62 -> :sswitch_5
        :return:
        """
        self.result[self.field_map.get('b', 'b')] = self.archive_d_g()
    def j_flag_7952(self):
        """
        0x1f10 -> :sswitch_6
        :return:
        """
        self.result[self.field_map.get('a', 'a')] = self.archive_d_g()
下面的model解碼程序是github大佬的艰垂,本來(lái)只能兼容比較舊的版本泡仗,我修改后能兼容到我測(cè)試所用的版本,之后的版本沒(méi)有繼續(xù)測(cè)試了材泄,沒(méi)有大版本更新的話是可以不作修改繼續(xù)使用的沮焕,具體情況請(qǐng)自行測(cè)試或回退幾個(gè)版本測(cè)試。
# encoding: utf-8

import struct
import os
from io import BytesIO
import logging
import time

flag_model_map = {}


class BaseModel:
    field_map = {}

    def __init__(self, data):
        self.result = {}
        if isinstance(data, BytesIO):
            self.data = data
            self.raw_data = data
        else:
            self.raw_data = data
            self.data = BytesIO(data)

    def unpack(self, fmt, stream):
        size = struct.calcsize(fmt)
        buf = stream.read(size)
        try:
            return struct.unpack(fmt, buf)
        except struct.error as e:
            logging.error("數(shù)據(jù)不全:{}".format(buf))

    def main(self):
        self.result = self.archive_d_a_archive_c()
        return self.result

    def decode(self):
        while True:
            j_flag = self.archive_d_j()
            if j_flag < 1:
                break
            j_flag_func_name = 'j_flag_{}'.format(j_flag)
            if hasattr(self, j_flag_func_name):
                getattr(self, j_flag_func_name)()
            else:
                try:
                    self.archive_d_i()
                except ValueError as e:
                    logging.error("數(shù)據(jù)錯(cuò)誤不解析了 j_flag:{} 當(dāng)前model:{}".format(j_flag, self.__class__))
                    raise e

    def archive_d_b(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x54:
            data = 0x1
        elif flag in [0x46, 0x4e]:
            data = 0x0
        else:
            logging.error("archive_d_b拋錯(cuò):unable to read boolean")
            raise ValueError()
        logging.info("找到bool:{}".format(data))
        return data

    def archive_d_c(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x49:
            data, = self.unpack(">i", self.data)
        elif flag == 0x4e:
            data = 0x00
        else:
            logging.error("archive_d_c拋錯(cuò)")
            raise ValueError()
        logging.info("找到int:{}".format(data))
        return data

    def archive_d_d(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x4c:
            data, = self.unpack(">q", self.data)
        elif flag == 0x4e:
            data = 0x0
        else:
            logging.error("archive_d_d拋錯(cuò)")
            raise ValueError()
        logging.info("archive_d_d找到string:{}".format(data))
        return data

    def archive_d_j(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x4d:
            data, = self.unpack(">h", self.data)
            data &= 0xffff
        elif flag == 0x5a:
            data = 0x00
        else:
            logging.error("archive_d_j拋錯(cuò)")
            raise ValueError()
        logging.info("當(dāng)前model{}".format(self.__class__))
        return data

    def archive_d_e(self):
        flag, = self.unpack(">b", self.data)

        if flag == 0x44:
            data, = self.unpack(">d", self.data)
        elif flag == 0x4e:
            data = 0x0
        else:
            logging.error("archive_d_e拋錯(cuò)")
            raise ValueError()
        logging.info("當(dāng)前model{}".format(self.__class__))
        return data
 
    def archive_d_g(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x53:
            length, = self.unpack(">h", self.data)
            length = 0xffff & length
            data, = self.unpack(">{}s".format(length), self.data)
        elif flag == 0x42:
            length, = self.unpack(">i", self.data)
            # length = 0xffff & length
            data, = self.unpack(">{}s".format(length), self.data)
        elif flag == 0x4e:
            data = b""
        else:
            logging.error("archive_d_g拋錯(cuò)")
            raise ValueError()
        logging.info("找到string:{}".format(data.decode()))
        return data.decode()

    def archive_d_n(self):
        data = []
        flag, = self.unpack(">b", self.data)
        if flag == 0x4e:
            data = [""]
        elif flag == 0x41:
            length, = self.unpack(">h", self.data)
            length = 0xffff & length
            for i in range(length):
                data.append(self.archive_d_g())
        else:
            logging.error("archive_d_n拋錯(cuò)")
            raise ValueError()
        logging.info("找到string:{}".format(data))
        return data

    def archive_d_i(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x41:
            length, = self.unpack(">h", self.data)
            length = length & 0xffff
            for i in range(length):
                self.archive_d_i()
        elif flag == 0x44:
            self.unpack(">d", self.data)
        elif flag == 0x49:
            self.unpack(">i", self.data)
        elif flag == 0x4c:
            self.unpack(">q", self.data)
        elif flag == 0x4f:
            self.unpack(">h", self.data)
            while self.archive_d_j() > 0:
                self.archive_d_i()
        elif flag == 0x53:
            position, = self.unpack(">h", self.data)
            position = (position & 0xffff) + self.data.tell()
            self.data.seek(position)
        elif flag == 0x55:
            self.unpack(">i", self.data)
        elif flag in [0x46, 0x4e, 0x54, ]:
            pass
        elif flag in [0x42, 0x43, 0x45, 0x47, 0x48, 0x4a, 0x4b, 0x4d, 0x50, 0x51, 0x52]:
            raise ValueError("unable to skip object:")

    def archive_d_a_archive_c(self):
        flag, = self.unpack(">b", self.data)
        if flag == 0x4e:
            logging.info("創(chuàng)建了一個(gè)空對(duì)象")
            return {}
        elif flag == 0x4f:
            data, = self.unpack(">h", self.data)
            data &= 0xffff
            model_class = flag_model_map.get(data, None)
            if model_class:
                model_instance = model_class(self.data)
                model_instance.decode()
                return model_instance.result
            else:
                logging.error("archive_d_a_archive_c未找到此model:{}".format(hex(data)))
                # raise ValueError()
                # return f'"archive_d_a_archive_c未找到此model:{format(hex(data))}"'
                print(f'"archive_d_a_archive_c未找到此model:{format(hex(data))}"')
        else:
            logging.error("archive_d_a_archive_c拋錯(cuò)")
            raise ValueError()

    def archive_d_b_archive_c(self):
        result = []
        flag, = self.unpack(">b", self.data)
        if flag == 0x4e:
            logging.info("創(chuàng)建空對(duì)象")
            return []
        elif flag == 0x41:
            length, = self.unpack(">h", self.data)
            length = 0xffff & length
            for i in range(length):
                data = self.archive_d_a_archive_c()
                logging.info("創(chuàng)建了一個(gè)對(duì)象:{}".format(data))
                result.append(data)
            return result
        else:
            logging.error("拋錯(cuò)")
            raise ValueError()


def add_model(flag):
    def wrapper(cls):
        if flag_model_map.get(flag):
            # raise ValueError("model已存在:{}".format(flag))
            print("model已存在:{}".format(flag))
            return cls
        flag_model_map[flag] = cls
        return cls

    return wrapper


def decode_model(data):
    """

    :param data:
    :return: dict
    """
    if not isinstance(data, bytes):
        data = bytes.fromhex(data)
    logging.debug("需要解密的body:{}".format(data.hex()))
    basemodel = BaseModel(data)
    basemodel.main()
    result = basemodel.result
    return result


def import_all_model():
    all_list = os.listdir(os.path.dirname(__file__))
    for i in all_list:
        if "__" not in i and ".py" in i:
            __import__("model." + i.replace(".py", ""))

t1 = time.time()
print('正在加載model...')
import_all_model()
print(f'[{round(time.time()-t1, 2)}s] model加載完成!')

六拉宗、優(yōu)缺點(diǎn)分析

序號(hào) 優(yōu)點(diǎn) 缺點(diǎn)
1 程序運(yùn)行更快 參數(shù)構(gòu)造麻煩
2 - response解析麻煩
3 - 需要對(duì)APP進(jìn)行逆向
4 - 需要理清APP的各個(gè)模塊間的邏輯關(guān)系

七峦树、結(jié)語(yǔ)

這是APP爬蟲(chóng),對(duì)于新手來(lái)說(shuō)還是難度很大的旦事,對(duì)于有逆向APP經(jīng)驗(yàn)的同學(xué)來(lái)說(shuō)魁巩,這里復(fù)雜的可能就是理清邏輯關(guān)系了,文中給出了關(guān)鍵的思路和代碼姐浮,有了這些思路谷遂,相信對(duì)于想入門高階爬蟲(chóng)的同學(xué)來(lái)說(shuō),多少還是有點(diǎn)幫助的卖鲤。但是在此再次聲明肾扰,雖然APP爬蟲(chóng)可以大規(guī)模爬取數(shù)據(jù)畴嘶,但是最好還是不要給對(duì)方服務(wù)器造成壓力,影響其服務(wù)的正常運(yùn)行集晚,做任何爬蟲(chóng)都是這樣的窗悯,主要還是以學(xué)習(xí)爬蟲(chóng)思想為主,了解爬蟲(chóng)的常見(jiàn)對(duì)抗升級(jí)方式偷拔。

注:

  1. 如果您不希望我在文章提及您文章的鏈接蒋院,或是對(duì)您的服務(wù)器造成了損害,請(qǐng)聯(lián)系我對(duì)文章進(jìn)行修改莲绰;
  2. 本文僅爬取公開(kāi)數(shù)據(jù)欺旧,不涉及到用戶隱私;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛤签,一起剝皮案震驚了整個(gè)濱河市辞友,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌顷啼,老刑警劉巖踏枣,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異钙蒙,居然都是意外死亡茵瀑,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門躬厌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)马昨,“玉大人,你說(shuō)我怎么就攤上這事扛施『枧酰” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵疙渣,是天一觀的道長(zhǎng)匙奴。 經(jīng)常有香客問(wèn)我,道長(zhǎng)妄荔,這世上最難降的妖魔是什么泼菌? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮啦租,結(jié)果婚禮上哗伯,老公的妹妹穿的比我還像新娘。我一直安慰自己篷角,他們只是感情好焊刹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般虐块。 火紅的嫁衣襯著肌膚如雪俩滥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天非凌,我揣著相機(jī)與錄音举农,去河邊找鬼。 笑死敞嗡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的航背。 我是一名探鬼主播喉悴,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼玖媚!你這毒婦竟也來(lái)了箕肃?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤今魔,失蹤者是張志新(化名)和其女友劉穎勺像,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體错森,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吟宦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涩维。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殃姓。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瓦阐,靈堂內(nèi)的尸體忽然破棺而出蜗侈,到底是詐尸還是另有隱情,我是刑警寧澤睡蟋,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布踏幻,位于F島的核電站,受9級(jí)特大地震影響戳杀,放射性物質(zhì)發(fā)生泄漏该面。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一豺瘤、第九天 我趴在偏房一處隱蔽的房頂上張望吆倦。 院中可真熱鬧,春花似錦坐求、人聲如沸蚕泽。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)须妻。三九已至仔蝌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荒吏,已是汗流浹背敛惊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绰更,地道東北人瞧挤。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像儡湾,于是被迫代替她去往敵國(guó)和親特恬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容