【JS 逆向百例】拉勾網(wǎng)爬蟲,traceparent、__lg_stoken__、X-S-HEADER 等參數(shù)分析

聲明

本文章中所有內(nèi)容僅供學(xué)習(xí)交流,抓包內(nèi)容、敏感網(wǎng)址、數(shù)據(jù)接口均已做脫敏處理,嚴(yán)禁用于商業(yè)用途和非法用途,否則由此產(chǎn)生的一切后果均與作者無關(guān),若有侵權(quán),請在公眾號聯(lián)系我立即刪除!

逆向目標(biāo)

本次的目標(biāo)是拉勾網(wǎng)職位的爬取,涉及到的一些關(guān)鍵參數(shù)如下:

  • 請求頭參數(shù):traceparent、X-K-HEADER、X-S-HEADER、X-SS-REQ-HEADERx-anit-forge-code、x-anit-forge-token
  • Cookie 值:user_trace_token、X_HTTP_TOKEN、__lg_stoken__
  • POST 請求數(shù)據(jù)加密,返回的加密職位信息解密,AES 算法

參數(shù)比較多,但事實上有些參數(shù)固定、或者直接不要,也是可以的,比如 Cookie 的三個值,請求頭的 X-K-HEADERX-SS-REQ-HEADER 等可以固定,x-anit-forge-codex-anit-forge-token 可有可無。盡管如此,本文還是把每個參數(shù)的來源都分析了,可根據(jù)你實際情況靈活處理。

另外即便是把所有參數(shù)都補齊了,拉勾網(wǎng)對于單個 IP 還有頻率限制,抓不了幾次就要求登錄,可自行搭配代理進行抓取,或者復(fù)制賬號登錄后的 cookies 到代碼里,可以解除限制,如果是賬號登錄后訪問,請求頭多了兩個參數(shù),即 x-anit-forge-codex-anit-forge-token,經(jīng)過測試這兩個參數(shù)其實不要也行。

抓包分析

搜索職位,點擊翻頁,就可以看到一條名為 positionAjax.json 的 Ajax 請求,不難判斷這就是返回的職位信息。重點參數(shù)已在圖中框出來了。

未登錄,正常 IP,正常請求,Header 以及 Cookies:

01
02

異常 IP,登錄賬號后再請求,Header 以及 Cookies:

03
04

請求數(shù)據(jù)和返回數(shù)據(jù)都經(jīng)過了加密:

05

Cookies 參數(shù)

先看 cookies 里的關(guān)鍵參數(shù),主要是 user_trace_token、X_HTTP_TOKEN__lg_stoken__。

user_trace_token

通過接口返回的,直接搜索就可以找到,如下圖所示:

06
07

請求參數(shù),time 是時間戳,a 值隨便,沒有都可以,不影響,其他值都是定值,獲取的關(guān)鍵代碼如下:

def get_user_trace_token() -> str:
    # 獲取 cookie 中的 user_trace_token
    json_url = "https://a.脫敏處理.com/json"
    headers = {
        "Host": "a.脫敏處理.com",
        "Referer": "https://www.脫敏處理.com/",
        "User-Agent": UA
    }
    params = {
        "lt": "trackshow",
        "t": "ad",
        "v": 0,
        "dl": "https://www.脫敏處理.com/",
        "dr": "https://www.脫敏處理.com",
        "time": str(int(time.time() * 1000))
    }
    response = requests.get(url=json_url, headers=headers, params=params)
    user_trace_token = response.cookies.get_dict()["user_trace_token"]
    return user_trace_token

X_HTTP_TOKEN

直接搜索沒有值,直接上 Hook 大法,小白朋友不清楚的話可以看 K 哥以前的文章,都有詳細教程,這里不再細說。

(function () {
    'use strict';
    var cookieTemp = "";
    Object.defineProperty(document, 'cookie', {
        set: function (val) {
            console.log('Hook捕獲到cookie設(shè)置->', val);
            if (val.indexOf('X_HTTP_TOKEN') != -1) {
                debugger;
            }
            cookieTemp = val;
            return val;
        },
        get: function () {
            return cookieTemp;
        }
    });
})();
08

往上跟棧調(diào)試,是一個小小的 OB 混淆,_0x32e0d2 就是最后的 X_HTTP_TOKEN 值了,如下圖所示:

09

直接梭哈,才300多行,不必扣了,全部 copy 下來,本地運行,發(fā)現(xiàn)會報錯 document 未定義,定位到代碼位置,下斷點調(diào)試一下,發(fā)現(xiàn)是正則匹配 cookie 中的 user_trace_token 的值,那么我們直接定義一下即可:var document = {"cookie": cookie},cookie 值把 user_trace_token 傳過來即可。

10

補全 document 后,再次運行,又會報錯 window 未定義,再次定位到源碼,如下圖所示:

11

分析一下,取了 window XMLHttpRequest 對象,向 wafcheck.json 這個接口發(fā)送了一個 Ajax GET 請求,然后取了 Response Header 的 Date 值賦值給 _0x309ac8,注意這個 Date 值比正常時間晚了8個小時,然而取 Date 值并沒有什么用,因為后面又 new 了一個新 Date 標(biāo)準(zhǔn)時間,賦值給了 _0x150c4dnew Date(_0x309ac8[_0x3551('0x2d')](/-/g, '/')) 語句雖然用到了前面的舊 Date,然而實際上是 replace() 替換方法,與舊的 Date 并沒有什么關(guān)系,然后調(diào)用 Date.parse() 方法將新 Date 轉(zhuǎn)換成時間戳賦值給 _0x4e6d5d,所以不需要這么復(fù)雜,直接本地把 _0x89ea429 方法修改一下就行了:

// 原方法
// function _0x89ea42() {
//     var _0x372cc0 = null;
//     if (window[_0x3551('0x26')]) {
//         _0x372cc0 = new window[(_0x3551('0x26'))]();
//     } else {
//         _0x372cc0 = new ActiveObject(_0x3551('0x27'));
//     }
//     _0x372cc0[_0x3551('0x28')](_0x3551('0x29'), _0x3551('0x2a'), ![]);
//     _0x372cc0[_0x3551('0x2b')](null);
//     var _0x309ac8 = _0x372cc0[_0x3551('0x2c')]('Date');
//     var _0x150c4d = new Date(_0x309ac8[_0x3551('0x2d')](/-/g, '/'));
//     var _0x4e6d5d = Date[_0x3551('0x2e')](_0x150c4d);
//     return _0x4e6d5d / 0x3e8;
// }

// 本地改寫
function _0x89ea42() {
    var _0x150c4d = new Date();
    var _0x4e6d5d = Date.parse(_0x150c4d);
    return _0x4e6d5d / 0x3e8;
}

本地測試 OK:

12

__lg_stoken__

__lg_stoken__ 這個參數(shù)是在點擊搜索后才開始生成的,直接搜索同樣沒值,Hook 一下,往上跟棧,很容易找到生成位置:

13
14

可以看到 d 就是 __lg_stoken__ 的值,d = (new g()).a()、g = window.gt,window.gt 實際上是調(diào)用了 _0x11db59

跟進混淆的 JS 看一下,就會發(fā)現(xiàn)末尾的這段代碼是關(guān)鍵,這里用到了 prototype 原型對象,我們直接 window.gt.prototype.a() 或者 (new window.gt).a() 就能獲取到 __lg_stoken__,如下圖所示:

15

到這里也許你想下斷點去調(diào)試一下,看看能不能扣個邏輯出來,但是你會發(fā)現(xiàn)刷新之后斷不下,因為這個混淆 JS 文件是一直在變化的,之前的斷點就不管用了,然后你就可能會想到直接替換掉這個 JS,讓文件名固定下來,就可以斷點調(diào)試了,如果你這樣操作的話,重新刷新會發(fā)現(xiàn)一直在加載中,打開控制臺會發(fā)現(xiàn)報錯了,造成這樣的原因就在于這個混淆 JS 不僅文件名會改變,他的內(nèi)容也會改變,當(dāng)然,內(nèi)容也不僅僅是改變了變量名那么簡單,有些值也是動態(tài)變化的,比如:

16

這里我們先不管那么多,直接把所有的混淆代碼 copy 下來,先在本地調(diào)試一下,看看能不能跑通,調(diào)試過程中,先后會提示 window is not defined、Cannot read properties of undefined (reading 'hostname'),定位到代碼,有個取 window.location.hostname 的操作,本地定義一下就行了:

17

再次調(diào)試又會報錯 Cannot read properties of undefined (reading 'substr')substr() 方法可在字符串中抽取從指定下標(biāo)開始的、指定數(shù)目的字符,是字符串對象 stringObject 具有的方法,我們定位到代碼,發(fā)現(xiàn)是 window.location.search 對象調(diào)用了 substr() 方法,所以同樣的,我們本地也要補齊。

18

本地補齊參數(shù)后,運行結(jié)果與網(wǎng)頁一致:

19

執(zhí)行結(jié)果沒問題了,那么還有一個問題,window.location.search 的值就是待加密參數(shù)了,是咋來的呢?我們直接搜索,就可以看到是一個接口302跳轉(zhuǎn)的地址,用的時候直接取就行了,這個接口是你搜索內(nèi)容組成的,搜索不同參數(shù),這個跳轉(zhuǎn)地址也是不一樣的:

20

調(diào)試成功后,我們隨便換一個搜索關(guān)鍵詞,將得到的302跳轉(zhuǎn)地址拿到這個 JS 中,加密一下,發(fā)現(xiàn)會報錯,這說明混淆 JS 傳入的參數(shù)和 JS 內(nèi)容應(yīng)該是相對應(yīng)的,這里的做法是直接請求拿到這個 JS 文件內(nèi)容,然后把要補的 window 和獲取 __lg_stoken__ 的方法加進去,然后直接執(zhí)行就行了。

獲取 __lg_stoken__ 的關(guān)鍵代碼如下(original_data 為原始搜索數(shù)據(jù)):

def get_lg_stoken(original_data: dict) -> str:
    # 獲取 cookie 中的 __lg_stoken__
    token_url = "https://www.脫敏處理.com/wn/jobs"
    token_headers = {
        "Host": "www.脫敏處理.com",
        "Referer": "https://www.脫敏處理.com/",
        "User-Agent": UA
    }
    params = {
        "kd": original_data["kd"],
        "city": original_data["city"]
    }
    token_response = requests.get(url=token_url, params=params, headers=token_headers, cookies=global_cookies, allow_redirects=False)
    if token_response.status_code != 302:
        raise Exception("獲取跳轉(zhuǎn)鏈接異常!檢查 global_cookies 是否已包含 __lg_stoken__!")
    # 獲取 302 跳轉(zhuǎn)的地址
    security_check_url = token_response.headers["Location"]
    if "login" in security_check_url:
        raise Exception("IP 被關(guān)進小黑屋啦!需要登錄!請補全登錄后的 Cookie,或者自行添加代理!")
    parse_result = parse.urlparse(security_check_url)
    # url 的參數(shù)為待加密對象
    security_check_params = parse_result.query
    # 取 name 參數(shù),為混淆 js 的文件名
    security_check_js_name = parse.parse_qs(security_check_params)["name"][0]

    # 發(fā)送請求,獲取混淆的 js
    js_url = "https://www.脫敏處理.com/common-sec/dist/" + security_check_js_name + ".js"
    js_headers = {
        "Host": "www.脫敏處理.com",
        "Referer": security_check_url,
        "User-Agent": UA
    }
    js_response = requests.get(url=js_url, headers=js_headers, cookies=global_cookies).text
    # 補全 js,添加 window 參數(shù)和一個方法,用于獲取 __lg_stoken__ 的值
    lg_js = """
    window = {
        "location": {
            "hostname": "www.脫敏處理.com",
            "search": '?%s'
        }
    }
    function getLgStoken(){
        return window.gt.prototype.a()
    }
    """ % security_check_params + js_response

    lg_stoken = execjs.compile(lg_js).call("getLgStoken")
    return lg_stoken

請求頭參數(shù)

請求頭參數(shù)比較多,有 traceparentX-K-HEADER、X-S-HEADER、X-SS-REQ-HEADER、x-anit-forge-code、x-anit-forge-token,其中最后兩個 x-anit 開頭的參數(shù)是登錄后才有的,實際測試中,即便是登錄了,不加這兩個好像也行。不過還是分析一下吧。

x-anit-forge-code / x-anit-forge-token

這兩個值是首次點擊搜索生成的,第一次訪問搜索接口,返回的 HTML 里面夾雜了一個 JSON 文件,里面的 submitCodesubmitToken 就是 x-anit-forge-codex-anit-forge-token 的值,如下圖所示:

21

請求這個接口要注意帶上登錄后的 cookies,有用的只有四個值,正確的 cookies 類似于:

cookies = {
    "login": "true",
    "gate_login_token": "54a31e93aa904a6bb9731bxxxxxxxxxxxxxx",
    "_putrc": "9550E53D830BE8xxxxxxxxxxxxxx",
    "JSESSIONID": "ABAAAECABIEACCA79BFxxxxxxxxxxxxxx"
}

注意,JSESSIONID 即便不登錄也會有,但是登錄時應(yīng)該會攜帶這個值,進行一個激活操作,如果你請求獲取到的 submitCode、submitToken 為空,那么就有可能 JSESSIONID 是無效的,以上所有值都必須登錄后復(fù)制過來!

獲取 x-anit-forge-codex-anit-forge-token 的關(guān)鍵代碼如下(original_data 為原始搜索數(shù)據(jù)):

def update_x_anit(original_data: dict) -> None:
    # 更新 x-anit-forge-code 和 x-anit-forge-token
    url = "https://www.脫敏處理.com/wn/jobs"
    headers = {
        "Host": "www.脫敏處理.com",
        "Referer": "https://www.脫敏處理.com/",
        "User-Agent": UA
    }
    params = {
        "kd": original_data["kd"],
        "city": original_data["city"]
    }
    response = requests.get(url=url, params=params, headers=headers, cookies=global_cookies)
    tree = etree.HTML(response.text)
    next_data_json = json.loads(tree.xpath("http://script[@id='__NEXT_DATA__']/text()")[0])
    submit_code = next_data_json["props"]["tokenData"]["submitCode"]
    submit_token = next_data_json["props"]["tokenData"]["submitToken"]
    # 注意 JSESSIONID 必須是登錄驗證后的!
    if not submit_code or not submit_token:
        raise Exception("submitCode & submitToken 為空,請檢查 JSESSIONID 是否正確!")
    global x_anit
    x_anit["x-anit-forge-code"] = submit_code
    x_anit["x-anit-forge-token"] = submit_token

traceparent

同樣的 Hook 大法,跟棧:

(function () {
    var org = window.XMLHttpRequest.prototype.setRequestHeader;
    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
        console.log('Hook 捕獲到 %s 設(shè)置 -> %s', key, value);
        if (key == 'traceparent') {
            debugger;
        }
        return org.apply(this, arguments);
    };
})();
22
23

觀察上面的代碼,三元表達式,t.sampledtrue,所以 e 值為 01n 值為 t.id,重點在于 t.traceIdt.id 了,跟棧發(fā)現(xiàn)很難調(diào),直接搜索關(guān)鍵字,可找到生成的位置:

24
25

E() 方法扣出來就行了,改寫一下即可:

getRandomValues = require('get-random-values')

function E(t) {
    for (var b = [], w = 0; w < 256; ++w)
            b[w] = (w + 256).toString(16).substr(1);
    var T = new Uint8Array(16);
    return function(t) {
        for (var e = [], n = 0; n < t.length; n++)
            e.push(b[t[n]]);
        return e.join("")
    }(getRandomValues(T)).substr(0, t)
}

function getTraceparent(){
    return "00-" + E() + "-" + E(16) + "-" + "01"
}

// 測試輸出
// console.log(getTraceparent())

X-K-HEADER / X-SS-REQ-HEADER

X-K-HEADERX-SS-REQ-HEADER 數(shù)據(jù)是一樣的,只不過后者是鍵值對形式,先直接全局搜索關(guān)鍵字,發(fā)現(xiàn)都是從本地拿這兩個值,清除 cookie 就為空了,那么直接搜索值,發(fā)現(xiàn)是 agreement 這個接口返回的,secretKeyValue 值就是我們要的,有可能瀏覽器抓包直接搜索的話搜索不到,使用抓包工具,比如 Fiddler 就能搜到了,如下圖所示:

26

這個接口是 post 請求,請求帶了一個 json 數(shù)據(jù),secretKeyDecode,直接搜索關(guān)鍵字,就一個值,定位跟棧:

27

zt() 是從本地緩存中取,At() 是重新生成:

28

這里就非常明顯了,t 是32位隨機字符串,賦值為 aesKey,后面緊接著一個 RSA 加密了 aesKey,賦值為 rsaEncryptData,而 rsaEncryptData 就是前面 agreement 接口請求的 secretKeyValue 值。

這里先說一下,最終搜索職位請求的 data 和返回數(shù)據(jù)都是 AES 加密解密,會用到這個 aesKey,請求頭的另一個參數(shù) X-S-HEADER 也會用到,如果這個 key 沒有經(jīng)過 RSA 加密并通過 agreement 接口驗證的話,是無效的,可以理解為 agreement 接口既是為了獲取 X-K-HEADERX-SS-REQ-HEADER,也是為了激活這個 aesKey。

這部分的 JS 代碼和 Python 代碼大致如下:

JSEncrypt = require("jsencrypt")

function getAesKeyAndRsaEncryptData() {
    var aesKey = function (t) {
        for (var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", r = "", n = 0; n < t; n++) {
            var i = Math.floor(Math.random() * e.length);
            r += e.substring(i, i + 1)
        }
        return r
    }(32);

    var e = new JSEncrypt();
    e.setPublicKey("-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbJqzIXk6qGotX5nD521Vk/24APi2qx6C+2allfix8iAfUGqx0MK3GufsQcAt/o7NO8W+qw4HPE+RBR6m7+3JVlKAF5LwYkiUJN1dh4sTj03XQ0jsnd3BYVqL/gi8iC4YXJ3aU5VUsB6skROancZJAeq95p7ehXXAJfCbLwcK+yFFeRKLvhrjZOMDvh1TsMB4exfg+h2kNUI94zu8MK3UA7v1ANjfgopaE+cpvoulg446oKOkmigmc35lv8hh34upbMmehUqB51kqk9J7p8VMI3jTDBcMC21xq5XF7oM8gmqjNsYxrT9EVK7cezYPq7trqLX1fyWgtBtJZG7WMftKwIDAQAB-----END PUBLIC KEY-----");
    var rsaEncryptData = e.encrypt(aesKey);

    return {
        "aesKey": aesKey,
        "rsaEncryptData": rsaEncryptData
    }
}

// 測試輸出
// console.log(getAesKeyAndRsaEncryptData())
def update_aes_key() -> None:
    # 通過JS獲取 AES Key,并通過接口激活,接口激活后會返回一個 secretKeyValue,后續(xù)請求頭會用到
    global aes_key, secret_key_value
    url = "https://gate.脫敏處理.com/system/agreement"
    headers = {
        "Content-Type": "application/json",
        "Host": "gate.脫敏處理.com",
        "Origin": "https://www.脫敏處理.com",
        "Referer": "https://www.脫敏處理.com/",
        "User-Agent": UA
    }
    encrypt_data = lagou_js.call("getAesKeyAndRsaEncryptData")
    aes_key = encrypt_data["aesKey"]
    rsa_encrypt_data = encrypt_data["rsaEncryptData"]
    data = {"secretKeyDecode": rsa_encrypt_data}
    response = requests.post(url=url, headers=headers, json=data).json()
    secret_key_value = response["content"]["secretKeyValue"]

X-S-HEADER

X-S-HEADER 你每次翻頁都會改變,直接搜索關(guān)鍵字可定位:

29
30

中間有一個 SHA256 加密,最后返回的 Rt(JSON.stringify({originHeader: JSON.stringify(e), code: t})) 就是 X-S-HEADER 的值了,Rt() 是一個 AES 加密,比較關(guān)鍵的,Vt(r) 是一個 URL,比如你搜索職位就是 positionAjax.json,搜索公司就是 companyAjax.json,可根據(jù)實際情況定制,然后 Lt(t) 就是搜索信息,字符串形式,包含了城市、頁碼、關(guān)鍵詞等。

獲取 X-S-HEADER 的 JS 代碼大致如下:

CryptoJS = require('crypto-js')

jt = function(aesKey, originalData, u) {
    var e = {deviceType: 1}
      , t = "".concat(JSON.stringify(e)).concat(u).concat(JSON.stringify(originalData))
      , t = (t = t, null === (t = CryptoJS.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());

    return Rt(JSON.stringify({
        originHeader: JSON.stringify(e),
        code: t
    }), aesKey)
}

Rt = function (t, aesKey) {
    var Ot = CryptoJS.enc.Utf8.parse("c558Gq0YQK2QUlMc"),
        Dt = CryptoJS.enc.Utf8.parse(aesKey),
        t = CryptoJS.enc.Utf8.parse(t);
    t = CryptoJS.AES.encrypt(t, Dt, {
        iv: Ot,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    return t.toString()
};

function getXSHeader(aesKey, originalData, u){
    return jt(aesKey, originalData, u)
}

// 測試樣例
// var url = "https://www.脫敏處理.com/jobs/v2/positionAjax.json"
// var aesKey = "dgHY1qVeo/Z0yDaF5WV/EEXxYiwbr5Jt"
// var originalData = {"first": "true", "needAddtionalResult": "false", "city": "全國", "pn": "2", "kd": "Java"}
// console.log(getXSHeader(aesKey, originalData, url))

請求/返回數(shù)據(jù)解密

前面抓包我們已經(jīng)發(fā)現(xiàn) positionAjax.json 是 POST 請求,F(xiàn)orm Data 中的數(shù)據(jù)是加密的,返回的 data 也是加密的,我們分析請求頭參數(shù)的時候,就涉及到 AES 加密解密,所以我們直接搜索 AES.encrypt、AES.decrypt,下斷點調(diào)試:

31
32

非常明顯了,這部分的 JS 代碼大致如下:

CryptoJS = require('crypto-js')

function getRequestData(aesKey, originalData){
    return Rt(JSON.stringify(originalData), aesKey)
}

function getResponseData(encryptData, aesKey){
    return It(encryptData, aesKey)
}

Rt = function (t, aesKey) {
    var Ot = CryptoJS.enc.Utf8.parse("c558Gq0YQK2QUlMc"),
        Dt = CryptoJS.enc.Utf8.parse(aesKey),
        t = CryptoJS.enc.Utf8.parse(t);
    t = CryptoJS.AES.encrypt(t, Dt, {
        iv: Ot,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    return t.toString()
};

It = function(t, aesKey) {
    var Ot = CryptoJS.enc.Utf8.parse("c558Gq0YQK2QUlMc"),
    Dt = CryptoJS.enc.Utf8.parse(aesKey);
    t = CryptoJS.AES.decrypt(t, Dt, {
        iv: Ot,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    }).toString(CryptoJS.enc.Utf8);
    try {
        t = JSON.parse(t)
    } catch (t) {}
    return t
}

// 測試樣例,注意,encryptedData 數(shù)據(jù)太多,省略了,直接運行解密是會報錯的
// var aesKey = "dgHY1qVeo/Z0yDaF5WV/EEXxYiwbr5Jt"
// var encryptedData = "r4MqbduYxu3Z9sFL75xDhelMTCYPHLluKaurYgzEXlEQ1Rg......"
// var originalData = {"first": "true", "needAddtionalResult": "false", "city": "全國", "pn": "2", "kd": "Java"}
// console.log(getRequestData(aesKey, originalData))
// console.log(getResponseData(encryptedData, aesKey))

大致的 Python 代碼如下:

def get_header_params(original_data: dict) -> dict:
    # 后續(xù)請求數(shù)據(jù)所需的請求頭參數(shù)
    # 職位搜索 URL,如果是搜索公司,那就是 https://www.脫敏處理.com/jobs/companyAjax.json,根據(jù)實際情況更改
    u = "https://www.脫敏處理.com/jobs/v2/positionAjax.json"
    return {
        "traceparent": lagou_js.call("getTraceparent"),
        "X-K-HEADER": secret_key_value,
        "X-S-HEADER": lagou_js.call("getXSHeader", aes_key, original_data, u),
        "X-SS-REQ-HEADER": json.dumps({"secret": secret_key_value})
    }


def get_encrypted_data(original_data: dict) -> str:
    # AES 加密原始數(shù)據(jù)
    encrypted_data = lagou_js.call("getRequestData", aes_key, original_data)
    return encrypted_data


def get_data(original_data: dict, encrypted_data: str, header_params: dict) -> dict:
    # 攜帶加密后的請求數(shù)據(jù)和完整請求頭,拿到密文,AES 解密得到明文職位信息
    url = "https://www.脫敏處理.com/jobs/v2/positionAjax.json"
    referer = parse.urljoin("https://www.脫敏處理.com/wn/jobs?", parse.urlencode(original_data))
    headers = {
        # "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Host": "www.脫敏處理.com",
        "Origin": "https://www.脫敏處理.com",
        "Referer": referer,
        "traceparent": header_params["traceparent"],
        "User-Agent": UA,
        "X-K-HEADER": header_params["X-K-HEADER"],
        "X-S-HEADER": header_params["X-S-HEADER"],
        "X-SS-REQ-HEADER": header_params["X-SS-REQ-HEADER"],
    }
    # 添加 x-anit-forge-code 和 x-anit-forge-token
    headers.update(x_anit)

    data = {"data": encrypted_data}
    response = requests.post(url=url, headers=headers, cookies=global_cookies, data=data).json()
    if "status" in response:
        if not response["status"] and "操作太頻繁" in response["msg"]:
            raise Exception("獲取數(shù)據(jù)失?。sg:%s!可以嘗試補全登錄后的 Cookies,或者添加代理!" % response["msg"])
        else:
            raise Exception("獲取數(shù)據(jù)異常!請檢查數(shù)據(jù)是否完整!")
    else:
        response_data = response["data"]
        decrypted_data = lagou_js.call("getResponseData", response_data, aes_key)
        return decrypted_data

最終整合所有代碼,成功拿到數(shù)據(jù):

33

逆向小技巧

瀏覽器開發(fā)者工具 Application - Storage 選項,可以一鍵清除所有 Cookies,也可以自定義存儲配額:

34

Storage - Cookies 可以查看每個站點的所有 Cookies,HttpOnly 打勾的表示是服務(wù)器返回的,選中一條 Cookie,右鍵可以直接定位到哪個請求帶了這個 Cookie,也可以直接編輯值,還可以刪除單個 Cookie,當(dāng)你登錄了賬號,但又需要清除某個 Cookie,且不想重新登錄時,這個功能或許有用。

35

完整代碼

文中給出了部分關(guān)鍵代碼,不能直接運行,部分細節(jié)可能沒提及到,完整代碼已放 GitHub,均有詳細注釋,歡迎 Star。所有內(nèi)容僅供學(xué)習(xí)交流,嚴(yán)禁用于商業(yè)用途、非法用途,否則由此產(chǎn)生的一切后果均與作者無關(guān),在倉庫中下載的文件學(xué)習(xí)完畢之后請于 24 小時內(nèi)刪除!

倉庫地址:https://github.com/kgepachong/crawler/

常見問題

  • JS 代碼里引用了三個庫,npm install 安裝一下即可,如果安裝了還提示找不到庫,那就是路徑問題,推薦在當(dāng)前目錄下執(zhí)行命令安裝,或者在 Python 代碼里指定完整路徑,具體方法可自行百度。

  • jsencrypt 這個庫,本地運行可能會報錯 window is not defined,在 \node_modules\jsencrypt\bin\jsencrypt.js 源碼中加入 var window = global; 即可,這是實現(xiàn) RSA 加密的庫,當(dāng)然還有很多其他實現(xiàn)方法或者庫,都可以。

  • execjs 執(zhí)行 JS 的時候,可能會報編碼錯誤 "gbk" can't decode byte...,有兩種解決方法,一是找到官方源碼 subprocess.py,搜索 encoding=None 改成 encoding='utf-8',二是直接在 Python 代碼里面加入以下代碼即可:

import subprocess
from functools import partial

subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容