基于实战经验重构 JSVMP 分析框架: - SKILL.md: 新增路径选择决策树(路径 A 算法追踪 vs 路径 B 环境伪装) - SKILL.md: 路径 B「环境伪装六步法」完整方法论(采集→对比→分级→补丁→验证→端到端) - SKILL.md: 新增场景 10「JSVMP + jsdom 环境伪装」、解法模式 E - SKILL.md: 标注三板斧适用边界与局限,经验法则扩充至 22 条 - references/jsdom-env-patches.md: 新增 jsdom 环境补丁知识库(Function.toString 三层防御 / plugins 完整模拟 / DOM 布局属性 / 30+ API 存根) - cases/: 新增经验库系统(模板 + 首个 JSVMP 实战案例含 58 项环境检测差异全表) - references/jsvmp-analysis.md: JSVMP 专项分析详细指南 - references/troubleshooting.md: 常见问题排查指南 - templates/python-request/: Python 项目模板 - 各 references 文档增强、Python 技术栈支持、MCP v1.1 适配(44→52 工具) Made-with: Cursor
122 lines
4.3 KiB
Python
122 lines
4.3 KiB
Python
"""
|
|
HTTP 请求封装
|
|
包含 Cookie 管理、重试机制、频率控制
|
|
"""
|
|
import time
|
|
import random
|
|
import requests
|
|
from typing import Optional
|
|
|
|
|
|
class RequestClient:
|
|
"""HTTP 客户端封装"""
|
|
|
|
def __init__(
|
|
self,
|
|
cookies: Optional[dict] = None,
|
|
headers: Optional[dict] = None,
|
|
max_retries: int = 3,
|
|
retry_delay: float = 2.0,
|
|
):
|
|
self.session = requests.Session()
|
|
self.max_retries = max_retries
|
|
self.retry_delay = retry_delay
|
|
|
|
default_headers = {
|
|
"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"
|
|
),
|
|
"Accept": "application/json, text/plain, */*",
|
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
"Connection": "keep-alive",
|
|
}
|
|
|
|
if headers:
|
|
default_headers.update(headers)
|
|
self.session.headers.update(default_headers)
|
|
|
|
if cookies:
|
|
for name, value in cookies.items():
|
|
self.session.cookies.set(name, value)
|
|
|
|
def request(self, method: str, url: str, **kwargs) -> requests.Response:
|
|
"""带重试的请求"""
|
|
last_error = None
|
|
|
|
for attempt in range(1, self.max_retries + 1):
|
|
try:
|
|
response = self.session.request(method, url, **kwargs)
|
|
|
|
if response.status_code == 429:
|
|
wait = self.retry_delay * (2 ** attempt) + random.uniform(0, 1)
|
|
print(f" 触发频率限制,等待 {wait:.1f}s 后重试 ({attempt}/{self.max_retries})")
|
|
time.sleep(wait)
|
|
continue
|
|
|
|
if response.status_code in (403, 412):
|
|
print(f" 请求被拒绝 ({response.status_code}),可能需要检查 Cookie 或签名")
|
|
response.raise_for_status()
|
|
|
|
if response.status_code >= 500:
|
|
wait = self.retry_delay * attempt + random.uniform(0, 1)
|
|
print(f" 服务端错误 ({response.status_code}),等待 {wait:.1f}s 后重试 ({attempt}/{self.max_retries})")
|
|
time.sleep(wait)
|
|
continue
|
|
|
|
return response
|
|
|
|
except requests.RequestException as e:
|
|
last_error = e
|
|
if attempt < self.max_retries:
|
|
wait = self.retry_delay * attempt
|
|
print(f" 请求异常: {e},等待 {wait:.1f}s 后重试 ({attempt}/{self.max_retries})")
|
|
time.sleep(wait)
|
|
|
|
raise last_error or Exception("请求失败,已达最大重试次数")
|
|
|
|
def get(self, url: str, **kwargs) -> requests.Response:
|
|
return self.request("GET", url, **kwargs)
|
|
|
|
def post(self, url: str, **kwargs) -> requests.Response:
|
|
return self.request("POST", url, **kwargs)
|
|
|
|
def request_with_delay(
|
|
self, method: str, url: str, delay: float = 1.0, **kwargs
|
|
) -> requests.Response:
|
|
"""带延迟的请求(避免触发频率限制)"""
|
|
jitter = random.uniform(0, delay * 0.3)
|
|
time.sleep(delay + jitter)
|
|
return self.request(method, url, **kwargs)
|
|
|
|
def set_cookie(self, name: str, value: str, domain: str = ""):
|
|
"""设置 Cookie"""
|
|
self.session.cookies.set(name, value, domain=domain or None)
|
|
|
|
def update_cookies_from_response(self, response: requests.Response):
|
|
"""从响应中更新 Cookie"""
|
|
self.session.cookies.update(response.cookies)
|
|
|
|
def get_cookie_string(self) -> str:
|
|
"""获取 Cookie 字符串"""
|
|
return "; ".join(f"{c.name}={c.value}" for c in self.session.cookies)
|
|
|
|
|
|
# ====== curl_cffi 版本(对抗 TLS 指纹检测时使用)======
|
|
#
|
|
# from curl_cffi import requests as curl_requests
|
|
#
|
|
# class CurlRequestClient:
|
|
# """使用 curl_cffi 的 HTTP 客户端,支持浏览器 TLS 指纹模拟"""
|
|
#
|
|
# def __init__(self, impersonate: str = "chrome120"):
|
|
# self.session = curl_requests.Session(impersonate=impersonate)
|
|
#
|
|
# def get(self, url: str, **kwargs):
|
|
# return self.session.get(url, **kwargs)
|
|
#
|
|
# def post(self, url: str, **kwargs):
|
|
# return self.session.post(url, **kwargs)
|