90 lines
3.0 KiB
Python
90 lines
3.0 KiB
Python
# 商城搜索的 HTTP:解 cookie、认 JSON、失败按次数重试。
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
from typing import Any
|
||
from urllib.parse import unquote
|
||
|
||
import requests
|
||
import urllib3
|
||
|
||
from mall_config import API_MAX_RETRIES, API_REQUEST_TIMEOUT_SECONDS, API_RETRY_BASE_SECONDS
|
||
|
||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||
|
||
|
||
# 去掉 cookie 值两端引号并做 URL 解码。空值当空串。
|
||
def parse_cookie_value(raw_value: str | None) -> str:
|
||
if not raw_value:
|
||
return ""
|
||
return unquote(raw_value.strip().strip('"'))
|
||
|
||
|
||
# 把商城响应收成 JSON 对象。剥掉 BOM 和防劫持前缀。不是对象就报平台和查询键。
|
||
def parse_response_json_or_raise(
|
||
response: requests.Response,
|
||
platform_name: str,
|
||
query_key: str,
|
||
) -> dict[str, Any]:
|
||
try:
|
||
payload_json = response.json()
|
||
except ValueError as exc:
|
||
raw_text = response.text or ""
|
||
cleaned_text = raw_text.lstrip("\ufeff").strip()
|
||
for prefix in (")]}',", "while(1);"):
|
||
if cleaned_text.startswith(prefix):
|
||
cleaned_text = cleaned_text[len(prefix) :].strip()
|
||
try:
|
||
payload_json = json.loads(cleaned_text)
|
||
except ValueError:
|
||
snippet = cleaned_text.replace("\r", " ").replace("\n", " ")[:200]
|
||
raise RuntimeError(
|
||
f"{platform_name} 接口返回非 JSON,查询键 {query_key},"
|
||
f"HTTP {response.status_code},响应片段:{snippet}"
|
||
) from exc
|
||
|
||
if not isinstance(payload_json, dict):
|
||
raise RuntimeError(
|
||
f"{platform_name} 接口 JSON 根结构异常,查询键 {query_key}:{type(payload_json)}"
|
||
)
|
||
return payload_json
|
||
|
||
|
||
# POST 商城搜索。HTTP 或 JSON 失败时按次数重试,仍失败就报平台和查询键。
|
||
def post_json_with_retry(
|
||
session: requests.Session,
|
||
url: str,
|
||
*,
|
||
platform_name: str,
|
||
query_key: str,
|
||
**request_kwargs: Any,
|
||
) -> dict[str, Any]:
|
||
last_error: Exception | None = None
|
||
for attempt in range(1, API_MAX_RETRIES + 1):
|
||
try:
|
||
response = session.post(
|
||
url,
|
||
timeout=API_REQUEST_TIMEOUT_SECONDS,
|
||
verify=False,
|
||
**request_kwargs,
|
||
)
|
||
response.raise_for_status()
|
||
return parse_response_json_or_raise(response, platform_name, query_key)
|
||
except (requests.RequestException, RuntimeError) as exc:
|
||
last_error = exc
|
||
if attempt >= API_MAX_RETRIES:
|
||
break
|
||
sleep_seconds = API_RETRY_BASE_SECONDS * attempt
|
||
print(
|
||
f"[重试] {platform_name} 查询键 {query_key} 第 {attempt}/{API_MAX_RETRIES} 次失败:"
|
||
f"{exc},{sleep_seconds:.1f} 秒后重试。"
|
||
)
|
||
time.sleep(sleep_seconds)
|
||
|
||
raise RuntimeError(
|
||
f"{platform_name} 接口请求失败,查询键 {query_key},"
|
||
f"重试 {API_MAX_RETRIES} 次后仍未成功:{last_error}"
|
||
) from last_error
|