|
|
@@ -57,6 +57,11 @@ class CrawapiDouyinClient:
|
|
|
default_sort_type: str = "综合排序",
|
|
|
default_publish_time: str = "不限",
|
|
|
default_cursor: str = "0",
|
|
|
+ piaoquantv_account_id: str = "",
|
|
|
+ piaoquantv_duration: str = "不限",
|
|
|
+ piaoquantv_cookie_batch: str = "default",
|
|
|
+ crawler_sort_type: str = "最多点赞",
|
|
|
+ crawler_cursor_default: str = "",
|
|
|
default_account_works_sort_type: str = "最新",
|
|
|
max_results_per_query: int | None = 5,
|
|
|
http_client: Any | None = None,
|
|
|
@@ -68,6 +73,7 @@ class CrawapiDouyinClient:
|
|
|
self._crawapi_base_urls = _dedupe_crawapi_base_urls(
|
|
|
[("primary", self.base_url), ("candidate", self.fallback_base_url)]
|
|
|
)
|
|
|
+ self._legacy_crawapi_base_urls = _legacy_crawapi_base_urls(self._crawapi_base_urls)
|
|
|
self.keyword_path = keyword_path.lstrip("/")
|
|
|
self.blogger_path = blogger_path.lstrip("/")
|
|
|
self.detail_path = detail_path.lstrip("/")
|
|
|
@@ -78,6 +84,11 @@ class CrawapiDouyinClient:
|
|
|
self.default_sort_type = default_sort_type
|
|
|
self.default_publish_time = default_publish_time
|
|
|
self.default_cursor = default_cursor
|
|
|
+ self.piaoquantv_account_id = piaoquantv_account_id or default_crawapi_account_ref
|
|
|
+ self.piaoquantv_duration = piaoquantv_duration
|
|
|
+ self.piaoquantv_cookie_batch = piaoquantv_cookie_batch
|
|
|
+ self.crawler_sort_type = crawler_sort_type
|
|
|
+ self.crawler_cursor_default = crawler_cursor_default
|
|
|
self.default_account_works_sort_type = default_account_works_sort_type
|
|
|
self.max_results_per_query = max_results_per_query
|
|
|
self.http_client = http_client or httpx.Client(timeout=timeout_seconds)
|
|
|
@@ -108,6 +119,17 @@ class CrawapiDouyinClient:
|
|
|
default_sort_type=_env("CONTENTFIND_DOUYIN_DEFAULT_SORT_TYPE", env, default="综合排序"),
|
|
|
default_publish_time=_env("CONTENTFIND_DOUYIN_DEFAULT_PUBLISH_TIME", env, default="不限"),
|
|
|
default_cursor=_env("CONTENTFIND_DOUYIN_DEFAULT_CURSOR", env, default="0"),
|
|
|
+ piaoquantv_account_id=_env(
|
|
|
+ "CONTENTFIND_DOUYIN_PIAOQUANTV_ACCOUNT_ID",
|
|
|
+ env,
|
|
|
+ default=_env("CONTENTFIND_DOUYIN_DEFAULT_ACCOUNT_ID", env, default=""),
|
|
|
+ ),
|
|
|
+ piaoquantv_duration=_env("CONTENTFIND_DOUYIN_PIAOQUANTV_DURATION", env, default="不限"),
|
|
|
+ piaoquantv_cookie_batch=_env(
|
|
|
+ "CONTENTFIND_DOUYIN_PIAOQUANTV_COOKIE_BATCH", env, default="default"
|
|
|
+ ),
|
|
|
+ crawler_sort_type=_env("CONTENTFIND_DOUYIN_CRAWLER_SORT_TYPE", env, default="最多点赞"),
|
|
|
+ crawler_cursor_default=_env("CONTENTFIND_DOUYIN_CRAWLER_CURSOR_DEFAULT", env, default=""),
|
|
|
default_account_works_sort_type=_env(
|
|
|
"CONTENTFIND_DOUYIN_ACCOUNT_WORKS_DEFAULT_SORT_TYPE", env, default="最新"
|
|
|
),
|
|
|
@@ -131,18 +153,7 @@ class CrawapiDouyinClient:
|
|
|
query: dict[str, Any],
|
|
|
max_results_per_query: int | None,
|
|
|
) -> list[dict[str, Any]]:
|
|
|
- payload = {
|
|
|
- "keyword": query["search_query"],
|
|
|
- "content_type": self.default_content_type,
|
|
|
- "sort_type": self.default_sort_type,
|
|
|
- "publish_time": self.default_publish_time,
|
|
|
- "cursor": str(query.get("page_cursor") or self.default_cursor),
|
|
|
- RAW_AUTHOR_ACCOUNT_KEY: self.default_crawapi_account_ref,
|
|
|
- }
|
|
|
- data = self._post_json(
|
|
|
- self.keyword_path, payload, operation="keyword_search",
|
|
|
- rate_limit_bucket=SEARCH_RATE_LIMIT_BUCKET,
|
|
|
- )
|
|
|
+ data = self._post_keyword_json(query)
|
|
|
data_block = data.get("data", {}) if isinstance(data.get("data"), dict) else {}
|
|
|
items = data_block.get("data", []) if isinstance(data_block.get("data"), list) else []
|
|
|
has_more = bool(data_block.get("has_more", False))
|
|
|
@@ -351,6 +362,52 @@ class CrawapiDouyinClient:
|
|
|
http_client=self.http_client,
|
|
|
)
|
|
|
|
|
|
+ def _post_keyword_json(self, query: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ attempts: list[dict[str, Any]] = []
|
|
|
+ for index, (role, base_url) in enumerate(self._crawapi_base_urls):
|
|
|
+ provider = _keyword_provider_for_base_url(role, base_url)
|
|
|
+ payload = self._keyword_payload_for_provider(provider, query)
|
|
|
+ try:
|
|
|
+ return post_crawapi_json(
|
|
|
+ http_client=self.http_client,
|
|
|
+ base_url=base_url,
|
|
|
+ path=self.keyword_path,
|
|
|
+ payload=payload,
|
|
|
+ operation="keyword_search",
|
|
|
+ timeout_seconds=self.timeout_seconds,
|
|
|
+ rate_limiter=self.rate_limiter,
|
|
|
+ rate_limit_bucket=SEARCH_RATE_LIMIT_BUCKET,
|
|
|
+ business_codes=RATE_LIMIT_BUSINESS_CODES,
|
|
|
+ )
|
|
|
+ except ContentAgentError as exc:
|
|
|
+ attempt = _crawapi_host_attempt_summary(role, base_url, exc, provider=provider)
|
|
|
+ self._raise_crawapi_error(exc, attempts + [attempt])
|
|
|
+ except (CrawapiBusinessError, CrawapiTransientError, RuntimeError) as exc:
|
|
|
+ attempt = _crawapi_host_attempt_summary(role, base_url, exc, provider=provider)
|
|
|
+ if index == len(self._crawapi_base_urls) - 1 or not _should_try_crawapi_candidate(exc):
|
|
|
+ self._raise_crawapi_error(exc, attempts + [attempt])
|
|
|
+ attempts.append(attempt)
|
|
|
+ raise RuntimeError("crawapi keyword_search failed: no base_url configured")
|
|
|
+
|
|
|
+ def _keyword_payload_for_provider(self, provider: str, query: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ if provider == "crawler":
|
|
|
+ return {
|
|
|
+ "content_type": self.default_content_type,
|
|
|
+ "keyword": query["search_query"],
|
|
|
+ "cursor": _query_cursor(query, self.crawler_cursor_default),
|
|
|
+ "sort_type": self.crawler_sort_type,
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ RAW_AUTHOR_ACCOUNT_KEY: self.piaoquantv_account_id,
|
|
|
+ "keyword": query["search_query"],
|
|
|
+ "content_type": self.default_content_type,
|
|
|
+ "sort_type": self.default_sort_type,
|
|
|
+ "publish_time": self.default_publish_time,
|
|
|
+ "duration": self.piaoquantv_duration,
|
|
|
+ "cursor": _query_cursor(query, self.default_cursor),
|
|
|
+ "cookie_batch": self.piaoquantv_cookie_batch,
|
|
|
+ }
|
|
|
+
|
|
|
def _post_json(
|
|
|
self,
|
|
|
path: str,
|
|
|
@@ -359,7 +416,8 @@ class CrawapiDouyinClient:
|
|
|
rate_limit_bucket: str | None = None,
|
|
|
) -> dict[str, Any]:
|
|
|
attempts: list[dict[str, Any]] = []
|
|
|
- for index, (role, base_url) in enumerate(self._crawapi_base_urls):
|
|
|
+ base_urls = self._legacy_crawapi_base_urls
|
|
|
+ for index, (role, base_url) in enumerate(base_urls):
|
|
|
try:
|
|
|
return post_crawapi_json(
|
|
|
http_client=self.http_client,
|
|
|
@@ -377,7 +435,7 @@ class CrawapiDouyinClient:
|
|
|
self._raise_crawapi_error(exc, attempts + [attempt])
|
|
|
except (CrawapiBusinessError, CrawapiTransientError, RuntimeError) as exc:
|
|
|
attempt = _crawapi_host_attempt_summary(role, base_url, exc)
|
|
|
- if index == len(self._crawapi_base_urls) - 1 or not _should_try_crawapi_candidate(exc):
|
|
|
+ if index == len(base_urls) - 1 or not _should_try_crawapi_candidate(exc):
|
|
|
self._raise_crawapi_error(exc, attempts + [attempt])
|
|
|
attempts.append(attempt)
|
|
|
raise RuntimeError(f"crawapi {operation} failed: no base_url configured")
|
|
|
@@ -406,6 +464,32 @@ def _dedupe_crawapi_base_urls(candidates: list[tuple[str, str]]) -> list[tuple[s
|
|
|
return result
|
|
|
|
|
|
|
|
|
+def _legacy_crawapi_base_urls(candidates: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
|
+ aiddit_candidates = [
|
|
|
+ (role, base_url) for role, base_url in candidates if _is_aiddit_crawapi_base_url(base_url)
|
|
|
+ ]
|
|
|
+ return aiddit_candidates or candidates
|
|
|
+
|
|
|
+
|
|
|
+def _is_aiddit_crawapi_base_url(base_url: str) -> bool:
|
|
|
+ return "crawler.aiddit.com" in base_url.lower()
|
|
|
+
|
|
|
+
|
|
|
+def _keyword_provider_for_base_url(role: str, base_url: str) -> str:
|
|
|
+ if _is_aiddit_crawapi_base_url(base_url):
|
|
|
+ return "crawler"
|
|
|
+ normalized = base_url.lower()
|
|
|
+ if "piaoquantv" in normalized:
|
|
|
+ return "piaoquantv"
|
|
|
+ return "crawler" if role == "candidate" else "piaoquantv"
|
|
|
+
|
|
|
+
|
|
|
+def _query_cursor(query: dict[str, Any], default: str) -> str:
|
|
|
+ if "page_cursor" in query:
|
|
|
+ return str(query.get("page_cursor") or "")
|
|
|
+ return str(default)
|
|
|
+
|
|
|
+
|
|
|
def _should_try_crawapi_candidate(exc: Exception) -> bool:
|
|
|
if not isinstance(exc, CrawapiBusinessError):
|
|
|
return True
|
|
|
@@ -426,9 +510,11 @@ def _crawapi_host_attempt_summary(
|
|
|
role: str,
|
|
|
base_url: str,
|
|
|
exc: Exception,
|
|
|
+ provider: str | None = None,
|
|
|
) -> dict[str, Any]:
|
|
|
summary: dict[str, Any] = {
|
|
|
"role": role,
|
|
|
+ "provider": provider or _keyword_provider_for_base_url(role, base_url),
|
|
|
"base_url": base_url.rstrip("/"),
|
|
|
"exception_type": type(exc).__name__,
|
|
|
}
|
|
|
@@ -450,6 +536,9 @@ def _crawapi_host_attempt_summary(
|
|
|
for key in ("status_code", "content_type"):
|
|
|
if key in response_summary:
|
|
|
summary[key] = response_summary.get(key)
|
|
|
+ request_payload_summary = detail.get("request_payload_summary")
|
|
|
+ if isinstance(request_payload_summary, dict):
|
|
|
+ summary["request_payload_summary"] = request_payload_summary
|
|
|
return summary
|
|
|
|
|
|
|