import hashlib import re import time import json import random import base64 import urllib.parse from os import path import subprocess original_popen = subprocess.Popen def patched_popen(*args, **kwargs): if kwargs.get('universal_newlines') or kwargs.get('text'): if 'encoding' not in kwargs: kwargs['encoding'] = 'utf-8' return original_popen(*args, **kwargs) subprocess.Popen = patched_popen import execjs import requests basedir = path.dirname(__file__) static_dir = path.join(basedir, 'static') node_modules = path.join(static_dir, 'node_modules') # 全局唯一 User-Agent:a_bogus 签名、HTTP 请求头、protobuf body、webid 采集等 # 必须全部使用同一个 UA,否则抖音服务端重算 a_bogus 时与请求头 UA 不一致 -> 7911。 # 该值需与浏览器登录上下文(playwright new_context user_agent)保持一致。 DEFAULT_USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ) # 动态编译 JS dy_path = path.join(static_dir, 'dy_ab.js') dy_js = execjs.compile(open(dy_path, 'r', encoding='utf-8').read(), cwd=node_modules) sign_path = path.join(static_dir, 'dy_live_sign.js') sign_js = execjs.compile(open(sign_path, 'r', encoding='utf-8').read(), cwd=node_modules) login_path = path.join(static_dir, 'login.js') login_js = execjs.compile(open(login_path, 'r', encoding='utf-8').read(), cwd=node_modules) def generateSecretPhoneNum(phone): return login_js.call('generateSecretPhoneNum', phone) def generateSecretCode(phone, code): return login_js.call('generateSecretCode', phone, code) def trans_cookies(cookies_str): cookies = {} for i in cookies_str.split("; "): try: parts = i.split('=') key = parts[0].strip() val = '='.join(parts[1:]).strip() # 防御性清洗:过滤掉因为误粘贴 fetch 等包含非法字符或换行的 Cookie 键 if not key or any(c in key for c in "()[]{}'\"\n \t\\"): continue cookies[key] = val except: continue return cookies def generate_req_sign(e, priK): """私信传 obj,其他的拼接""" return dy_js.call('get_req_sign', e, priK) def generate_a_bogus(query, data="", user_agent=None): """query, data 都是拼接字符串。 user_agent 必须与实际发出请求所用的 User-Agent 完全一致(见 DEFAULT_USER_AGENT), 否则抖音服务端用请求头 UA 重算 a_bogus 会对不上,导致 7911 安全校验失败。 """ return dy_js.call('get_ab', query, data, user_agent or DEFAULT_USER_AGENT) def generate_signature(room_id, user_unique_id): raw_string = f"live_id=1,aid=6383,version_code=180800,webcast_sdk_version=1.0.15,room_id={room_id},sub_room_id=,sub_channel_id=,did_rule=3,user_unique_id={user_unique_id},device_platform=web,device_type=,ac=,identity=audience" x_ms_stub = hashlib.md5(raw_string.encode("utf-8")).hexdigest() result = sign_js.call("get_signature", x_ms_stub) return result.get("X-Bogus") def generate_ree_key(prik): """传递私钥""" return dy_js.call('get_ree_key', prik) def generate_bd_ticket_client_data(api, ticket, ts_sign, priK): """传递 query, ticket, ts_sign, priK""" timestamp = int(time.time()) res_sign = f"ticket={ticket}&path={api}×tamp={timestamp}" p = { 'ts_sign': ts_sign, 'req_content': 'ticket,path,timestamp', 'req_sign': generate_req_sign(res_sign, priK), 'timestamp': timestamp, } p = json.dumps(p, ensure_ascii=False, separators=(',', ':')) return base64.urlsafe_b64encode(p.encode('utf-8')).decode('utf-8') def generate_msToken(randomlength=107): random_str = '' base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789=' length = len(base_str) - 1 for _ in range(randomlength): random_str += base_str[random.randint(0, length)] return random_str def generate_fake_webid(random_length=19): random_str = '' base_str = '0123456789' length = len(base_str) - 1 for _ in range(random_length): random_str += base_str[random.randint(0, length)] return random_str def generate_webid(auth=None, url=""): # 优先用已采集到的 web_id(避免每次发送都发起一次阻塞的 HTTP 请求,导致事件循环卡顿) cached = getattr(auth, "web_id", None) if auth is not None else None if cached: return str(cached) if url == "": url = "https://www.douyin.com/discover?modal_id=7376449060384935209" try: from .auth import DouyinAuth headers = { "User-Agent": DEFAULT_USER_AGENT, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "upgrade-insecure-requests": "1" } if auth and auth.cookie_str: headers['cookie'] = auth.cookie_str try: from rpa_engine.runtime_config import requests_proxies proxies = requests_proxies() except Exception: proxies = None response = requests.get( url, headers=headers, verify=False, timeout=10, proxies=proxies ) res_text = response.text user_unique_id = re.findall(r'\\"user_unique_id\\":\\"(.*?)\\"', res_text)[0] return user_unique_id except Exception: return generate_fake_webid() def generate_millisecond(): return int(round(time.time() * 1000)) def normalize_client_cert(cert: str) -> str: """统一 client_cert / sdk_cert 格式为「base64 证书体」。 web_protect.client_cert 与 frontier WS 的 sdk_cert 通常已是 base64(PEM); 若误把 PEM 原文或 frontier 证书二次 base64,会导致 7911。 """ cert = (cert or "").strip() if not cert: return "" if cert.startswith("-----BEGIN"): return base64.b64encode(cert.encode("utf-8")).decode("utf-8") return cert def resolve_proto_device_id(device_id: str = "", web_id: str = "", my_uid: int = 0) -> str: """protobuf / frontier 更倾向使用数字 device_id(通常等于 my_uid)。""" for candidate in (device_id, web_id, str(my_uid or "")): c = str(candidate or "").strip() if c.isdigit(): return c return str(device_id or web_id or "0") def splice_url(params): splice_url_str = '' for key, value in params.items(): if value is None: value = '' splice_url_str += key + '=' + urllib.parse.quote(str(value)) + '&' return splice_url_str[:-1]