type: 收窄虎牙 OCR 与登录类型
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from requests.cookies import RequestsCookieJar
|
||||||
|
|
||||||
|
|
||||||
def cookie_pairs(cookie: str) -> list[tuple[str, str]]:
|
def cookie_pairs(cookie: str) -> list[tuple[str, str]]:
|
||||||
@@ -36,16 +37,25 @@ def normalize_cookie_pairs(pairs: Iterable[tuple[str, str]]) -> str:
|
|||||||
return "; ".join(f"{key}={values[key]}" for key in ordered_keys)
|
return "; ".join(f"{key}={values[key]}" for key in ordered_keys)
|
||||||
|
|
||||||
|
|
||||||
def normalize_huya_cookie(cookie: str | Mapping[str, str] | requests.cookies.RequestsCookieJar | None) -> str:
|
def normalize_huya_cookie(
|
||||||
|
cookie: str | Mapping[str, str] | RequestsCookieJar | None,
|
||||||
|
) -> str:
|
||||||
"""把 Cookie 字符串、dict 或 CookieJar 转成去重后的浏览器 Cookie 字符串。"""
|
"""把 Cookie 字符串、dict 或 CookieJar 转成去重后的浏览器 Cookie 字符串。"""
|
||||||
if not cookie:
|
if not cookie:
|
||||||
return ""
|
return ""
|
||||||
if isinstance(cookie, str):
|
if isinstance(cookie, str):
|
||||||
return normalize_cookie_pairs(cookie_pairs(cookie))
|
return normalize_cookie_pairs(cookie_pairs(cookie))
|
||||||
return normalize_cookie_pairs(cookie.items())
|
return normalize_cookie_pairs(
|
||||||
|
[
|
||||||
|
(str(key), "" if value is None else str(value))
|
||||||
|
for key, value in cookie.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def cookie_value(cookie: str | Mapping[str, str] | requests.cookies.RequestsCookieJar | None, key: str) -> str:
|
def cookie_value(
|
||||||
|
cookie: str | Mapping[str, str] | RequestsCookieJar | None, key: str
|
||||||
|
) -> str:
|
||||||
"""从 Cookie 中读取 key;有重复时以最后一次出现为准。"""
|
"""从 Cookie 中读取 key;有重复时以最后一次出现为准。"""
|
||||||
if not cookie or not key:
|
if not cookie or not key:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
+68
-39
@@ -14,6 +14,7 @@ from http.cookies import SimpleCookie
|
|||||||
from urllib.parse import quote, urlsplit, urlunsplit
|
from urllib.parse import quote, urlsplit, urlunsplit
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from requests.cookies import RequestsCookieJar
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .cookie_utils import normalize_huya_cookie
|
from .cookie_utils import normalize_huya_cookie
|
||||||
@@ -75,7 +76,7 @@ def password_sha1(password: str) -> str:
|
|||||||
return hashlib.sha1(password.encode("utf-8")).hexdigest()
|
return hashlib.sha1(password.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def cookie_string(cookies: requests.cookies.RequestsCookieJar | Mapping[str, str]) -> str:
|
def cookie_string(cookies: RequestsCookieJar | Mapping[str, str]) -> str:
|
||||||
"""把 CookieJar/dict 转成浏览器 Cookie 字符串。"""
|
"""把 CookieJar/dict 转成浏览器 Cookie 字符串。"""
|
||||||
return normalize_huya_cookie(cookies)
|
return normalize_huya_cookie(cookies)
|
||||||
|
|
||||||
@@ -116,15 +117,19 @@ def encode_behavior(page: str = HUYA_PAGE_URL) -> str:
|
|||||||
actions.append({"id": action_id, "d": elapsed, "time": now})
|
actions.append({"id": action_id, "d": elapsed, "time": now})
|
||||||
now += random.randint(120, 400)
|
now += random.randint(120, 400)
|
||||||
elapsed += random.randint(120, 400)
|
elapsed += random.randint(120, 400)
|
||||||
actions.append({
|
actions.append(
|
||||||
"id": "11",
|
{
|
||||||
"x": random.randint(430, 560),
|
"id": "11",
|
||||||
"y": random.randint(250, 330),
|
"x": random.randint(430, 560),
|
||||||
"d": elapsed,
|
"y": random.randint(250, 330),
|
||||||
"time": now,
|
"d": elapsed,
|
||||||
})
|
"time": now,
|
||||||
|
}
|
||||||
|
)
|
||||||
value = {"furl": page, "curl": page, "user_action": actions}
|
value = {"furl": page, "curl": page, "user_action": actions}
|
||||||
return quote(json.dumps(value, separators=(",", ":"), ensure_ascii=False), safe="~()*!.'")
|
return quote(
|
||||||
|
json.dumps(value, separators=(",", ":"), ensure_ascii=False), safe="~()*!.'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HuyaPasswordLogin:
|
class HuyaPasswordLogin:
|
||||||
@@ -177,23 +182,25 @@ class HuyaPasswordLogin:
|
|||||||
self._setup_headers()
|
self._setup_headers()
|
||||||
|
|
||||||
def _setup_headers(self) -> None:
|
def _setup_headers(self) -> None:
|
||||||
self.session.headers.update({
|
self.session.headers.update(
|
||||||
"Accept": "*/*",
|
{
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Accept": "*/*",
|
||||||
"Cache-Control": "no-cache",
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
"Connection": "keep-alive",
|
"Cache-Control": "no-cache",
|
||||||
"Content-Type": "application/json;charset=UTF-8",
|
"Connection": "keep-alive",
|
||||||
"Origin": "https://aq.huya.com",
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
"Pragma": "no-cache",
|
"Origin": "https://aq.huya.com",
|
||||||
"Referer": "https://aq.huya.com/",
|
"Pragma": "no-cache",
|
||||||
"Sec-Fetch-Dest": "empty",
|
"Referer": "https://aq.huya.com/",
|
||||||
"Sec-Fetch-Mode": "cors",
|
"Sec-Fetch-Dest": "empty",
|
||||||
"Sec-Fetch-Site": "same-site",
|
"Sec-Fetch-Mode": "cors",
|
||||||
"User-Agent": self.ua,
|
"Sec-Fetch-Site": "same-site",
|
||||||
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
|
"User-Agent": self.ua,
|
||||||
"sec-ch-ua-mobile": "?0",
|
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
|
||||||
"sec-ch-ua-platform": '"macOS"',
|
"sec-ch-ua-mobile": "?0",
|
||||||
})
|
"sec-ch-ua-platform": '"macOS"',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _safe_url(url: str) -> str:
|
def _safe_url(url: str) -> str:
|
||||||
@@ -202,7 +209,9 @@ class HuyaPasswordLogin:
|
|||||||
|
|
||||||
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
||||||
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
||||||
logger.debug(f"{method.upper()} {self._safe_url(url)} -> {response.status_code}")
|
logger.debug(
|
||||||
|
f"{method.upper()} {self._safe_url(url)} -> {response.status_code}"
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
try:
|
try:
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -219,10 +228,12 @@ class HuyaPasswordLogin:
|
|||||||
logger.debug(f"GET {self._safe_url(url)} -> {response.status_code}")
|
logger.debug(f"GET {self._safe_url(url)} -> {response.status_code}")
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
logger.debug(f"虎牙UDB middle初始化失败: {host}: {exc}")
|
logger.debug(f"虎牙UDB middle初始化失败: {host}: {exc}")
|
||||||
self.session.headers.update({
|
self.session.headers.update(
|
||||||
"Origin": "https://udblgn.huya.com",
|
{
|
||||||
"Referer": self.middle_url,
|
"Origin": "https://udblgn.huya.com",
|
||||||
})
|
"Referer": self.middle_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def prepare_device(self) -> str:
|
def prepare_device(self) -> str:
|
||||||
"""获取虎牙风控 sdid(优先 hydevice 高信任指纹,失败降级旧版)。"""
|
"""获取虎牙风控 sdid(优先 hydevice 高信任指纹,失败降级旧版)。"""
|
||||||
@@ -230,8 +241,11 @@ class HuyaPasswordLogin:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 一号一设备: 每账号独立 hydevice 状态, sdid/40hex hdid 不跨账号同源
|
# 一号一设备: 每账号独立 hydevice 状态, sdid/40hex hdid 不跨账号同源
|
||||||
result = get_huya_sdid(app_id="5008", timeout=self.timeout,
|
result = get_huya_sdid(
|
||||||
state_dir=account_state_dir(self.username))
|
app_id="5008",
|
||||||
|
timeout=self.timeout,
|
||||||
|
state_dir=account_state_dir(self.username),
|
||||||
|
)
|
||||||
if result.sdid:
|
if result.sdid:
|
||||||
self.sdid = result.sdid
|
self.sdid = result.sdid
|
||||||
self.sdid_source = result.source
|
self.sdid_source = result.source
|
||||||
@@ -242,12 +256,16 @@ class HuyaPasswordLogin:
|
|||||||
logger.warning("虎牙高信任指纹异常: {},降级旧流程", exc)
|
logger.warning("虎牙高信任指纹异常: {},降级旧流程", exc)
|
||||||
|
|
||||||
token_payload = {"encryptVersion": "1.0.1", "fingerprintVersion": "1.2.41"}
|
token_payload = {"encryptVersion": "1.0.1", "fingerprintVersion": "1.2.41"}
|
||||||
token_res = self._request_json("post", DF_TOKEN_URL, "获取虎牙 df token", json=token_payload)
|
token_res = self._request_json(
|
||||||
|
"post", DF_TOKEN_URL, "获取虎牙 df token", json=token_payload
|
||||||
|
)
|
||||||
token = token_res.get("data", {}).get("token")
|
token = token_res.get("data", {}).get("token")
|
||||||
if not token:
|
if not token:
|
||||||
raise HuyaLoginError(f"获取虎牙 df token 失败: {token_res}")
|
raise HuyaLoginError(f"获取虎牙 df token 失败: {token_res}")
|
||||||
|
|
||||||
collect_res = self._request_json("post", DF_COLLECT_URL, "获取虎牙 sdid", json={"token": token})
|
collect_res = self._request_json(
|
||||||
|
"post", DF_COLLECT_URL, "获取虎牙 sdid", json={"token": token}
|
||||||
|
)
|
||||||
self.sdid = collect_res.get("data", {}).get("sdid", "")
|
self.sdid = collect_res.get("data", {}).get("sdid", "")
|
||||||
if not self.sdid:
|
if not self.sdid:
|
||||||
raise HuyaLoginError(f"获取虎牙 sdid 失败: {collect_res}")
|
raise HuyaLoginError(f"获取虎牙 sdid 失败: {collect_res}")
|
||||||
@@ -303,7 +321,11 @@ class HuyaPasswordLogin:
|
|||||||
from .verification import HuyaNoSessionError, HuyaVerificationSolver
|
from .verification import HuyaNoSessionError, HuyaVerificationSolver
|
||||||
|
|
||||||
solver = HuyaVerificationSolver(
|
solver = HuyaVerificationSolver(
|
||||||
cookie=self.session.cookies.get_dict(),
|
cookie={
|
||||||
|
key: value
|
||||||
|
for key, value in self.session.cookies.get_dict().items()
|
||||||
|
if value is not None
|
||||||
|
},
|
||||||
ua=self.ua,
|
ua=self.ua,
|
||||||
sdid=self.sdid,
|
sdid=self.sdid,
|
||||||
session=self.session,
|
session=self.session,
|
||||||
@@ -323,7 +345,10 @@ class HuyaPasswordLogin:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_credential_error(payload: dict) -> bool:
|
def _is_credential_error(payload: dict) -> bool:
|
||||||
text = json.dumps(payload, ensure_ascii=False)
|
text = json.dumps(payload, ensure_ascii=False)
|
||||||
return any(word in text for word in ("密码错误", "账号或密码", "账号不存在", "用户不存在"))
|
return any(
|
||||||
|
word in text
|
||||||
|
for word in ("密码错误", "账号或密码", "账号不存在", "用户不存在")
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _payload_data(payload: dict) -> dict:
|
def _payload_data(payload: dict) -> dict:
|
||||||
@@ -407,13 +432,17 @@ class HuyaPasswordLogin:
|
|||||||
logger.info(f"虎牙密码登录触发风控: {return_code}")
|
logger.info(f"虎牙密码登录触发风控: {return_code}")
|
||||||
auth_id = self._solve_verification(payload)
|
auth_id = self._solve_verification(payload)
|
||||||
for index in range(3):
|
for index in range(3):
|
||||||
payload = self._password_login_once(auth_id=auth_id, session_data=session_data)
|
payload = self._password_login_once(
|
||||||
|
auth_id=auth_id, session_data=session_data
|
||||||
|
)
|
||||||
return_code = int(payload.get("returnCode") or 0)
|
return_code = int(payload.get("returnCode") or 0)
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
return self._success(payload)
|
return self._success(payload)
|
||||||
if self._is_credential_error(payload):
|
if self._is_credential_error(payload):
|
||||||
raise HuyaCredentialError(f"虎牙账号或密码错误: {payload}")
|
raise HuyaCredentialError(f"虎牙账号或密码错误: {payload}")
|
||||||
session_data = str(self._payload_data(payload).get("sessionData") or session_data)
|
session_data = str(
|
||||||
|
self._payload_data(payload).get("sessionData") or session_data
|
||||||
|
)
|
||||||
if return_code in (10030, 10039):
|
if return_code in (10030, 10039):
|
||||||
logger.info(f"虎牙密码登录二次风控: {return_code} ({index + 1}/3)")
|
logger.info(f"虎牙密码登录二次风控: {return_code} ({index + 1}/3)")
|
||||||
auth_id = self._solve_verification(payload)
|
auth_id = self._solve_verification(payload)
|
||||||
|
|||||||
+188
-151
@@ -36,108 +36,110 @@ SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode"
|
|||||||
SMS_LOGIN_URL = "https://udblgn.huya.com/web/v2/smsLogin"
|
SMS_LOGIN_URL = "https://udblgn.huya.com/web/v2/smsLogin"
|
||||||
DF_TOKEN_URL = "https://df.huya.com/web/df/token"
|
DF_TOKEN_URL = "https://df.huya.com/web/df/token"
|
||||||
DF_COLLECT_URL = "https://df.huya.com/web/df/collect"
|
DF_COLLECT_URL = "https://df.huya.com/web/df/collect"
|
||||||
HUYA_COUNTRY_CALLING_CODES = frozenset({
|
HUYA_COUNTRY_CALLING_CODES = frozenset(
|
||||||
"1",
|
{
|
||||||
"7",
|
"1",
|
||||||
"20",
|
"7",
|
||||||
"27",
|
"20",
|
||||||
"30",
|
"27",
|
||||||
"31",
|
"30",
|
||||||
"32",
|
"31",
|
||||||
"33",
|
"32",
|
||||||
"34",
|
"33",
|
||||||
"36",
|
"34",
|
||||||
"39",
|
"36",
|
||||||
"40",
|
"39",
|
||||||
"41",
|
"40",
|
||||||
"43",
|
"41",
|
||||||
"44",
|
"43",
|
||||||
"45",
|
"44",
|
||||||
"46",
|
"45",
|
||||||
"47",
|
"46",
|
||||||
"48",
|
"47",
|
||||||
"49",
|
"48",
|
||||||
"52",
|
"49",
|
||||||
"55",
|
"52",
|
||||||
"60",
|
"55",
|
||||||
"61",
|
"60",
|
||||||
"62",
|
"61",
|
||||||
"63",
|
"62",
|
||||||
"64",
|
"63",
|
||||||
"65",
|
"64",
|
||||||
"66",
|
"65",
|
||||||
"81",
|
"66",
|
||||||
"82",
|
"81",
|
||||||
"84",
|
"82",
|
||||||
"86",
|
"84",
|
||||||
"90",
|
"86",
|
||||||
"91",
|
"90",
|
||||||
"92",
|
"91",
|
||||||
"93",
|
"92",
|
||||||
"94",
|
"93",
|
||||||
"95",
|
"94",
|
||||||
"98",
|
"95",
|
||||||
"212",
|
"98",
|
||||||
"213",
|
"212",
|
||||||
"216",
|
"213",
|
||||||
"218",
|
"216",
|
||||||
"234",
|
"218",
|
||||||
"254",
|
"234",
|
||||||
"351",
|
"254",
|
||||||
"352",
|
"351",
|
||||||
"353",
|
"352",
|
||||||
"354",
|
"353",
|
||||||
"355",
|
"354",
|
||||||
"356",
|
"355",
|
||||||
"357",
|
"356",
|
||||||
"358",
|
"357",
|
||||||
"359",
|
"358",
|
||||||
"370",
|
"359",
|
||||||
"371",
|
"370",
|
||||||
"372",
|
"371",
|
||||||
"373",
|
"372",
|
||||||
"374",
|
"373",
|
||||||
"375",
|
"374",
|
||||||
"376",
|
"375",
|
||||||
"377",
|
"376",
|
||||||
"378",
|
"377",
|
||||||
"380",
|
"378",
|
||||||
"381",
|
"380",
|
||||||
"382",
|
"381",
|
||||||
"385",
|
"382",
|
||||||
"386",
|
"385",
|
||||||
"387",
|
"386",
|
||||||
"389",
|
"387",
|
||||||
"420",
|
"389",
|
||||||
"421",
|
"420",
|
||||||
"852",
|
"421",
|
||||||
"853",
|
"852",
|
||||||
"855",
|
"853",
|
||||||
"856",
|
"855",
|
||||||
"886",
|
"856",
|
||||||
"960",
|
"886",
|
||||||
"961",
|
"960",
|
||||||
"962",
|
"961",
|
||||||
"963",
|
"962",
|
||||||
"964",
|
"963",
|
||||||
"965",
|
"964",
|
||||||
"966",
|
"965",
|
||||||
"967",
|
"966",
|
||||||
"968",
|
"967",
|
||||||
"971",
|
"968",
|
||||||
"972",
|
"971",
|
||||||
"973",
|
"972",
|
||||||
"974",
|
"973",
|
||||||
"975",
|
"974",
|
||||||
"976",
|
"975",
|
||||||
"977",
|
"976",
|
||||||
"992",
|
"977",
|
||||||
"993",
|
"992",
|
||||||
"994",
|
"993",
|
||||||
"995",
|
"994",
|
||||||
"996",
|
"995",
|
||||||
"998",
|
"996",
|
||||||
})
|
"998",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -154,7 +156,9 @@ class HuyaSmsCodeResult:
|
|||||||
request_id: str = ""
|
request_id: str = ""
|
||||||
|
|
||||||
|
|
||||||
def _split_country_calling_code(digits: str, *, allow_single_digit: bool = True) -> tuple[str, str]:
|
def _split_country_calling_code(
|
||||||
|
digits: str, *, allow_single_digit: bool = True
|
||||||
|
) -> tuple[str, str]:
|
||||||
"""从国际号码中拆出国家/地区区号。
|
"""从国际号码中拆出国家/地区区号。
|
||||||
|
|
||||||
allow_single_digit=False 时不匹配 1/7 等单位区号,避免把国内 1 开头手机号误判成美国号。
|
allow_single_digit=False 时不匹配 1/7 等单位区号,避免把国内 1 开头手机号误判成美国号。
|
||||||
@@ -238,7 +242,9 @@ def normalize_huya_phone(phone: str) -> str:
|
|||||||
# 国内号
|
# 国内号
|
||||||
if digits.startswith("86") and len(digits) == 13:
|
if digits.startswith("86") and len(digits) == 13:
|
||||||
return f"0{digits}"
|
return f"0{digits}"
|
||||||
if len(digits) == 11 and digits.startswith(("13", "14", "15", "16", "17", "18", "19")):
|
if len(digits) == 11 and digits.startswith(
|
||||||
|
("13", "14", "15", "16", "17", "18", "19")
|
||||||
|
):
|
||||||
return f"086{digits}"
|
return f"086{digits}"
|
||||||
if len(digits) == 11:
|
if len(digits) == 11:
|
||||||
return f"086{digits}"
|
return f"086{digits}"
|
||||||
@@ -251,28 +257,34 @@ def encode_sms_behavior(page: str = HUYA_PAGE_URL) -> str:
|
|||||||
"""生成短信登录抓包同款行为轨迹。"""
|
"""生成短信登录抓包同款行为轨迹。"""
|
||||||
now = int(time.time() * 1000) - random.randint(8000, 18000)
|
now = int(time.time() * 1000) - random.randint(8000, 18000)
|
||||||
elapsed = random.randint(1200, 2400)
|
elapsed = random.randint(1200, 2400)
|
||||||
actions = [{
|
actions = [
|
||||||
"id": "12",
|
{
|
||||||
"x": random.randint(520, 620),
|
"id": "12",
|
||||||
"y": random.randint(45, 75),
|
"x": random.randint(520, 620),
|
||||||
"d": elapsed,
|
"y": random.randint(45, 75),
|
||||||
"time": now,
|
"d": elapsed,
|
||||||
}]
|
"time": now,
|
||||||
|
}
|
||||||
|
]
|
||||||
for action_id in ("17", "17"):
|
for action_id in ("17", "17"):
|
||||||
now += random.randint(1800, 4200)
|
now += random.randint(1800, 4200)
|
||||||
elapsed += random.randint(1800, 4200)
|
elapsed += random.randint(1800, 4200)
|
||||||
actions.append({"id": action_id, "d": elapsed, "time": now})
|
actions.append({"id": action_id, "d": elapsed, "time": now})
|
||||||
now += random.randint(80, 240)
|
now += random.randint(80, 240)
|
||||||
elapsed += random.randint(80, 240)
|
elapsed += random.randint(80, 240)
|
||||||
actions.append({
|
actions.append(
|
||||||
"id": "18",
|
{
|
||||||
"x": random.randint(610, 660),
|
"id": "18",
|
||||||
"y": random.randint(165, 195),
|
"x": random.randint(610, 660),
|
||||||
"d": elapsed,
|
"y": random.randint(165, 195),
|
||||||
"time": now,
|
"d": elapsed,
|
||||||
})
|
"time": now,
|
||||||
|
}
|
||||||
|
)
|
||||||
value = {"furl": page, "curl": page, "user_action": actions}
|
value = {"furl": page, "curl": page, "user_action": actions}
|
||||||
return quote(json.dumps(value, separators=(",", ":"), ensure_ascii=False), safe="~()*!.'")
|
return quote(
|
||||||
|
json.dumps(value, separators=(",", ":"), ensure_ascii=False), safe="~()*!.'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HuyaSmsLogin:
|
class HuyaSmsLogin:
|
||||||
@@ -298,6 +310,7 @@ class HuyaSmsLogin:
|
|||||||
self.context = generate_context(self.device_id)
|
self.context = generate_context(self.device_id)
|
||||||
self.middle_url = f"https://udblgn.huya.com/web/middle/{APP_VERSION}/{self.exchange}/https/{self.device_id}"
|
self.middle_url = f"https://udblgn.huya.com/web/middle/{APP_VERSION}/{self.exchange}/https/{self.device_id}"
|
||||||
self.sdid = ""
|
self.sdid = ""
|
||||||
|
self.session_data = ""
|
||||||
|
|
||||||
self.session = session or requests.Session()
|
self.session = session or requests.Session()
|
||||||
self.session.trust_env = False
|
self.session.trust_env = False
|
||||||
@@ -308,23 +321,25 @@ class HuyaSmsLogin:
|
|||||||
self._setup_headers()
|
self._setup_headers()
|
||||||
|
|
||||||
def _setup_headers(self) -> None:
|
def _setup_headers(self) -> None:
|
||||||
self.session.headers.update({
|
self.session.headers.update(
|
||||||
"Accept": "*/*",
|
{
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Accept": "*/*",
|
||||||
"Cache-Control": "no-cache",
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
"Connection": "keep-alive",
|
"Cache-Control": "no-cache",
|
||||||
"Content-Type": "application/json;charset=UTF-8",
|
"Connection": "keep-alive",
|
||||||
"Origin": "https://aq.huya.com",
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
"Pragma": "no-cache",
|
"Origin": "https://aq.huya.com",
|
||||||
"Referer": "https://aq.huya.com/",
|
"Pragma": "no-cache",
|
||||||
"Sec-Fetch-Dest": "empty",
|
"Referer": "https://aq.huya.com/",
|
||||||
"Sec-Fetch-Mode": "cors",
|
"Sec-Fetch-Dest": "empty",
|
||||||
"Sec-Fetch-Site": "same-site",
|
"Sec-Fetch-Mode": "cors",
|
||||||
"User-Agent": self.ua,
|
"Sec-Fetch-Site": "same-site",
|
||||||
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
|
"User-Agent": self.ua,
|
||||||
"sec-ch-ua-mobile": "?0",
|
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
|
||||||
"sec-ch-ua-platform": '"macOS"',
|
"sec-ch-ua-mobile": "?0",
|
||||||
})
|
"sec-ch-ua-platform": '"macOS"',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _safe_url(url: str) -> str:
|
def _safe_url(url: str) -> str:
|
||||||
@@ -339,7 +354,9 @@ class HuyaSmsLogin:
|
|||||||
|
|
||||||
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
||||||
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
||||||
logger.debug(f"{method.upper()} {self._safe_url(url)} -> {response.status_code}")
|
logger.debug(
|
||||||
|
f"{method.upper()} {self._safe_url(url)} -> {response.status_code}"
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
try:
|
try:
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -356,20 +373,26 @@ class HuyaSmsLogin:
|
|||||||
logger.debug(f"GET {self._safe_url(url)} -> {response.status_code}")
|
logger.debug(f"GET {self._safe_url(url)} -> {response.status_code}")
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
logger.debug(f"虎牙UDB middle初始化失败: {host}: {exc}")
|
logger.debug(f"虎牙UDB middle初始化失败: {host}: {exc}")
|
||||||
self.session.headers.update({
|
self.session.headers.update(
|
||||||
"Origin": "https://udblgn.huya.com",
|
{
|
||||||
"Referer": self.middle_url,
|
"Origin": "https://udblgn.huya.com",
|
||||||
})
|
"Referer": self.middle_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def prepare_device(self) -> str:
|
def prepare_device(self) -> str:
|
||||||
"""获取虎牙风控 sdid。"""
|
"""获取虎牙风控 sdid。"""
|
||||||
token_payload = {"encryptVersion": "1.0.1", "fingerprintVersion": "1.2.41"}
|
token_payload = {"encryptVersion": "1.0.1", "fingerprintVersion": "1.2.41"}
|
||||||
token_res = self._request_json("post", DF_TOKEN_URL, "获取虎牙 df token", json=token_payload)
|
token_res = self._request_json(
|
||||||
|
"post", DF_TOKEN_URL, "获取虎牙 df token", json=token_payload
|
||||||
|
)
|
||||||
token = token_res.get("data", {}).get("token")
|
token = token_res.get("data", {}).get("token")
|
||||||
if not token:
|
if not token:
|
||||||
raise HuyaLoginError(f"获取虎牙 df token 失败: {token_res}")
|
raise HuyaLoginError(f"获取虎牙 df token 失败: {token_res}")
|
||||||
|
|
||||||
collect_res = self._request_json("post", DF_COLLECT_URL, "获取虎牙 sdid", json={"token": token})
|
collect_res = self._request_json(
|
||||||
|
"post", DF_COLLECT_URL, "获取虎牙 sdid", json={"token": token}
|
||||||
|
)
|
||||||
self.sdid = collect_res.get("data", {}).get("sdid", "")
|
self.sdid = collect_res.get("data", {}).get("sdid", "")
|
||||||
if not self.sdid:
|
if not self.sdid:
|
||||||
raise HuyaLoginError(f"获取虎牙 sdid 失败: {collect_res}")
|
raise HuyaLoginError(f"获取虎牙 sdid 失败: {collect_res}")
|
||||||
@@ -380,7 +403,11 @@ class HuyaSmsLogin:
|
|||||||
from .verification import HuyaVerificationSolver
|
from .verification import HuyaVerificationSolver
|
||||||
|
|
||||||
solver = HuyaVerificationSolver(
|
solver = HuyaVerificationSolver(
|
||||||
cookie=self.session.cookies.get_dict(),
|
cookie={
|
||||||
|
key: value
|
||||||
|
for key, value in self.session.cookies.get_dict().items()
|
||||||
|
if value is not None
|
||||||
|
},
|
||||||
ua=self.ua,
|
ua=self.ua,
|
||||||
sdid=self.sdid,
|
sdid=self.sdid,
|
||||||
session=self.session,
|
session=self.session,
|
||||||
@@ -511,7 +538,9 @@ class HuyaSmsLogin:
|
|||||||
return_code = int(payload.get("returnCode") or 0)
|
return_code = int(payload.get("returnCode") or 0)
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
cookie = cookie_string(self.session.cookies)
|
cookie = cookie_string(self.session.cookies)
|
||||||
logger.success(f"虎牙短信登录成功: {self.phone}, Cookie长度: {len(cookie)}")
|
logger.success(
|
||||||
|
f"虎牙短信登录成功: {self.phone}, Cookie长度: {len(cookie)}"
|
||||||
|
)
|
||||||
return HuyaLoginResult(
|
return HuyaLoginResult(
|
||||||
success=True,
|
success=True,
|
||||||
cookie=cookie,
|
cookie=cookie,
|
||||||
@@ -563,7 +592,9 @@ class HuyaSmsLogin:
|
|||||||
"middleUrl": self.middle_url,
|
"middleUrl": self.middle_url,
|
||||||
"createdAt": int(time.time()),
|
"createdAt": int(time.time()),
|
||||||
}
|
}
|
||||||
raw = json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
raw = json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
return base64.urlsafe_b64encode(raw).decode("ascii")
|
return base64.urlsafe_b64encode(raw).decode("ascii")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -588,7 +619,11 @@ class HuyaSmsLogin:
|
|||||||
|
|
||||||
login = cls(
|
login = cls(
|
||||||
phone=expected_phone,
|
phone=expected_phone,
|
||||||
cookie=data.get("cookies") or {},
|
cookie={
|
||||||
|
key: value
|
||||||
|
for key, value in (data.get("cookies") or {}).items()
|
||||||
|
if value is not None
|
||||||
|
},
|
||||||
ua=str(data.get("ua") or DEFAULT_UA),
|
ua=str(data.get("ua") or DEFAULT_UA),
|
||||||
proxies=proxies,
|
proxies=proxies,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
@@ -600,10 +635,12 @@ class HuyaSmsLogin:
|
|||||||
login.device_id = str(data.get("deviceId") or login.device_id)
|
login.device_id = str(data.get("deviceId") or login.device_id)
|
||||||
login.middle_url = str(data.get("middleUrl") or login.middle_url)
|
login.middle_url = str(data.get("middleUrl") or login.middle_url)
|
||||||
login.session_data = str(data.get("sessionData") or "")
|
login.session_data = str(data.get("sessionData") or "")
|
||||||
login.session.headers.update({
|
login.session.headers.update(
|
||||||
"Origin": "https://udblgn.huya.com",
|
{
|
||||||
"Referer": login.middle_url,
|
"Origin": "https://udblgn.huya.com",
|
||||||
})
|
"Referer": login.middle_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
return login
|
return login
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -19,7 +20,12 @@ MODEL_DIR = Path(__file__).resolve().parent / "models"
|
|||||||
class YoloOnnx:
|
class YoloOnnx:
|
||||||
"""轻量 YOLO ONNX 推理封装。"""
|
"""轻量 YOLO ONNX 推理封装。"""
|
||||||
|
|
||||||
def __init__(self, model_path: str | Path, classes: list[str], providers: list[str] | None = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
model_path: str | Path,
|
||||||
|
classes: list[str],
|
||||||
|
providers: list[str] | None = None,
|
||||||
|
):
|
||||||
providers = providers or ["CPUExecutionProvider"]
|
providers = providers or ["CPUExecutionProvider"]
|
||||||
ort.set_default_logger_severity(3)
|
ort.set_default_logger_severity(3)
|
||||||
self.session = ort.InferenceSession(str(model_path), providers=providers)
|
self.session = ort.InferenceSession(str(model_path), providers=providers)
|
||||||
@@ -68,7 +74,9 @@ class YoloOnnx:
|
|||||||
return np.expand_dims(input_tensor, axis=0)
|
return np.expand_dims(input_tensor, axis=0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def nms(boxes: np.ndarray, scores: np.ndarray, iou_threshold: float = 0.3) -> list[int]:
|
def nms(
|
||||||
|
boxes: np.ndarray, scores: np.ndarray, iou_threshold: float = 0.3
|
||||||
|
) -> list[int]:
|
||||||
order = scores.argsort()[::-1]
|
order = scores.argsort()[::-1]
|
||||||
keep = []
|
keep = []
|
||||||
|
|
||||||
@@ -84,7 +92,9 @@ class YoloOnnx:
|
|||||||
width = np.maximum(0.0, xx2 - xx1)
|
width = np.maximum(0.0, xx2 - xx1)
|
||||||
height = np.maximum(0.0, yy2 - yy1)
|
height = np.maximum(0.0, yy2 - yy1)
|
||||||
intersection = width * height
|
intersection = width * height
|
||||||
area_i = (boxes[index, 2] - boxes[index, 0]) * (boxes[index, 3] - boxes[index, 1])
|
area_i = (boxes[index, 2] - boxes[index, 0]) * (
|
||||||
|
boxes[index, 3] - boxes[index, 1]
|
||||||
|
)
|
||||||
area_j = (boxes[order[1:], 2] - boxes[order[1:], 0]) * (
|
area_j = (boxes[order[1:], 2] - boxes[order[1:], 0]) * (
|
||||||
boxes[order[1:], 3] - boxes[order[1:], 1]
|
boxes[order[1:], 3] - boxes[order[1:], 1]
|
||||||
)
|
)
|
||||||
@@ -134,20 +144,25 @@ class YoloOnnx:
|
|||||||
for index in keep_indices:
|
for index in keep_indices:
|
||||||
class_id = int(class_ids[index])
|
class_id = int(class_ids[index])
|
||||||
x1, y1, x2, y2 = map(int, xyxy_boxes[index].tolist())
|
x1, y1, x2, y2 = map(int, xyxy_boxes[index].tolist())
|
||||||
results.append({
|
results.append(
|
||||||
"label_id": class_id,
|
{
|
||||||
"label_name": self.names[class_id],
|
"label_id": class_id,
|
||||||
"confidence": float(scores[index]),
|
"label_name": self.names[class_id],
|
||||||
"box_mid_xy": [(x1 + x2) // 2, (y1 + y2) // 2],
|
"confidence": float(scores[index]),
|
||||||
"xyxy": [x1, y1, x2, y2],
|
"box_mid_xy": [(x1 + x2) // 2, (y1 + y2) // 2],
|
||||||
})
|
"xyxy": [x1, y1, x2, y2],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
results.sort(key=lambda item: item["confidence"], reverse=True)
|
results.sort(key=lambda item: item["confidence"], reverse=True)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def detect(self, image: Image.Image) -> list[dict]:
|
def detect(self, image: Image.Image) -> list[dict]:
|
||||||
input_tensor = self.preprocess(image)
|
input_tensor = self.preprocess(image)
|
||||||
outputs = self.session.run([self.output_name], {self.input_name: input_tensor})
|
outputs = cast(
|
||||||
|
list[np.ndarray],
|
||||||
|
self.session.run([self.output_name], {self.input_name: input_tensor}),
|
||||||
|
)
|
||||||
return self.postprocess(outputs)
|
return self.postprocess(outputs)
|
||||||
|
|
||||||
|
|
||||||
@@ -158,7 +173,7 @@ class SimilarityOnnx:
|
|||||||
providers = providers or ["CPUExecutionProvider"]
|
providers = providers or ["CPUExecutionProvider"]
|
||||||
ort.set_default_logger_severity(3)
|
ort.set_default_logger_severity(3)
|
||||||
self.session = ort.InferenceSession(str(model_path), providers=providers)
|
self.session = ort.InferenceSession(str(model_path), providers=providers)
|
||||||
self.input_shape = [64, 64]
|
self.input_shape: tuple[int, int] = (64, 64)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sigmoid(value: np.ndarray) -> np.ndarray:
|
def sigmoid(value: np.ndarray) -> np.ndarray:
|
||||||
@@ -175,24 +190,38 @@ class SimilarityOnnx:
|
|||||||
return Image.open(value)
|
return Image.open(value)
|
||||||
|
|
||||||
def _tensor(self, value) -> np.ndarray:
|
def _tensor(self, value) -> np.ndarray:
|
||||||
image = self._to_image(value).convert("RGB").resize(tuple(reversed(self.input_shape)), 1)
|
image = (
|
||||||
|
self._to_image(value)
|
||||||
|
.convert("RGB")
|
||||||
|
.resize((self.input_shape[1], self.input_shape[0]), 1)
|
||||||
|
)
|
||||||
array = np.array(image).astype(np.float32) / 255.0
|
array = np.array(image).astype(np.float32) / 255.0
|
||||||
return np.expand_dims(np.transpose(array, (2, 0, 1)), 0)
|
return np.expand_dims(np.transpose(array, (2, 0, 1)), 0)
|
||||||
|
|
||||||
def score(self, image_1, image_2) -> int:
|
def score(self, image_1, image_2) -> int:
|
||||||
out = self.session.run(None, {"x1": self._tensor(image_1), "x2": self._tensor(image_2)})
|
out = self.session.run(
|
||||||
similarity = self.sigmoid(out[0])[0][0]
|
None, {"x1": self._tensor(image_1), "x2": self._tensor(image_2)}
|
||||||
|
)
|
||||||
|
similarity = self.sigmoid(cast(np.ndarray, out[0]))[0][0]
|
||||||
return int(round(similarity.item(), 2) * 100)
|
return int(round(similarity.item(), 2) * 100)
|
||||||
|
|
||||||
|
|
||||||
class HuyaCaptchaOcr:
|
class HuyaCaptchaOcr:
|
||||||
"""封装虎牙滑块和点选识别。"""
|
"""封装虎牙滑块和点选识别。"""
|
||||||
|
|
||||||
def __init__(self, model_dir: str | Path = MODEL_DIR, providers: list[str] | None = None):
|
def __init__(
|
||||||
|
self, model_dir: str | Path = MODEL_DIR, providers: list[str] | None = None
|
||||||
|
):
|
||||||
model_dir = Path(model_dir)
|
model_dir = Path(model_dir)
|
||||||
self.similarity = SimilarityOnnx(model_dir / "weights.onnx", providers=providers)
|
self.similarity = SimilarityOnnx(
|
||||||
self.click_model = YoloOnnx(model_dir / "best.onnx", classes=["target", "char"], providers=providers)
|
model_dir / "weights.onnx", providers=providers
|
||||||
self.slider_model = YoloOnnx(model_dir / "slider_2.onnx", classes=["slider"], providers=providers)
|
)
|
||||||
|
self.click_model = YoloOnnx(
|
||||||
|
model_dir / "best.onnx", classes=["target", "char"], providers=providers
|
||||||
|
)
|
||||||
|
self.slider_model = YoloOnnx(
|
||||||
|
model_dir / "slider_2.onnx", classes=["slider"], providers=providers
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _open_image(image_bytes: bytes) -> Image.Image:
|
def _open_image(image_bytes: bytes) -> Image.Image:
|
||||||
@@ -209,11 +238,16 @@ class HuyaCaptchaOcr:
|
|||||||
"""识别点选坐标,返回按目标顺序排列的点击点。"""
|
"""识别点选坐标,返回按目标顺序排列的点击点。"""
|
||||||
image = self._open_image(image_bytes)
|
image = self._open_image(image_bytes)
|
||||||
detections = self.click_model.detect(image)
|
detections = self.click_model.detect(image)
|
||||||
results = [{**item, "cropped_image": image.crop(tuple(item["xyxy"]))} for item in detections]
|
results = [
|
||||||
|
{**item, "cropped_image": image.crop(tuple(item["xyxy"]))}
|
||||||
|
for item in detections
|
||||||
|
]
|
||||||
|
|
||||||
char_list = [item for item in results if item.get("label_name") == "char"]
|
char_list = [item for item in results if item.get("label_name") == "char"]
|
||||||
target_list = [item for item in results if item.get("label_name") == "target"]
|
target_list = [item for item in results if item.get("label_name") == "target"]
|
||||||
target_list = [item for item in target_list if item["xyxy"][2] - item["xyxy"][0] > 10]
|
target_list = [
|
||||||
|
item for item in target_list if item["xyxy"][2] - item["xyxy"][0] > 10
|
||||||
|
]
|
||||||
char_list.sort(key=lambda item: item["xyxy"][0])
|
char_list.sort(key=lambda item: item["xyxy"][0])
|
||||||
target_list.sort(key=lambda item: item["xyxy"][0])
|
target_list.sort(key=lambda item: item["xyxy"][0])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user