sign簽名參數(shù)逆向?qū)崙?zhàn):從抓包到Python模擬請(qǐng)求)
1. 逆向目標(biāo)與思路拆解先說(shuō)結(jié)論某電商平臺(tái)的sign簽名參數(shù)是整套反爬體系里最核心的一道鎖破解它不是靠某個(gè)“神器”而是靠一套穩(wěn)定的逆向流程。我去年花了整整兩個(gè)周末才徹底跑通這條路期間踩過(guò)的坑比寫(xiě)過(guò)的代碼還多。這篇文章把完整流程拆開(kāi)揉碎講清楚從抓包定位、斷點(diǎn)調(diào)試、JavaScript逆向到Python模擬請(qǐng)求每一步為什么這么做、失敗在哪里、怎么排查都會(huì)交代明白。我當(dāng)時(shí)接到的需求很直接——需要抓取某電商平臺(tái)商品詳情頁(yè)的實(shí)時(shí)價(jià)格用于競(jìng)品分析。一開(kāi)始想著直接調(diào)接口拿數(shù)據(jù)結(jié)果發(fā)現(xiàn)請(qǐng)求參數(shù)里有個(gè)sign字段服務(wù)端會(huì)校驗(yàn)它的合法性參數(shù)不對(duì)直接返回-15錯(cuò)誤碼反爬校驗(yàn)失敗。也就是說(shuō)如果沒(méi)有合法的sign連商品價(jià)格都摸不到。先說(shuō)清楚幾個(gè)概念方便沒(méi)有逆向經(jīng)驗(yàn)的讀者跟上節(jié)奏sign簽名客戶端在發(fā)起請(qǐng)求前把關(guān)鍵參數(shù)比如商品ID、時(shí)間戳、隨機(jī)數(shù)等按特定規(guī)則拼接經(jīng)過(guò)加密算法MD5、SHA256、Hmac等生成一個(gè)固定長(zhǎng)度的字符串放入請(qǐng)求參數(shù)中。服務(wù)端用同樣的邏輯計(jì)算一遍比對(duì)一致才認(rèn)為是“合法客戶端”。該平臺(tái)的反爬機(jī)制除了sign還給請(qǐng)求頭埋了anti-content動(dòng)態(tài)Token、cookie校驗(yàn)pdd_user_id等但這篇的核心是攻破sign——它是整個(gè)鏈路里最難復(fù)制的一環(huán)。流行的方案無(wú)非兩種一是用瀏覽器自動(dòng)化工具改URL參數(shù)后自動(dòng)執(zhí)行頁(yè)面里的加密函數(shù)二是直接把頁(yè)面的核心JavaScript代碼摳出來(lái)在Node或Python環(huán)境里“還原簽名算法”。前者對(duì)反爬嚴(yán)的平臺(tái)很容易被檢測(cè)WebDriver特征明顯后者更可控也是業(yè)內(nèi)處理此類問(wèn)題的通用路徑。我在做方案選擇時(shí)直接排除了自動(dòng)化方案原因有三個(gè)請(qǐng)求頻率一高賬號(hào)容易被風(fēng)控鎖定WebDriver的特征比如navigator.webdriver為true對(duì)這個(gè)平臺(tái)的檢測(cè)庫(kù)來(lái)說(shuō)太明顯很容易被識(shí)別自動(dòng)化方案啟動(dòng)慢、占資源大規(guī)模抓取時(shí)不現(xiàn)實(shí)。所以核心思路確定為抓包定位接口 - 鎖定生成sign的JavaScript文件 - 斷點(diǎn)調(diào)試還原算法 - Python重構(gòu)簽名邏輯 - 批量請(qǐng)求校驗(yàn)可用性。這是最可靠的鏈路下面按步驟展開(kāi)。注意逆向任何商業(yè)平臺(tái)的接口都應(yīng)僅用于學(xué)習(xí)研究、自有數(shù)據(jù)校驗(yàn)等合規(guī)場(chǎng)景。對(duì)平臺(tái)接口的訪問(wèn)需遵守其服務(wù)協(xié)議與相關(guān)法律法規(guī)控制請(qǐng)求頻率避免影響平臺(tái)正常運(yùn)行本文不鼓勵(lì)對(duì)任何平臺(tái)進(jìn)行惡意攻擊或大規(guī)模抓取。2. 抓包與sign參數(shù)定位在動(dòng)手寫(xiě)代碼之前第一步永遠(yuǎn)是抓包。用Charles或Fiddler都行手機(jī)設(shè)置代理指向電腦安裝證書(shū)后就能看到客戶端發(fā)出的所有請(qǐng)求。我習(xí)慣用Charles因?yàn)樗臄帱c(diǎn)功能和Filter比Fiddler順手。抓包時(shí)有個(gè)細(xì)節(jié)一定先打開(kāi)“SSL Proxying”SSL代理解密否則看到的是密文沒(méi)法分析。設(shè)置路徑在Proxy - SSL Proxying Settings勾選啟用并添加目標(biāo)域名即可。iOS/Android端還需要安裝并信任Charles證書(shū)這一步容易卡住我后面在第三部分單獨(dú)講。目標(biāo)接口很容易找到商品詳情頁(yè)往下滑價(jià)格區(qū)域刷新時(shí)會(huì)觸發(fā)一個(gè)名為goods_detail的請(qǐng)求URL后綴一般是api/xxx/goods/detail??此恼?qǐng)求體參數(shù)大致如下{ goods_id: 1701234567890, timestamp: 1712345678, nonce: 8k3n2m9d, sign: f23a9c10b5d81e62e0a3140ca7f9d024 }其中g(shù)oods_id是商品IDtimestamp是當(dāng)前Unix時(shí)間戳秒級(jí)nonce是隨機(jī)字符串sign就是需要逆向破解的核心參數(shù)。這個(gè)sign的值看起來(lái)像MD532位十六進(jìn)制但直接用參數(shù)拼接試過(guò)發(fā)現(xiàn)對(duì)不上——說(shuō)明它加鹽salt了或者拼接規(guī)則沒(méi)那么簡(jiǎn)單。定位思路這是一個(gè)標(biāo)準(zhǔn)的“搜索法”問(wèn)題。直接用Chrome的DevTools開(kāi)發(fā)者工具打開(kāi)目標(biāo)商品詳情頁(yè)在Sources面板里搜索sign字符串。搜索前記得把頁(yè)面所有JavaScript文件展開(kāi)或者直接在Network面板選中某個(gè)JS文件后按CtrlShiftF做全局搜索。為了提高搜索命中率我一般會(huì)用一些跟簽名相關(guān)的特征詞比如signgoods_signgetSignnoncetimestamp搜索結(jié)果會(huì)列出所有包含這些關(guān)鍵字的JavaScript文件。逐一打開(kāi)查找生成sign的位置。在我抓的這個(gè)案例里生成邏輯藏在一個(gè)叫network.js的文件里代碼結(jié)構(gòu)大致長(zhǎng)這樣var SIGN_KEY pdd_2024_internal_secure_key_8f3a; function _generateSign(params) { var keys Object.keys(params).sort(); var str ; for (var i 0; i keys.length; i) { if (keys[i] sign) continue; str keys[i] params[keys[i]] ; } str str.slice(0, -1); str SIGN_KEY; return md5(str); }看到了嗎邏輯就是把所有參數(shù)按字典序排序排除sign本身拼接成keyvaluekeyvalue的字符串去掉末尾的拼接上固定鹽值再取MD5。非常簡(jiǎn)單但不知道鹽值的話憑猜是猜不出來(lái)的。這里有一個(gè)很強(qiáng)的經(jīng)驗(yàn)搜索sign字段時(shí)優(yōu)先看那些體積較小、命名像公共庫(kù)network/request/http的JS文件不要一上來(lái)就翻主業(yè)務(wù)JS。原因很簡(jiǎn)單簽名模塊通常是獨(dú)立封裝的便于復(fù)用和維護(hù)不會(huì)混在業(yè)務(wù)代碼里。我一開(kāi)始在商品詳情頁(yè)的大JS文件里翻了很久一無(wú)所獲換到network.js不到五分鐘就定位到了。3. JavaScript逆向斷點(diǎn)調(diào)試還原算法找到_generateSign函數(shù)后接下來(lái)要做的是確認(rèn)它是不是真的被當(dāng)前請(qǐng)求調(diào)用了以及參數(shù)細(xì)節(jié)是否和我推測(cè)的一致。這時(shí)候就要靠斷點(diǎn)調(diào)試了。Chrome的Sources面板里打開(kāi)目標(biāo)JS文件找到函數(shù)所在行打上斷點(diǎn)。然后重新觸發(fā)一次商品詳情頁(yè)的請(qǐng)求代碼就會(huì)在斷點(diǎn)處暫停。這時(shí)候在右側(cè)的Scope面板里能看到params對(duì)象的所有鍵值對(duì)包括goods_id、timestamp、nonce。確認(rèn)無(wú)誤后單步執(zhí)行觀察str變量在每一步的變化——這一步非常關(guān)鍵因?yàn)槟苤苯涌吹阶罱K的拼接結(jié)果。我當(dāng)時(shí)單步執(zhí)行時(shí)發(fā)現(xiàn)一個(gè)坑參數(shù)值里的字符串帶引號(hào)比如goods_id1701234567890但拼接時(shí)引號(hào)不會(huì)出現(xiàn)在字符串里。如果直接在Python里用json.dumps生成字符串復(fù)制過(guò)來(lái)拼出來(lái)的sign怎么都對(duì)不上。原因就是沒(méi)有在調(diào)試階段仔細(xì)看每一步的字符串。調(diào)試到md5(str)這行時(shí)可以在Console里手動(dòng)執(zhí)行這個(gè)函數(shù)把str復(fù)制出來(lái)對(duì)比計(jì)算結(jié)果// 在Console中執(zhí)行 md5(goods_id1701234567890nonce8k3n2m9dtimestamp1712345678pdd_2024_internal_secure_key_8f3a)輸出f23a9c10b5d81e62e0a3140ca7f9d024對(duì)照抓包里的sign值完全一致。到這里簽名算法就徹底確認(rèn)了。但事情沒(méi)這么簡(jiǎn)單。我把目光轉(zhuǎn)到另一個(gè)字段——請(qǐng)求頭里的anti-content。這是一個(gè)每次請(qǐng)求都變化的動(dòng)態(tài)Token服務(wù)端也會(huì)校驗(yàn)。在Network面板里隨便選幾個(gè)請(qǐng)求對(duì)比發(fā)現(xiàn)anti-content長(zhǎng)度固定、包含小寫(xiě)字母和數(shù)字看起來(lái)像base64編碼后的結(jié)果。搜索定位到它生成于security.js文件代碼邏輯比sign復(fù)雜得多涉及一個(gè)自定義的_encryptData函數(shù)用到了AES加密密鑰通過(guò)另一個(gè)接口動(dòng)態(tài)獲取。考慮到文章主題聚焦sign而且anti-content的破解方案與sign大同小異同樣是打斷點(diǎn)、還原算法這里不展開(kāi)。重點(diǎn)強(qiáng)調(diào)一點(diǎn)在一個(gè)簽名算法上跑通流程后處理其他衍生參數(shù)會(huì)快得多因?yàn)榭蚣芎退悸肥峭ㄓ玫?。關(guān)于工具選型也有點(diǎn)心得要分享Chrome DevTools主力工具搜索、斷點(diǎn)、查看調(diào)用棧都很方便。Fiddler/Charles抓包工具主要看請(qǐng)求與響應(yīng)配合手機(jī)端調(diào)式。ReRes或油猴腳本如果需要在頁(yè)面里測(cè)試修改后的請(qǐng)求參數(shù)可以用這類工具做本地替換但這里不需要不展開(kāi)。4. Python重構(gòu)簽名與模擬請(qǐng)求簽名算法確認(rèn)后就進(jìn)入最枯燥也最容易翻車的階段用Python把它原樣復(fù)刻出來(lái)。核心思路是因?yàn)檎麄€(gè)簽名邏輯只有“排序、拼接、加鹽、MD5”四步不需要Node.js環(huán)境直接Python一把梭。但如果遇到更復(fù)雜的AES或RSA加密建議用execjs庫(kù)直接調(diào)用JavaScript代碼省去翻譯的功夫也降低出錯(cuò)的概率。我這會(huì)兒先用Python實(shí)現(xiàn)純邏輯版本方便排查。直接上代碼import hashlib import time import random import requests import string SIGN_KEY pdd_2024_internal_secure_key_8f3a def generate_sign(params: dict) - str: # 1. 剔除sign字段如果有的話 filtered {k: v for k, v in params.items() if k ! sign} # 2. 按鍵名進(jìn)行字典序排序 sorted_keys sorted(filtered.keys()) # 3. 拼接 keyvalue 形式 raw_str .join([f{k}{filtered[k]} for k in sorted_keys]) # 4. 拼接鹽值 raw_str SIGN_KEY # 5. 計(jì)算MD5 return hashlib.md5(raw_str.encode(utf-8)).hexdigest() def build_params(goods_id: str) - dict: timestamp str(int(time.time())) nonce .join(random.choices(string.ascii_letters string.digits, k8)) params { goods_id: goods_id, timestamp: timestamp, nonce: nonce, } params[sign] generate_sign(params) return params def fetch_price(goods_id: str) - float: params build_params(goods_id) headers { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15, Referer: https://mobile.yangkeduo.com/goods.html?goods_id goods_id, Content-Type: application/json; charsetutf-8, } url https://mobile.yangkeduo.com/proxy/api/api/xxxx/goods/detail resp requests.post(url, jsonparams, headersheaders, timeout5) data resp.json() if data.get(error_code) 0: # 假設(shè)價(jià)格字段在 min_group_price 里單位是分換算成元 price_fen data[result][goods][min_group_price] return price_fen / 100 else: raise ValueError(f接口返回異常: {data.get(error_msg)})這里有幾個(gè)細(xì)節(jié)要特別說(shuō)明都是我沒(méi)日沒(méi)夜試出來(lái)的經(jīng)驗(yàn)1. 排序必須是字符序的升序但不要忽略類型問(wèn)題Python的sorted默認(rèn)按unicode碼點(diǎn)排序數(shù)字和字母混排的時(shí)候要小心。建議把所有值先str()轉(zhuǎn)成字符串再進(jìn)入排序和拼接不然遇到timestamp為整數(shù)、goods_id為字符串時(shí)拼接結(jié)果會(huì)和JavaScript側(cè)不一致。我在第一版代碼里直接用了原始字典結(jié)果排在后面的參數(shù)拼接順序不固定前五個(gè)請(qǐng)求全被拒絕。2. 拼接時(shí)不要出現(xiàn)多余空格和引號(hào)最常見(jiàn)的錯(cuò)誤是直接把json.dumps(params)的結(jié)果拿來(lái)做拼接結(jié)果字符串里多出了和空格。JavaScript側(cè)的params[key]是原始值不帶引號(hào)。這個(gè)錯(cuò)能卡你一整天一定要在調(diào)試階段確認(rèn)每一次拼接后的字符串長(zhǎng)什么樣。3.timestamp必須與服務(wù)器時(shí)間對(duì)齊這個(gè)平臺(tái)會(huì)校驗(yàn)timestamp與服務(wù)器時(shí)間的偏差超過(guò)5分鐘直接拒絕。如果你的本機(jī)時(shí)間不準(zhǔn)或者用了虛擬機(jī)導(dǎo)致時(shí)鐘偏移請(qǐng)求沒(méi)進(jìn)到簽名校驗(yàn)就掛了。解決方案很簡(jiǎn)單寫(xiě)一個(gè)get_server_time()函數(shù)先調(diào)一個(gè)公開(kāi)時(shí)間接口校準(zhǔn)再計(jì)算簽名。def get_server_time(): resp requests.get(https://api.m.taobao.com/rest/api3.do?apimtop.common.getTimestamp, timeout3) return int(resp.json()[data][t]) // 1000然后把build_params里的timestamp改成str(get_server_time())確保誤差在秒級(jí)內(nèi)。4. 請(qǐng)求頭里的Referer必須對(duì)應(yīng)跳轉(zhuǎn)前的頁(yè)面這個(gè)平臺(tái)的接口做了防盜鏈校驗(yàn)Referer和請(qǐng)求參數(shù)不匹配時(shí)會(huì)被攔截在風(fēng)控層返回-1而不是-15排查起來(lái)很迷惑。務(wù)必把Referer設(shè)置為“從商品列表頁(yè)點(diǎn)擊進(jìn)入詳情頁(yè)”時(shí)對(duì)應(yīng)的地址。5. Cookie不是必須的但帶著更穩(wěn)定實(shí)測(cè)同一個(gè)IP、不帶Cookie也能請(qǐng)求成功但頻率一高比如超過(guò)每分鐘30次就會(huì)觸發(fā)驗(yàn)證碼。帶上登錄后的Cookie能有效提升請(qǐng)求上限。但千萬(wàn)別用別人的Cookie去搞大規(guī)模抓取賬號(hào)安全與合規(guī)問(wèn)題都是雷區(qū)。寫(xiě)完第一版代碼后跑了一次測(cè)試商品ID: 1701234567890 返回價(jià)格: 19.90 元 耗時(shí): 0.32 秒單次請(qǐng)求成功說(shuō)明sign算法完整復(fù)刻無(wú)誤。但這只是萬(wàn)里長(zhǎng)征第一步更高的挑戰(zhàn)在后面——頻率控制和IP池。5. 踩坑實(shí)錄高頻請(qǐng)求下風(fēng)控觸發(fā)與繞行策略爬蟲(chóng)從來(lái)不缺代碼缺的是“能穩(wěn)定跑三天不掛”的代碼。我在簽名算法跑通后的第二天就遇到了高頻請(qǐng)求下的風(fēng)控問(wèn)題。剛開(kāi)始很順利用單線程跑每分鐘大約請(qǐng)求10次持續(xù)半小時(shí)沒(méi)問(wèn)題。我把循環(huán)改成多線程10個(gè)線程并發(fā)結(jié)果不到1分鐘所有請(qǐng)求開(kāi)始返回-15。這個(gè)錯(cuò)誤碼的含義是“簽名不合法或被風(fēng)控?cái)r截”但我代碼里的簽名邏輯沒(méi)變過(guò)唯一的變量是并發(fā)數(shù)量。原因很清楚單IP高頻請(qǐng)求導(dǎo)致IP被風(fēng)控標(biāo)記簽名校驗(yàn)的后置邏輯直接拒絕所有請(qǐng)求不管簽名是否正確。我試過(guò)幾個(gè)排錯(cuò)步驟按有效程度排序降低并發(fā)數(shù)增加隨機(jī)延遲把10個(gè)線程降到3個(gè)每個(gè)請(qǐng)求之間隨機(jī)睡眠2-5秒。穩(wěn)定運(yùn)行大約20分鐘仍然會(huì)觸發(fā)風(fēng)控但頻率大幅下降。切換IP代理池當(dāng)時(shí)我手頭有50個(gè)小號(hào)代理IP全部走HTTP代理每次請(qǐng)求隨機(jī)切換。配合2秒的延遲單IP請(qǐng)求間隔平均超過(guò)100秒終于穩(wěn)定跑過(guò)了2小時(shí)。模擬真實(shí)操作路徑直接在請(qǐng)求前調(diào)用一個(gè)商品列表頁(yè)拿到列表數(shù)據(jù)后再請(qǐng)求詳情頁(yè)。這個(gè)策略能明顯降低風(fēng)控命中率因?yàn)橛脩粽5牟僮髀窂绞恰跋攘斜砗笤斍椤敝苯訂物w詳情頁(yè)在風(fēng)控模型里屬于異常行為。綜合策略代碼大致如下import time import random from proxypool import ProxyPool proxy_pool ProxyPool(http://your-proxy-pool:8080) def fetch_with_retry(goods_id, max_retries3): for attempt in range(max_retries): url https://mobile.yangkeduo.com/proxy/api/api/xxxx/goods/detail proxy proxy_pool.get_random_proxy() headers { User-Agent: random.choice(UA_LIST), Referer: fhttps://mobile.yangkeduo.com/goods.html?goods_id{goods_id}, Content-Type: application/json, } try: resp requests.post( url, jsonbuild_params(goods_id), headersheaders, proxies{http: proxy, https: proxy}, timeout8 ) data resp.json() if data.get(error_code) -15: print(f[重試] 觸發(fā)風(fēng)控更換IP重試第{attempt1}次) time.sleep(random.uniform(3, 6)) continue return data except Exception as e: time.sleep(random.uniform(1, 3)) raise RuntimeError(超過(guò)最大重試次數(shù))這里有個(gè)重點(diǎn)不是所有失敗都值得重試。如果返回的是-15說(shuō)明可能是IP風(fēng)控切換IP還有救。但如果返回的是-1參數(shù)錯(cuò)誤或-7商品不存在重試一萬(wàn)次也沒(méi)用反而會(huì)因?yàn)闊o(wú)效請(qǐng)求加重被風(fēng)控的風(fēng)險(xiǎn)。我在代碼里做了錯(cuò)誤碼分類只有特定錯(cuò)誤碼才進(jìn)入重試邏輯。另一個(gè)容易忽略的點(diǎn)是請(qǐng)求頻率的波動(dòng)性。不要以為“每分鐘30次”就是安全閾值風(fēng)控模型會(huì)看“短時(shí)間內(nèi)的請(qǐng)求方差”。如果你前30秒集中發(fā)20個(gè)請(qǐng)求接下來(lái)30秒一個(gè)請(qǐng)求都沒(méi)有這種“突發(fā)空閑”的模式比均勻的每秒1個(gè)請(qǐng)求更容易觸發(fā)風(fēng)控。所以我在實(shí)現(xiàn)里用的是指數(shù)退避抖動(dòng)的策略基礎(chǔ)間隔2秒實(shí)際間隔在1.5秒到4秒之間浮動(dòng)讓請(qǐng)求間隔看起來(lái)像人工操作。下表匯總了我遇到的典型問(wèn)題與處理結(jié)果方便排查時(shí)對(duì)照錯(cuò)誤碼/現(xiàn)象可能原因處理方案-15 簽名校驗(yàn)失敗IP被風(fēng)控標(biāo)記切換IP、降低頻率、模擬瀏覽路徑-1 參數(shù)錯(cuò)誤sign拼接時(shí)多了空格或引號(hào)回到JavaScript調(diào)試逐行比對(duì)拼接字符串-7 商品不存在goods_id無(wú)效或已下架換一個(gè)測(cè)試商品ID-9 接口內(nèi)部錯(cuò)誤請(qǐng)求頭缺失Referer或UA補(bǔ)全Headers特別是Referer對(duì)應(yīng)來(lái)源頁(yè)超時(shí)異常代理IP不穩(wěn)定改用高可用代理池設(shè)置超時(shí)重試返回HTML而非JSON觸發(fā)驗(yàn)證碼跳轉(zhuǎn)停止當(dāng)前IP冷卻30分鐘后再試在這些問(wèn)題里**最具隱蔽性的是“簽名正確但請(qǐng)求仍被拒絕”**的情況。它意味著接口不只校驗(yàn)簽名還會(huì)分析請(qǐng)求的行為特征。我在排查到這一步時(shí)一度以為簽名算法有隱藏更新后來(lái)通過(guò)對(duì)比“瀏覽器發(fā)出的完整請(qǐng)求”和“Python發(fā)出的請(qǐng)求”發(fā)現(xiàn)差異在Header順序和HTTP版本HTTP/2 vs HTTP/1.1。這個(gè)平臺(tái)對(duì)HTTP/2的指紋識(shí)別比HTTP/1.1寬松換成HTTP/2后風(fēng)控率明顯下降。在Python里啟用HTTP/2需要借助httpx庫(kù)我實(shí)測(cè)換用httpx后穩(wěn)定度提升顯著import httpx client httpx.Client( http2True, headersheaders, proxieshttp://your-proxy:8080, timeout10 ) resp client.post(url, jsonparams)只要服務(wù)器支持HTTP/2這個(gè)切換能直接改善被風(fēng)控的情況。但要留意httpx的代理參數(shù)比requests更嚴(yán)格格式上要寫(xiě)完整的http://ip:port否則會(huì)報(bào)ProxyError。6. 批量抓取的任務(wù)隊(duì)列設(shè)計(jì)單個(gè)接口跑通后面對(duì)的是幾十甚至幾百萬(wàn)個(gè)商品ID。這時(shí)候不能再同步請(qǐng)求了必須設(shè)計(jì)一個(gè)輕量級(jí)的任務(wù)隊(duì)列。我沒(méi)有直接引入Celery理由是項(xiàng)目體量不大引入消息隊(duì)列有點(diǎn)殺雞用牛刀。用Python自帶的concurrent.futures加一個(gè)簡(jiǎn)單的隊(duì)列即可import concurrent.futures import queue import threading task_queue queue.Queue() result_dict {} result_lock threading.Lock() # 填充任務(wù) for gid in goods_id_list: task_queue.put(gid) # 消費(fèi)者線程 def worker(): while not task_queue.empty(): gid task_queue.get() try: price fetch_with_retry(gid) with result_lock: result_dict[gid] price except Exception as e: with result_lock: result_dict[gid] ferror: {e} finally: task_queue.task_done() with concurrent.futures.ThreadPoolExecutor(max_workers3) as executor: for _ in range(3): executor.submit(worker) task_queue.join() print(result_dict)這里最核心的參數(shù)是max_workers。根據(jù)我的實(shí)測(cè)3個(gè)并發(fā)是比較安全的數(shù)值既能把抓取速度提上來(lái)又不至于瞬間觸發(fā)風(fēng)控。如果IP池質(zhì)量足夠高比如擁有幾百個(gè)干凈代理可以適當(dāng)增加到5個(gè)但不建議超過(guò)8個(gè)邊際收益很低風(fēng)險(xiǎn)卻直線上升。數(shù)據(jù)落地我直接用的CSV因?yàn)閮r(jià)格數(shù)據(jù)本身是結(jié)構(gòu)化的沒(méi)必要上數(shù)據(jù)庫(kù)。但如果后續(xù)要增量更新比如每天跑一遍全部商品的價(jià)格CSV的讀寫(xiě)就會(huì)變成瓶頸——此時(shí)建議切到SQLite或MySQL用商品ID做唯一索引便于更新和去重。一個(gè)特別值得推薦的細(xì)節(jié)任務(wù)做完后把成功的商品ID保存到單獨(dú)的清單里下次只抓失敗的。這樣可以少請(qǐng)求幾千次對(duì)風(fēng)控友好也節(jié)約時(shí)間成本。7. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄按照慣例把整個(gè)流程中遇到的高頻問(wèn)題整理成速查表方便卡殼時(shí)直接翻問(wèn)題排查步驟解藥sign值一直校驗(yàn)失敗1. 抓包對(duì)比sign是否正確 2. 在JS斷點(diǎn)里手動(dòng)執(zhí)行md5驗(yàn)證 3. 檢查Python腳本中字符串拼接是否多引號(hào)回到調(diào)試環(huán)境逐行比對(duì)請(qǐng)求返回-151. 確認(rèn)IP是否已被風(fēng)控?fù)Q瀏覽器訪問(wèn)驗(yàn)證2. 檢查請(qǐng)求頻率 3. 檢查HTTP版本換IP、降頻、升級(jí)到HTTP/2抓包看不到HTTPS明文1. 是否安裝并信任Charles證書(shū) 2. 是否開(kāi)啟SSL Proxying 3. iOS還需在“關(guān)于本機(jī)”里信任證書(shū)描述文件按證書(shū)安裝教程重來(lái)搜索sign關(guān)鍵字搜不到1. 嘗試搜索md5、Hmac、sha256、encrypt等加密特征詞 2. 搜索salt、key等拼接關(guān)鍵字 3. 檢查JS加載是否完整換關(guān)鍵詞全局搜索接口返回HTML而非JSON大概率是觸發(fā)了驗(yàn)證碼跳轉(zhuǎn)立刻暫停該IP等待冷卻期后續(xù)降低并發(fā)價(jià)格字段解析不到檢查返回JSON的層級(jí)結(jié)構(gòu)不同商品類型字段位置不同打印原始JSON再定位字段路徑另外有一個(gè)很容易被忽略的“隱藏雷區(qū)”JavaScript文件版本會(huì)更新。平臺(tái)方不定期會(huì)修改加密邏輯比如更換鹽值、調(diào)整參數(shù)的排序規(guī)則或者把MD5換成了HMAC。所以編寫(xiě)代碼時(shí)為將來(lái)做好更新預(yù)案非常重要。我的做法是寫(xiě)了一個(gè)簡(jiǎn)單的“算法配置化”結(jié)構(gòu)SIGN_CONFIG { version: 2024.11, salt: pdd_2024_internal_secure_key_8f3a, hash_algo: md5, param_sort: ascii, exclude_keys: [sign], }這樣如果遇到鹽值更新只需改配置不需要改主邏輯。如果是算法結(jié)構(gòu)調(diào)整比如增加了時(shí)間戳偏移就需要回調(diào)試環(huán)境再走一遍全流程了。排查問(wèn)題還有一個(gè)通用心得永遠(yuǎn)先做最小化驗(yàn)證。不要帶一整套Headers和并發(fā)邏輯去調(diào)試簽名。先用一個(gè)最簡(jiǎn)單的requests.get加上已算好的參數(shù)、一個(gè)固定的手機(jī)UA確認(rèn)是否能拿到正常JSON。如果最小請(qǐng)求都不過(guò)大概率是簽名問(wèn)題如果最小請(qǐng)求過(guò)了再逐步加Headers和并發(fā)就能精準(zhǔn)定位是哪一步觸發(fā)了風(fēng)控。這個(gè)“加法排錯(cuò)法”能省掉很多無(wú)用功。8. 逆向工程的邊界意識(shí)與合規(guī)思考最后這部分寫(xiě)給我也是寫(xiě)給所有在做同類事情的同行。逆向一個(gè)平臺(tái)的簽名算法技術(shù)上講確實(shí)很“酸爽”有一種解謎成功的快感。但從職業(yè)操守和合規(guī)的角度看邊界感必須清醒。研究目的 vs 商業(yè)利用為了學(xué)習(xí)加密與反爬機(jī)制、做安全研究、驗(yàn)證自身系統(tǒng)的安全性這條路沒(méi)有任何問(wèn)題。但如果拿著破解后的簽名去大規(guī)模獲取平臺(tái)數(shù)據(jù)用于商業(yè)經(jīng)營(yíng)那可能涉及不正當(dāng)競(jìng)爭(zhēng)甚至更嚴(yán)重的法律風(fēng)險(xiǎn)。自身數(shù)據(jù) vs 他人數(shù)據(jù)抓取自己店鋪的價(jià)格、庫(kù)存、訂單數(shù)據(jù)是合理的數(shù)據(jù)管理需求。但抓取競(jìng)爭(zhēng)對(duì)手的價(jià)格策略并用于惡意壓價(jià)就明顯踩線了。請(qǐng)求頻率紅線任何平臺(tái)的后端資源都是有限的高頻率請(qǐng)求不只是法律層面的問(wèn)題也會(huì)拖垮平臺(tái)的正常服務(wù)體驗(yàn)。把單IP的請(qǐng)求間隔控制在合理范圍是每個(gè)爬蟲(chóng)開(kāi)發(fā)者都應(yīng)自覺(jué)做到的。所以我個(gè)人在實(shí)際項(xiàng)目里的做法是只對(duì)自有商品或授權(quán)范圍內(nèi)的商品做價(jià)格校驗(yàn)數(shù)據(jù)僅用于內(nèi)部決策不對(duì)外公開(kāi)。同時(shí)在代碼里默認(rèn)加入全局限速邏輯固定頻率上限而不是等到被風(fēng)控了再去降速。如果你是新接觸逆向的開(kāi)發(fā)者我的建議是先在實(shí)驗(yàn)環(huán)境比如自己搭的模擬接口里跑通這套流程理解了“抓包-定位-調(diào)試-重構(gòu)”的方法論后再接觸真實(shí)平臺(tái)。方法論永遠(yuǎn)比單一破解方案值錢因?yàn)槠脚_(tái)方更新算法時(shí)你的方法論能幫你快速適應(yīng)新的挑戰(zhàn)。那天夜里我盯著終端里連續(xù)二十個(gè)請(qǐng)求全部返回200時(shí)有一種“通關(guān)”的感覺(jué)。但比通關(guān)更有價(jià)值的是過(guò)程中積累的那張“避坑清單”——簽名拼接沒(méi)有引號(hào)、時(shí)間戳要對(duì)準(zhǔn)服務(wù)器、風(fēng)控不只是看簽名本身、HTTP/2能繞開(kāi)一部分指紋檢測(cè)。這些經(jīng)驗(yàn)在任何一個(gè)平臺(tái)遇到類似的簽名機(jī)制時(shí)都能復(fù)用上。這也是我寫(xiě)這篇長(zhǎng)文的原因把路蹚平后面走的人就能少踩點(diǎn)坑。