老师要求做一个裁判文书网的爬虫,算是我写的第一个爬虫吧,在这里记录一下。

登录接口

这个登录没有验证码,没有机器人验证,这不来一手自动登录?F12 之后抓登录的包:

image-20210605234235272.png

可以看到用户名没变,密码是加密过的,剩下的 appDomain 是固定的,不用管。

password 加密算法

下一步是找密码的加密算法和密钥。首先翻看 html 里面引入的 js ,在 index.js 中找到了和登录相关的代码:

image-20210605234804334.png

但仔细看能发现,这里绑定的 name 在 html 文件中根本没有,所以是错的登录代码。

查看 html 文件中登录按钮的代码:

<div class="login-button-container">
  <span tabindex="0" data-mainkey="true" class="button button-primary" data-action="login-submit" data-api="/api/login">登录</span>
</div>

可以看到有个 data-action ,必然是绑定登录操作时候用的。全局搜索 login-submit ,在 login.js 中找到了绑定登录操作的代码:

image-20210605235659512.png

这是个 minify 过后的 js 文件,不是很容易读。搜索 password 关键字可以找到有个 encodePassword 函数,猜想这个就是加密函数:

image-20210606000723185.png

测试后确定这个是密码的加密函数。用的 JSEncrypt ,用 python 简单实现一下:

    def encrypt_password(self):
        rsa_key = RSA.importKey(self.passwdPublicKey)
        cipher = Cipher_pkcs1_v1_5.new(rsa_key)
        cipher_text = base64.b64encode(cipher.encrypt(self.password.encode(encoding="utf-8")))
        value = cipher_text.decode('utf-8')
        return value

至此,密码的加密搞定。

小问题

搞定密码后一直出现 "帐号没有授权" 之类的错误,卡了挺久的。最后模拟登录时候的情况,把登录前后所有的包都模拟发送一遍后解决。

        pre_auth_url = 'https://wenshu.court.gov.cn/tongyiLogin/authorize'
        pre_auth_res = requests.post(url=pre_auth_url, data={}, headers=headers)

        cookies = {
            pre_auth_res.cookies.items()[0][0]: pre_auth_res.cookies.items()[0][1]
        }

        pre_auth_url_with_params = pre_auth_res.text
        with_params_url_res = requests.get(url=pre_auth_url_with_params, headers=headers, allow_redirects=False)

        auth_cookies = {
            with_params_url_res.cookies.items()[0][0]: with_params_url_res.cookies.items()[0][1]
        }

        data = {
            "username": self.username,
            "password": quote(self.encrypt_password()),
            "appDomain": "wenshu.court.gov.cn",
        }

        print("login ...")
        login_url = 'https://account.court.gov.cn/api/login'
        login_res = requests.post(login_url, headers=headers, data=data, cookies=auth_cookies)
        login_res.raise_for_status()

        login_text = json.loads(login_res.text)
        if login_text['code'] != '000000':
            raise Exception(login_text['message'])

        login_cookies = {
            login_res.cookies.items()[0][0]: login_res.cookies.items()[0][1]
        }
        with_params_url_res = requests.get(
            url=pre_auth_url_with_params,
            headers=headers,
            cookies=login_cookies,
            allow_redirects=False
        )

        jump_url = with_params_url_res.headers["Location"]
        requests.get(url=jump_url, headers=headers, cookies=cookies)

        # print(cookies)
        self.cookies = cookies

数据接口

搞定登录之后开始找数据接口。按照惯例打开 F12 并搜索一些东西:

image-20210605233551036.png

有用的数据是后面几个 rest.q4w 的 JSON 数据。体积较大的是文书数据,另外一个是侧边栏的数据。

image-20210605233802599.png

可以看到文书数据是加密过的。

image-20210605233859036.png

发送的请求包有些数据也是加密过的。下面一步步解决这些问题。

参数

首先我们来找这些参数都是什么意思,来自哪里。

pageId

pageId 在当前页面的 URL 里有出现:

https://wenshu.court.gov.cn/website/wenshu/181217BMTKHNT2W0/index.html?pageId=26871ff89db9e96d3af2b870c1942b00&s21=%E7%BD%91%E7%BB%9C%E8%AF%88%E9%AA%97

那么 URL 中的 pageId 是从哪里来的呢?

index.js 里面有这么一行:

image-20210606002939810.png

接着搜索这个 uuid 方法,在 website.js 中找到这个方法:

        uuid: function(){
            var guid = "";
            for (var i = 1; i <= 32; i++) {
                var n = Math.floor(Math.random() * 16.0).toString(16);
                guid += n;
                // if ((i == 8) || (i == 12) || (i == 16) || (i == 20)) guid +=
                // "-";
            }
            return guid;
        },

其实就是个 32 位的随机十六进制数。

s21

这其实就是一个分类标识,保持不变就好了。

wenshulist1.js 中可以找到这是哪个分类

sortFields

排序标识,保持不变即可。

ciphertext

明显的二进制密文,找找哪里生成的:

image-20210606003901907.png

接着看这个 cipher 函数,在 strToBinary.js 中找到它的原型:

function cipher() {
    var date = new Date();
    var timestamp = date.getTime().toString();
    var salt = $.WebSite.random(24);
    var year = date.getFullYear().toString();
    var month = (date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date
            .getMonth()).toString();
    var day = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate())
            .toString();
    var iv = year + month + day;
    var enc = DES3.encrypt(timestamp, salt, iv).toString();
    var str = salt + iv + enc;
    var ciphertext = strTobinary(str);
    return ciphertext;
}

function strTobinary(str) {
    var result = [];
    var list = str.split("");
    for (var i = 0; i < list.length; i++) {
        if (i != 0) {
            result.push(" ");
        }
        var item = list[i];
        var binaryStr = item.charCodeAt().toString(2);
        result.push(binaryStr);
    };
    return result.join("");
}

按照这个代码逻辑直接写一个相同的加密函数即可。

    @staticmethod
    def make_ciphertext():
        timestamp = str(int(time.time() * 1000))
        salt = ''.join([random.choice(string.digits + string.ascii_lowercase) for _ in range(24)])
        iv = datetime.datetime.now().strftime('%Y%m%d')
        des = Des()
        enc = des.encrypt(timestamp, salt)
        strs = salt + iv + enc
        result = []
        for i in strs:
            result.append(bin(ord(i))[2:])
            result.append(' ')
        return ''.join(result[:-1])

pageNum && pageSize

当前页数和每页数据的数量。

queryCondition

查询条件,其实和上面的 s21 是一样的,只是 JSON 化了。

cfg

固定的字符串,保持不变。

__RequestVerificationToken

website.js 中找到:

image-20210606004725823.png

24位随机数。

解密返回值

website.js 中找到请求数据的代码:

image-20210606005137406.png

可以看到用的是 3DES ,密钥是从返回值中获取的。

class Des(object):

    @staticmethod
    def encrypt(text, key):
        text = pad(text.encode(), DES3.block_size)
        iv = datetime.datetime.now().strftime('%Y%m%d').encode()
        crypto = DES3.new(key, DES3.MODE_CBC, iv)
        x = len(text) % 8
        ciphertext = crypto.encrypt(text)
        return base64.b64encode(ciphertext).decode("utf-8")

    @staticmethod
    def decrypt(text, key):
        iv = datetime.datetime.now().strftime('%Y%m%d').encode()
        crypto = DES3.new(key, DES3.MODE_CBC, iv)
        de_text = base64.b64decode(text)
        plain_text = crypto.decrypt(de_text)
        out = unpad(plain_text, DES3.block_size)
        return out.decode()

至此,爬虫已经基本完成了。因为老师不要求爬详情页,所以,交差,饮茶去了 XD

drinkTea.jpg

Last modification:November 25th, 2023 at 08:31 pm
If you think my article is useful to you, please feel free to appreciate