AutoRpa/rpa_selection/http_util.py

118 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 商城搜索的 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('"'))
# 只留下能放进 HTTP Cookie 头的项。名字必须 ASCII,值必须 latin-1。
#
# 浏览器登录会带上企业名、站点名等中文字段,requests 发不出去。
# 按编码过滤,不按 cookie 名写死,换账号打开浏览器同样适用。
def cookies_for_http(cookies: dict[str, str] | None) -> dict[str, str]:
safe: dict[str, str] = {}
if not cookies:
return safe
for name, value in cookies.items():
key = str(name)
val = "" if value is None else str(value)
try:
key.encode("ascii")
val.encode("latin-1")
except UnicodeEncodeError:
continue
if not key:
continue
safe[key] = val
return safe
# 把能进 HTTP 头的 cookie 写进会话。中文展示字段丢掉。
def set_session_cookies(session: requests.Session, cookies: dict[str, str] | None) -> None:
for name, value in cookies_for_http(cookies).items():
session.cookies.set(name, value)
# 把商城响应收成 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