【爬蟲成長之路】(七)【大眾點評】PC微信小程序+requests爬取數(shù)據(jù)

本系列文章共十篇:

【爬蟲成長之路】(一)爬蟲系列文章導讀
【爬蟲成長之路】(二)各篇需要用到的庫和工具
【爬蟲成長之路】(三)【大眾點評】selenium爬蟲
【爬蟲成長之路】(四)【大眾點評】selenium登錄+requests爬取數(shù)據(jù)
【爬蟲成長之路】(五)【大眾點評】瀏覽器掃碼登錄+油猴直接爬取數(shù)據(jù)
【爬蟲成長之路】(六)【大眾點評】mitmproxy中間人代理爬蟲
【爬蟲成長之路】(七)【大眾點評】PC微信小程序+requests爬取數(shù)據(jù)
【爬蟲成長之路】(八)【大眾點評】安卓APP爬蟲

聲明:本文相比于前面的文章來說,算是進階爬蟲了,由于涉及到較多的加解密算法,故只提供相關的加解密算法,不提供加解密算法的秘鑰。讀者在學習過程中,主要還是以學習爬蟲思想為主

本文需要用到的工具:Fiddler、JSON解析工具、小程序逆向工具
本文需要用到的庫:requests、json、base64Crypto、xxtea、zlib、urllib.parse...

爬取目標數(shù)據(jù):

  1. 指定城市的店鋪列表及其評分數(shù)據(jù)
  2. 指定店鋪下的用戶評論數(shù)據(jù)

一、需求分析

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

  1. 某城市的店鋪列表頁面
  2. 某店鋪的評論列表頁面

二、獲取目標頁面的URL

由于這里的目標對象是微信小程序,獲取接口就必須借助第三方工具了,文中使用的是Fiddler,當然你也可以用你順手的工具,接下來簡單講下如何操作:

  1. 打開Fiddler,進入localhost://[port]獲取Fiddler的證書,安裝證書
  2. 配置好Fiddler,使其可以抓HTTPS的包
  3. 打開電腦客戶端點評的小程序,進入到對應的頁面,如果配置沒有問題就能在Fiddler中看到數(shù)據(jù)包了
  4. 如果Fiddler里全是Tunnel to 443,請重新安裝證書,并重啟Fiddler,如果還有別的問題,請自行百度
  5. 開啟代理后,小程序的很容易出現(xiàn)獲取不到數(shù)據(jù)的狀況,這個多刷新幾次,到處點幾下就好了,多試幾次就能找到接口了
  6. 如果出來的過太多了,也可以在Fiddler中開啟過濾,pass掉一部分無關URL

操作完后,獲取到了店鋪列表接口和評論列表接口,分別如下:

# 店鋪列表接口
https://m.dianping.com/wxmapi/index/cnxh?...
# 評論列表接口
https://m.dianping.com/ugc/review/reviewlist?...

三、請求頭Header、URL參數(shù)解析

獲取到相應的接口后,就要去看里面的參數(shù)了,這里總共有三部分的參數(shù)需要查看,分別是:

  1. URL 后面帶了參數(shù)
  2. response 返回的參數(shù)(json數(shù)據(jù),未加密)
  3. 請求頭Header里面還有參數(shù)

評論接口里面的相關參數(shù):
序號 名稱 參數(shù)個數(shù) 重點參數(shù) 是否加密 是否固定
1 方法類型 GET - - -
2 URL參數(shù) 14 shopUuid,cx,_token,optimus_uuid
3 Header參數(shù) 17 openid,openidPlt,token
4 response - reviewList

Header參數(shù):

序號 Header參數(shù) 說明
1 Host m.dianping.com 域名
2 Connection keep-alive -
3 User-Agent Mozilla/5.0 (Windows NT 6.1;WOW64) ***WindowsWechat 用戶代理
4 channel weixin 渠道
5 channelversion 7.0.9 渠道版本
6 content-type application/json 傳輸類型
7 minaname dianping-wxapp 可固定
8 minaversion 6.7.0 可固定,也可適當變化
9 openid Hv********** 微信提供給小程序的唯一ID
10 openidPlt oPp*********** 服務端生成
11 platform windows 平臺
12 platformversion 1064 平臺版本win10,64位
13 sdkversion 2.13.2 SDK版本
14 token 12a********** 服務端生成
15 wechatversion 7.0.9 微信版本
16 Referer https://servicewechat.com/wx734c1ad7b3562129/231/page-frame.html 來源
17 Accept-Encoding gzip,deflate,br 接受的編碼格式

URL 參數(shù):

序號 URL參數(shù) 說明
1 tagType 1 評論類型,美食評論or其他類型評論
2 tag 全部 獲取全部評論,也可進行篩選
3 offset 10 頁偏移,從0開始,以10為步進
4 shopUuid H58vsmtj2***** 店鋪ID,店鋪列表獲取
5 cx WX__ver1.2.0_CCCC_QZ+jty4u********XU= 本地生成
6 mtsiReferrer /packages/ugc/pages/reviewlist/reviewlist?msource=wxappmain&shopUuid=H58vsmtj2*****&tag=全部&tagType=1 類似于refer
7 _token eJx********* 本地生成
8 optimus_uuid 176****** 本地生成
9 optimus_platform 13 平臺類型
10 optimus_partner 203 未知,可固定
11 optimus_risk_level 71 風險等級,可固定
12 optimus_code 10 可固定
13 pullDown false 可固定
14 reLoad false 可固定
點評小程序評論列表接口及參數(shù)分析

店鋪列表接口相關參數(shù):
序號 名稱 參數(shù)個數(shù) 重點參數(shù) 是否加密 是否固定
1 方法類型 GET - - -
2 URL參數(shù) 14 wxuuid,sessionId,optimus_uuid,_token
3 Header參數(shù) 17 openid,openidPlt,token
4 response - reviewList

Header參數(shù)(不變,同評論列表接口):

URL參數(shù):

序號 名稱 說明
1 cityId 4 城市ID
2 wxuuid Hvc*** 同微信openid
3 page 2 頁碼
4 lat ** 緯度
5 lng ** 經(jīng)度
6 sessionId e7ef*** 會話ID
7 mtsiReferrer /pages/index/index?cityId=4&wxuuid=Hvc...&page=2&lat=...&lng=...&sessionId=e7ef... 類似refer
8 optimus_uuid 1763b93*****d 本地生成的uuid參數(shù)
9 optimus_platform 13 平臺類型
10 optimus_partner 203 可固定
11 optimus_risk_level 71 風險等級,可固定
12 optimus_code 10 可固定
13 _token eJx***********pAA== 本地生成
店鋪列表接口及參數(shù)分析

四、請求頭Header、URL參數(shù)構造

在進行爬取時,我們必須要能夠重新構造請求里的全部參數(shù),因此,知道這些參數(shù)是如何生成的就十分有必要了,當然,這一步也是爬蟲工作中最難、最費時間的一步,接下來教大家分析這些參數(shù)是如何生成的,其中對命名不準確的參數(shù)要大膽的去猜測其含義。

STEP 1:分析Header里的參數(shù)

  1. 參數(shù)一:openid
    從上面的Header參數(shù)列表我們可以知道,Header里重點需要關注的參數(shù)就3個,分別是openid、openidPlttoken,有過微信公眾號或小程序開發(fā)的都應該知道,openid是微信為公眾號或小程序提供的唯一openid,用于標識不同的用戶,在不同公眾號或小程序,同一用戶的openid是不一致的;所以這個openid我們不能自行生成,必須要服務端返回給我們才行。如果沒有相關開發(fā)經(jīng)驗,可以通過百度相關字段,也能在微信開發(fā)文檔中找到其含義。

  2. 參數(shù)二:openidPlt
    這個參數(shù)也帶了openid,應該也與openid有關系,不是本地生成的就是服務端獲取的,就這兩種情況,通過對請求的數(shù)據(jù)進行分析,易知,這個字段是服務端返回的;那如何知道的呢?我們看請求列表中會有這樣一個請求,

https://maccount.dianping.com/thirdlogin/ajax/auth

在它的response中,可以看到有openidPlt這個字段(圖中沒有框起來),這里定位這個字段,需要在請求中找到這個字段第一次出現(xiàn)的位置,否則也是不能確定其是如何產(chǎn)生的。我們經(jīng)過簡單的分析后,可以知道openidPlt這個字段就是通過.../thirdlogin/ajax/auth這個請求從服務端獲取的,因此這個字段是如何來的我們也確定了。

token參數(shù)獲取接口及參數(shù)分析
  1. 參數(shù)三:token
    這個token參數(shù)的分析同參數(shù)二openidPlt,心細的同學可以發(fā)現(xiàn),在參數(shù)二的獲取時,其response響應返回的字段中,就有這么一個token參數(shù),于是我們也可以知道,token這個參數(shù)也是服務端返回的,不能再本地生成。

通過上面的分析,Header里的參數(shù)已經(jīng)解決了,知道是怎么來的了。因為Header里的openidopenidPlt、token這三個參數(shù)都是通過.../thirdlogin/ajax/auth這個接口向服務端獲取的,因此我們只需要模擬一下這個請求就好了。那我們就再來看一下這個.../thirdlogin/ajax/auth請求,

  • 先看Header,可以知道它使用的方法是POST,Header里也沒有什么重要的參數(shù),基本可以確定這個請求就是首次驗證身份用的。
  • 再看URL和Body,可以從下圖.../thirdlogin/ajax/auth的Header看到,URL中并未帶有其他參數(shù),于是再看其body中是否帶有相關參數(shù),通過上圖token參數(shù)獲取接口及參數(shù)分析可以看到,其body中有5個參數(shù),分別是codesourceType、directLogin、cx、_token,分析時先從簡單的入手,通過對比多個.../thirdlogin/ajax/auth請求,而我們可以發(fā)現(xiàn),sourceTypedirectLogin這兩個字段是固定的,不用變動。而對于其他三個參數(shù)codecx、_token,明顯經(jīng)過加密,我們也不能從上下文的response響應中找到其身影,說明這三個參數(shù)就是本地生成的。
.../thirdlogin/ajax/auth的Header

現(xiàn)在大概流程就比較清晰了,Header里的關鍵參數(shù)在.../thirdlogin/ajax/auth的response中,發(fā)起.../thirdlogin/ajax/auth請求需要構造code、cx、_token這三個關鍵參數(shù),而則三個關鍵參數(shù)是在本地生成的。通過一番百度,查看微信小程序開發(fā)手冊可以得知,其中code這個參數(shù)是微信APP提供給小程序的,每次提供的code都是不一樣的,這個和openid不一樣,同樣是微信提供給小程序的,但openid是始終不變的,而code則是小程序通過調用接口,由微信APP生成的,并且有時效性,這個可以通過查看微信小程序登錄的時序圖來判斷。而剩下的兩個參數(shù)cx、_token由于是在本地生成的,就必須要去看小程序的源碼了(其實code也是通過查看小程序源碼來確定的...),要查看小程序源碼,就需要借助其他的工具了,關于微信小程序逆向又可以寫一篇文章了,這不是這篇文章的重點,故不展開講,下面提供一些微信小程序逆向的參考文章:

  1. 反編譯獲取任何微信小程序源碼(微信小程序逆向教程)
  2. wxappUnpacker
  3. 逆向小程序破解js-(逆向篇)
  4. 微信小程序“反編譯”實戰(zhàn)(一):解包
  5. 小程序爆破之旅---分析微信源碼來解析微信小程序文件格式
微信小程序登錄時序圖
時序圖說明
微信開發(fā)手冊關于code

STEP 2:分析URL里的參數(shù)
其實通過上面的分析,也大概可以知道,就拿評論列表里的URL參數(shù)來說,關鍵的參數(shù)就3個,分別是cx_token、optimus_uuid,這里要注意,這里的_token和前面提到的token并不是同一個參數(shù),這三個參數(shù)的分析思路也也前面的Header里的參數(shù)分析很相似,先從請求列表中尋找其首次出現(xiàn)的位置,如果首次首先的位置在請求的URL參數(shù)或Body中,則這個參數(shù)是本地生成的,如果是在響應的response中,就是從服務端獲取到的。這里我就不做進一步的分析了,這三個參數(shù)其實也是通過分析反編譯后小程序才能分析出來的,這三個參數(shù)都是其他參數(shù)經(jīng)過了加密生成的。

這里反編譯點評的微信小程序之后,最好還是能夠導入到微信開發(fā)者工具中進行動態(tài)調試,無奈本人太菜,逆向過來的小程序導入后一堆錯誤,不想去改了,就直接靜態(tài)分析了,費勁...

在獲取到了token參數(shù)之后,還需要向服務器提交驗證服務器(這一步不確定是否必須,沒驗證)

token參數(shù)接口及校驗

所以綜合來說,最關鍵的參數(shù)總共有這么幾個,即驗證身份所需要的codecx、_token,知道了這幾個參數(shù)如何生成,這個小程序爬蟲就解決了。其中cx、_token可以通過靜態(tài)或動態(tài)分析小程序源碼來解出,但code這個參數(shù)由于是微信APP給小程序提供的,所以要獲取這個參數(shù),就必須逆向微信APP,然而逆向微信APP這個工程有點大,所以這里就換種思路。在實際測試過程中,我們發(fā)現(xiàn)token這個參數(shù)的有效時長還是蠻長的,所以我們就先將手動獲取到的token參數(shù)保存下來給后面的爬蟲用。

通過觀察發(fā)現(xiàn),cx、_token這兩個參數(shù)在每次請求時都發(fā)生了變化,故這兩個參數(shù)里必然帶了時間戳進行加密,由于是靜態(tài)解密,所以要做的就是搜索關鍵字,閱讀源碼,理清邏輯結構,也沒啥技術含量,就不在這里講解如何操作了。

五、相關參數(shù)的加解密算法

這里就直接給出相關參數(shù)的加解密算法了,具體如何得來,就是依靠靜態(tài)分析,當然能動靜結合自然是更好的。

通過搜索關鍵字就能發(fā)現(xiàn),參數(shù)加解密的算法全部在finger.jskonan.js這兩個文件里面,可以多看看finger.js這個文件,但這個文件太大了,我沒看完,就看了部分和參數(shù)加解密相關的,需要的同學自行去查看。其中代碼涉及到秘鑰的部分會隱去,需要學習使用的同學請自行動手反編譯小程序后查看。

cx:小程序原始js加密算法(部分代碼)

wxPre = "WX__ver1.2.0_CCCC_", 
    getFinger = function(t) {
        fingerData.app = config.app, fingerData.openid = config.openid, fingerData.unionid = config.unionid, 
        fingerData.mchid = config.mchid;
        var e, r = wxPre;
        try {
            fingerData.location || getLocation(), getTimeStamp(), fingerData.userInfo ? 
            (e = JSON.stringify(fingerData), r += CBCencrypt(e), t && t(r)) : 
            getUserInfo(function() {
                var e = JSON.stringify(fingerData);
                r += CBCencrypt(e), t && t(r);
                console.log(r);
            });
        } catch (e) {
            t && t(r);
        }
    }

function CBCencrypt(t) {
        function e() {
            for (var t = [ "91EBA6DBE4E***", "C5F5FDF5F2F5F3F***" ], e = [], r = "", n = 0; n < t.length; n++) {
                r = "";
                for (var i = t[n], o = i.length, a = parseInt("0x" + i.substr(0, 2)), s = 2; s < o; s += 2) {
                    var c = parseInt("0x" + i.charAt(s) + i.charAt(s + 1));
                    r += String.fromCharCode(c ^ a);
                }
                e.push(r);
            }
            return e;   /* e = ["z7Jut6Ywr***", "080706050***"] */
        }
        var r = e()[0], n = e()[1], i = cryptoJs.enc.Utf8.parse(r), o = {  /* i.toString() -> 7a374a757436597**** */
            iv: cryptoJs.enc.Utf8.parse(n), /* 偏移量 .toString() -> 30383037303630**** */
            mode: cryptoJs.mode.CBC,        /* 加密模式 */ 
            padding: cryptoJs.pad.Pkcs7     /* 填充 */
        };
        return cryptoJs.AES.encrypt(t, i, o).toString(); /* t:待加密內容, i:秘鑰,  */
}

cx 加解密算法,python版本

import base64
import urllib.parse
from Crypto.Cipher import AES
class AES_CBC:

    def __init__(self):
        self.key = 'z7Jut6Ywr***'
        self.iv = '080706050***'

    def add_to_16(self, value):
        while len(value) % 16 != 0:
            value += '\0'
        return str.encode(value)

    def encrypt(self, content):
        aes = AES.new(self.add_to_16(self.key), AES.MODE_CBC, self.add_to_16(self.iv))
        bs = AES.block_size
        pad2 = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs)  # PKS7填充
        # 執(zhí)行加密并轉碼返回bytes
        encrypt_aes = aes.encrypt(str.encode(pad2(content)))
        # 用base64轉成字符串形式
        encrypted_content = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
        return encrypted_content.replace('\n', '')

    def decrypt(self, en_content):
        aes = AES.new(self.add_to_16(self.key), AES.MODE_CBC, self.add_to_16(self.iv))
        base64_decrypted = base64.decodebytes(en_content.encode(encoding='utf-8'))  # 優(yōu)先逆向解密base64成bytes
        decrypted_content = str(aes.decrypt(base64_decrypted), encoding='utf-8')  # 執(zhí)行解密并轉碼返回str
        unpad = lambda s: s[0:-ord(s[-1])]
        return unpad(decrypted_content)

cx解密完成后,結果如下??梢钥吹剑@個cx參數(shù)的生成幾乎獲取了手機的全部能獲取到的參數(shù)。

{
  "system": {
    "albumAuthorized": true,
    "benchmarkLevel": -1,
    "bluetoothEnabled": false,
    "brand": "microsoft",
    "cameraAuthorized": true,
    "errMsg": "getSystemInfo:ok",
    "fontSizeSetting": 15,
    "language": "zh_CN",
    "locationAuthorized": true,
    "locationEnabled": true,
    "microphoneAuthorized": true,
    "model": "microsoft",
    "notificationAuthorized": true,
    "notificationSoundEnabled": true,
    "pixelRatio": 1.25,
    "platform": "windows",
    "power": 100,
    "safeArea": {
      "bottom": 676,
      "height": 676,
      "left": 0,
      "right": 415,
      "top": 0,
      "width": 415
    },
    "screenHeight": 736,
    "screenWidth": 415,
    "statusBarHeight": 20,
    "system": "win10",
    "theme": "light",
    "version": "7.0.9",
    "wifiEnabled": true,
    "windowHeight": 676,
    "windowWidth": 415,
    "SDKVersion": "2.11.0",
    "devicePixelRatio": 1.25,
    "WifiInfo": "",
    "Beacons": "",
    "LaunchOptionsSync": "{\"path\":\"\",\"query\":{},\"scene\":1001,\"referrerInfo\":{}}",
    "networkType": "wifi",
    "brightness": 0.88,
    "StorageInfo": "{\"currentSize\":768,\"errMsg\":\"getStorageInfo:ok\",\"keys\":[\"$PIKE_LOADBALANCE_WIFI\",\"OH_LAST_SELECTED_CITY\",\"WXOWLKEY-unionId\",\"_lx_sdk_lxcuid\",\"_lx_sdk_wxid\",\"_pike_config\",\"city\",\"dp_categoryList\",\"dp_city_history\",\"dp_index_abtest\",\"dp_index_activity\",\"dp_index_group-seckill\",\"dp_index_guesslike\",\"dp_list\",\"dp_my_columns\",\"dp_my_tools\",\"dp_shop\",\"dp_shop_dinevote_showtips\",\"dp_shop_list\",\"dp_shop_pop\",\"dp_shop_style\",\"geo\",\"hotelCheckTime\",\"keyword_history\",\"loccity\",\"logan_days_info\",\"logan_session_token\",\"loganlog_2020-12-07_1_0\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/common/caster-report\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/common/launch\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/dpweapp/shop-branch\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/dpweapp/shop-mall\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/dpweapp/tab-my-index\",\"openid\",\"quickConfig\",\"showData\",\"uuid\",\"webp\"],\"limitSize\":10000}",
    "BatteryInfo": "{\"isCharging\":true,\"level\":\"100\",\"errMsg\":\"getBatteryInfo:ok\"}"
  },
  "app": 0,
  "timestamp": 160799699656
}

_token 加解密算法,python版本

import base64
import zlib
import urllib.parse
class Token:
    def encrypt(self, data):
        data_byte = json.dumps(data).encode()  # 字典 ---> 字符串 ---> byte數(shù)據(jù)
        compressed_data_byte = zlib.compress(data_byte)  # byte數(shù)據(jù) ---> 壓縮數(shù)據(jù)
        base64_data_byte = base64.b64encode(compressed_data_byte)  # 壓縮數(shù)據(jù)  --->  base64加密數(shù)據(jù)(byte數(shù)據(jù))
        decode_data = base64_data_byte.decode()  # 加密后的byte數(shù)據(jù) --->  字符串token
        final_data = urllib.parse.quote(decode_data)
        return final_data

    def decrypt(self, sign_or_token):
        normal_data = urllib.parse.unquote(sign_or_token)  # 對其中的url進行解碼
        decode_data_r = normal_data.encode()  # 將字符數(shù)據(jù)轉成byte數(shù)據(jù)
        base64_data_byte_r = base64.b64decode(decode_data_r)  # 將byte數(shù)據(jù)進行解密
        compressed_data_byte_r = zlib.decompress(base64_data_byte_r)  # 將經(jīng)過base64解密過的byte數(shù)據(jù)解壓縮
        data_r = compressed_data_byte_r.decode()  # 將解壓后的數(shù)據(jù)轉成字符串
        return data_r

這里對_token解密完成后,解密結果如下,可以看到里面還有一個被加密了的sign參數(shù):

token_data = {"rId": 100, "ts": int(time.time() * 1000), "cts": int(time.time() * 1000) + 1000000, "brVD": [415, 676],
              "brR": [[519, 845], [519, 845], 24, 24], "bI": ["pages/detail/detail", "packages/search/pages/list/list"],
              "mT": [], "kT": [], "aT": [], "tT": [],
              "sign": "eJx1jl1rw***ByIXg=="}

其實這個sign參數(shù)也是用的和_token相同的加密算法。

那就還剩下一個optimus_uuid了,這個怎么生成的我也沒弄明白,因為在爬取的過程中發(fā)現(xiàn)它不會變動,所以也就不想去看了,逆向后的小程序關于optimus_uuid的部分如下,所在文件是preyoda.js..wechat-sdk/lib/index.js

//preyoda.js
exports.default = new r.default({
    type: "request",
    resolve: function(e) {
        var r = e.request;
        try {
            var a = i.default.getCurrentPage();
            a.hasRedirectStatusInited || (u.default.initRedirectStatus(), a.hasRedirectStatusInited = !0);
            var s = r.data;
            s && s.mtsiReferrer && (s.optimus_uuid = s.optimus_uuid || t.default.get("lxcuid"), 
            s.optimus_platform = s.optimus_platform || 13, s.optimus_partner = s.optimus_partner || 203, 
            s.optimus_risk_level = 71, s.optimus_code = 10, e.request = r);
        } catch (e) {}
        return e;
    }
});

// ..wechat-sdk/lib/index.js
Ct.lxcuid = function(e) {
    function t(e, t) {
        var n, r = 0;
        for (n = 0; n < t.length; n++) r |= s[n] << 8 * n;
        return e ^ r;
    }
    var n = S("lxcuid");
    if (n) return n;
    var r, i, a = function() {
        for (var e = 1 * new Date(), t = 0; e === 1 * new Date() && t < 200; ) t++;
        return e.toString(16) + t.toString(16);
    }, o = +(Math.random() + "").slice(2), c = e.ua || "", s = [], u = 0;
    for (r = 0; r < c.length; r++) i = c.charCodeAt(r), s.unshift(255 & i), 4 <= s.length && (u = t(u, s), 
    s = []);
    0 < s.length && (u = t(u, s)), c = u;
    var f = 0;
    e.sc && (f = +(f = e.sc.split("*"))[0] * +f[1]);
    var l = [ a(), o, c, f, a() ].map(function(e) {
        return e.toString(16);
    }).join("-");
    return x("lxcuid", l), l;
}(Ct);

在爬取的過程中,還遇到了驗證碼,提交驗證碼有兩步,觸發(fā)驗證的數(shù)據(jù)如下,驗證碼提交時也有一個重要的參數(shù),是behavior,這里就不再去觸發(fā)它的驗證碼了,在測試的過程中可以使用Fiddler劫持response,來達到觸發(fā)驗證碼,下面就直接給出behavior的加解密算法,不再進行說明:

{
  "msg": "您的網(wǎng)絡好像不太給力,請稍后再試",
  "code": 406,
  "customData": {
    "verifyPageUrl": "https://verify.meituan.com/v2/app/general_page?action=spiderindefence&requestCode=ca0d5061bbaf49***&platform=13&adaptor=auto&succCallbackUrl=https://optimus-mtsi.meituan.com/optimus/verifyResult",
    "imageUrl": "https://verify.meituan.com/v2/captcha?action=spiderindefence&request_code=ca0d5061bbaf4951944***",
    "requestCode": "ca0d5061bbaf49519***",
    "verifyUrl": "https://optimus-mtsi.meituan.com/optimus/verify?request_code=ca0d5061bbaf4951***"
  }
}

下面是驗證碼加解密相關的小程序JS源碼,文件所在位置是konan.js

// XXTEA 加密算法
function xxtea_encrypt(str, key) {
    var e, n, o, a, f, i, u = str.length, h = u - 1;
    for (n = str[h], o = 0, i = 0 | Math.floor(6 + 0x34 / u); i > 0; --i) {
        for (a = (o = o + 0x9E3779B9 & 0xFFFFFFFF) >>> 2 & 3, f = 0; f < h; ++f)
            e = str[f + 1];
        n = str[f] = str[f] + ((n >>> 5 ^ e << 2) + (e >>> 3 ^ n << 4) ^ (o ^ e) + (key[3 & f ^ a] ^ n)) & 0xFFFFFFFF;
        e = str[0];
        n = str[h] = str[h] + ((n >>> 5 ^ e << 2) + (e >>> 3 ^ n << 4) ^ (o ^ e) + (key[3 & h ^ a] ^ n)) & 0xFFFFFFFF;
    }
    return str;
}

function xxtea_decrypt(str, key) {
    if (str == "") {
        return "";
    }
    var v = str2long(str,  false);
    var k = str2long(key,  false);
    if (k.length < 4) {
        k.length = 4;
    }
    var n = v.length - 1;

    var z = v[n - 1], y = v[0], delta = 0x9E3779B9;
    var mx, e, p, q = Math.floor(6 + 52 / (n + 1)), sum = q * delta & 0xffffffff;
    while (sum != 0) {
       e = sum >>> 2 & 3;
        for (p = n; p > 0; p--) {
           z = v[p - 1];
           mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
           y = v[p] = v[p] - mx & 0xffffffff;
       }
       z = v[n];
       mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
       y = v[0] = v[0] - mx & 0xffffffff;
       sum = sum - delta & 0xffffffff;
   }

    return long2str(v,  true);
}

behavior參數(shù)加解密算法,python版本:

# behavior 的解析和生成
class Behavior:
    def gen_behavior(self, key):
        t = time.time()
        t1 = int(round(t)*1000)
        raw_behavior = {
            "env": {
                "zone": [2**.8, 25.54***],
                "client": [63.**, 36.3**],
                "Timestamp": [t1, t1 + 2077],
                # "Timestamp": [160907***, 160907***],
                "count": 1,
                "timeout": 0
            },
            "trajectory": [{
                "point": [
                    [0, 89+random.randint(-2,2), 350, 2077+random.randint(-2,2)],
                    [0, 101+random.randint(-2,2), 350, 2226+random.randint(-2,2)],
                    [0, 108+random.randint(-2,2), 350, 2232+random.randint(-2,2)],
                    [0, 116+random.randint(-2,2), 350, 2258+random.randint(-2,2)],
                    [0, 120+random.randint(-2,2), 350, 2261+random.randint(-2,2)],
                    [0, 128+random.randint(-2,2), 350, 2262+random.randint(-2,2)],
                    [0, 136+random.randint(-2,2), 350, 2266+random.randint(-2,2)],
                    [0, 144+random.randint(-2,2), 350, 2287+random.randint(-2,2)],
                    [0, 152+random.randint(-2,2), 350, 2287+random.randint(-2,2)],
                    [0, 164+random.randint(-2,2), 350, 2289+random.randint(-2,2)],
                    [0, 177+random.randint(-2,2), 350, 2311+random.randint(-2,2)],
                    [0, 192+random.randint(-2,2), 350, 2312+random.randint(-2,2)],
                    [0, 208+random.randint(-2,2), 350, 2314+random.randint(-2,2)],
                    [0, 236+random.randint(-2,2), 350, 2335+random.randint(-2,2)],
                    [0, 257+random.randint(-2,2), 350, 2335+random.randint(-2,2)],
                    [0, 284+random.randint(-2,2), 350, 2336+random.randint(-2,2)],
                    [0, 306+random.randint(-2,2), 350, 2359+random.randint(-2,2)],
                    [0, 324+random.randint(-2,2), 350, 2359+random.randint(-2,2)],
                    [0, 336+random.randint(-2,2), 350, 2359+random.randint(-2,2)]
                ],
                "vector":{
                    "orientation": "h"
                }
            }]
        }
        '''
        [{"point":[
        [0,93,350,1405],[0,100,350,1436],[0,102,350,1460],[0,106,350,1460],[0,113,352,1460],[0,124,353,1485],[0,136,353,1485],[0,148,353,1485],[0,158,353,1509],[0,176,353,1510],[0,188,353,1510],[0,204,353,1539],[0,217,353,1539],[0,230,353,1539],[0,244,353,1545],[0,258,353,1566],[0,272,353,1566],[0,284,353,1566],[0,298,353,1589],[0,310,353,1589],[0,322,353,1590],[0,332,353,1611],[0,340,353,1612]
        ],"vector":{"orientation":"h"}}]
        '''
        en1 = xxtea.encrypt(json.dumps(raw_behavior).replace(' ', ''), key)
        en2 = base64.b64encode(en1).decode('utf-8')
        return en2

    def de_behavior(self, content, key):
        en_xxtea = base64.b64decode(content)
        raw_data = xxtea.decrypt(en_xxtea, key)
        return raw_data.decode('utf-8')

六、優(yōu)缺點分析

序號 優(yōu)點 缺點
1 程序運行更快 參數(shù)構造麻煩
2 - 需要對小程序進行逆向

七、結語

文章到此已經(jīng)篇幅過大了,就不再對程序進行演示,由于爬蟲越高級,就越有可能使服務器無法區(qū)分是否是爬蟲,這樣容易給對方服務器造成過大的壓力。但就本文所提到的爬蟲而言,由于用戶無法生成openidcode這兩個參數(shù),所以想要利用小程序爬蟲發(fā)起大規(guī)模的并發(fā)請求是不現(xiàn)實的,希望同學們僅供學習使用,不要惡意發(fā)起大量請求。

在測試過程中發(fā)現(xiàn),小程序爬蟲還是很容易觸發(fā)驗證碼的,所以想要大量爬取數(shù)據(jù),也只能放慢速度,慢慢爬取。

注:

  1. 如果您不希望我在文章提及您文章的鏈接,或是對您的服務器造成了損害,請聯(lián)系我對文章進行修改;
  2. 本文僅爬取公開數(shù)據(jù),不涉及到用戶隱私;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容