import math import time import json import random from kuaishou.ks_http import download_q from utils.rotate_captcha import export_single_discern from xhs.xhs_param import get_xs, encrypt_info, decrypt_info from loguru import logger class SignalCaptcha(object): """ 小红书单旋转验证码 """ def __init__(self, web_session, verify_uuid, a1, webId, source="", is_proxy=False): self.is_proxy = is_proxy self.source = source self.verify_uuid = verify_uuid self.register_url = "https://edith.xiaohongshu.com/api/redcaptcha/v2/captcha/register" self.check_url = "https://edith.xiaohongshu.com/api/redcaptcha/v2/captcha/check" self.headers = { "authority": "edith.xiaohongshu.com", "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9", "cache-control": "no-cache", "content-type": "application/json;charset=UTF-8", "origin": "https://www.xiaohongshu.com", "pragma": "no-cache", "referer": "https://www.xiaohongshu.com/", "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"macOS\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "x-s": "XYW_eyJzaWduU3ZuIjoiNTEiLCJzaWduVHlwZSI6IngxIiwiYXBwSWQiOiJsb2dpbiIsInNpZ25WZXJzaW9uIjoiMSIsInBheWxvYWQiOiJkNzkxNmQ0MDAyOTZmZDg4ZmIzYjcxOGY2MWRlOGU2MTEwYzZmMTBlMjZiOWI0NTQ1OTg0M2VjMTI2NmRlMGZiNDNkZGI3MmE5MWYyOTJkZTM4Yjk3MDlmZjcyMDBmMTRjOWUzYmZkYTFmYWExZWI5MGQ3NGFhMzFiNTRjNzJjZDBkNzRhYTMxYjU0YzcyY2RhYzQ4OWI5ZGE4Y2U1ZTQ4ZjRhZmI5YWNmYzNlYTI2ZmUwYjI2NmE2YjRjYzNjYjVhMDE2NzA1NzIyMWMxZjhkMzY4YzFkMjBjZGE4MGY5OWIxMWYyZWFhMGE5ZmQ3MWQ3NmZmZTgwMmI3MjM3NWQ2OWZiY2JjYzk1OTUxMmNhZDRmZWFjNWU1MjY1MDljYTliZDA3MzE0OGZjMDEzYzJlMzU4ZWE1ZDg5ZGQ4MzZkNWU2OTM4NGJkN2FmNjQ3MjY0ZmNjZWQxMmVkMWFiNmFjY2U1YTE1MWZmZDMzODNlZDE4OGMyMWE4OTlmY2E2Y2UifQ==", "x-s-common": "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", "x-t": "1709171462456" } self.pic_headers = { "authority": "picasso-static.xiaohongshu.com", "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "accept-language": "zh-CN,zh;q=0.9", "cache-control": "no-cache", "origin": "https://www.xiaohongshu.com", "pragma": "no-cache", "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"macOS\"", "sec-fetch-dest": "image", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } self.cookies = { "a1": a1, "webId": webId, "web_session": web_session, } @staticmethod def get_slide_track(distance): """ 根据滑动距离生成滑动轨迹 :param distance: 需要滑动的距离 :return: 滑动轨迹: [[x,y,t], ...] x: 已滑动的横向距离 y: 已滑动的纵向距离, 除起点外, 均为0 t: 滑动过程消耗的时间, 单位: 毫秒 """ t = 246 count = random.randint(4, 10) _y_list = [0, 0, -1, 1, 1, 1, 1, 0] y = 0 x = 0 slide_track = [[0, 0, 246]] for i in range(count): if x >= distance: break t += random.randint(50, 200) y += _y_list[random.randint(0, 7)] x += random.randint(30, 60) slide_track.append([x, y, t]) if x != distance: slide_track[-1][0] = distance return slide_track, slide_track[-1][-1] @staticmethod def get_captcha_info(fg_path, bg_path): """ 图片识别 以及对于captcha_info加密参数的封装 :param fg_path: :param bg_path: :return: """ angle = export_single_discern(fg_path, bg_path) distance = math.ceil(angle * 0.79) logger.info(f"处理后的 距离为 : {distance}") track, last_time = SignalCaptcha.get_slide_track(distance) _time = last_time + random.randint(100, 200) # 处理字段 logger.info(f"_time: {_time}") logger.info(f"track : {track}") captcha_info = { "mouseEnd": encrypt_info(str(distance), _type="mouseEnd"), "time": encrypt_info(str(_time), _type="time"), "track": encrypt_info(str(track).replace(" ", ""), _type="track"), "width": encrypt_info("286", _type="width") } captcha_info = json.dumps(captcha_info, ensure_ascii=False, separators=(',', ':')) return captcha_info def check(self, rid, captcha_info): """ 验证函数:最终提交验证码验证请求 :param rid: :param captcha_info: :return: """ data = { "rid": rid, "verifyType": 102, "verifyBiz": "461", "verifyUuid": self.verify_uuid, "biz": "sns_web", "sourceSite": self.source, "version": "1.0.1", "captchaInfo": captcha_info } headers = self.headers.copy() data = json.dumps(data, separators=(',', ':'), ensure_ascii=False) h = get_xs(self.cookies["a1"], "/api/redcaptcha/v2/captcha/check", data, str(int(time.time() * 1000))) headers.update(h) logger.info(data) response = download_q(self.check_url, headers=headers, cookies=self.cookies, data=data, is_proxy=self.is_proxy) return response.json() def register(self): """ 初始化函数:获得验证码初始化信息请求 :return: """ data = { "secretId": "000", "verifyType": "102", "verifyUuid": self.verify_uuid, "verifyBiz": "461", "biz": "sns_web", "sourceSite": self.source, "version": "1.0.1" } data = json.dumps(data, separators=(',', ':'), ensure_ascii=False) h = get_xs(self.cookies["a1"], "/api/redcaptcha/v2/captcha/register", data, str(int(time.time() * 1000))) headers = self.headers.copy() headers["x-s-common"] = "" headers.update(h) response = download_q(self.register_url, headers=headers, cookies=self.cookies, data=data, is_proxy=self.is_proxy) return response.json() def run(self): """ main: 执行函数 :return: """ try: register_data = self.register() logger.info(f"初始化验证码信息:{register_data}") data = register_data["data"] rid = data["rid"] captcha_info = data["captchaInfo"] source = json.loads(decrypt_info(captcha_info)[:-2]) # 因为有补位则去掉补位结果 logger.info(f"解密验证码初始信息:{source}") bg_url = source["backgroundUrl"] fg_url = source["captchaUrl"] bg_path = download_q(bg_url, headers=self.pic_headers, cookies={}).content fg_path = download_q(fg_url, headers=self.pic_headers, cookies={}).content captcha_info = SignalCaptcha.get_captcha_info(fg_path, bg_path) logger.info(f"生成验证信息:{captcha_info}") result = self.check(rid, captcha_info) logger.info(f"验证结果:{result}") except Exception as e: result = {"msg": "失败", "data": {}} logger.error(e) return result if __name__ == '__main__': verify_uuid = "22318d30-daf4-4fc1-8148-c26d86b0a51a*CiZjkIoB" web_session = "030037a2d377ba382693826598224a7950081e" a1 = "" webId = "" source = "https://www.xiaohongshu.com/explore/65c02be2000000002c028b93" A = SignalCaptcha(web_session, verify_uuid, a1, webId, source=source) res = A.run() print(res)