mirror of
https://github.com/NanmiCoder/MediaCrawler.git
synced 2026-09-19 10:57:54 +08:00
fix(dy): 补上 detail 接口的 uifid/verifyFp 风控参数
抖音 detail 接口的 Argus 风控要求 uifid / verifyFp / fp 三个参数,缺一则直接 403:响应体为 "Blocked by ArgusSecurityPlugin Uifid Not Found",补上 uifid 但 verifyFp 不对时换成 "... Signature Not Found"。原实现只传 aweme_id,于是 get_aweme_detail 全线失败,详情和媒体都拿不到。 - 三个参数取自浏览器 cookie:uifid 用 UIFID(缺失时退到 UIFID_TEMP), verifyFp/fp 用 s_v_web_id。必须用 cookie 里的 s_v_web_id——实测 uifid 搭配 自生成的 verifyFp 会被判 Signature Not Found,两者需要同源 - 新增 tests/test_douyin_aweme_detail_params.py(不发网络请求) 验证:抓包比对真实浏览器发出的 detail 请求,参数逐项一致;真实爬取中详情与 74MB 视频均下载成功。
This commit is contained in:
@@ -214,7 +214,19 @@ class DouYinClient(AbstractApiClient, ProxyRefreshMixin):
|
||||
:param aweme_id:
|
||||
:return:
|
||||
"""
|
||||
params = {"aweme_id": aweme_id}
|
||||
# 抖音 detail 接口的 Argus 风控要求这两个参数成套出现,缺一则直接 403
|
||||
# (响应体为 "Blocked by ArgusSecurityPlugin Uifid Not Found"):
|
||||
# uifid = UIFID cookie,没有时退到 UIFID_TEMP
|
||||
# verifyFp / fp = s_v_web_id cookie
|
||||
# 必须用 cookie 里的 s_v_web_id:实测 uifid 搭配自生成的 verifyFp 会被判成
|
||||
# "Signature Not Found",两者同源才能通过。
|
||||
s_v_web_id = self.cookie_dict.get("s_v_web_id", "")
|
||||
params = {
|
||||
"aweme_id": aweme_id,
|
||||
"uifid": self.cookie_dict.get("UIFID") or self.cookie_dict.get("UIFID_TEMP", ""),
|
||||
"verifyFp": s_v_web_id,
|
||||
"fp": s_v_web_id,
|
||||
}
|
||||
headers = copy.copy(self.headers)
|
||||
del headers["Origin"]
|
||||
res = await self.get("/aweme/v1/web/aweme/detail/", params, headers)
|
||||
|
||||
107
tests/test_douyin_aweme_detail_params.py
Normal file
107
tests/test_douyin_aweme_detail_params.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025 relakkes@gmail.com
|
||||
#
|
||||
# This file is part of MediaCrawler project.
|
||||
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/tests/test_douyin_aweme_detail_params.py
|
||||
# GitHub: https://github.com/NanmiCoder
|
||||
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
|
||||
#
|
||||
|
||||
"""抖音 aweme detail 接口的风控参数回归测试。
|
||||
|
||||
回归背景:上游 detail 接口的 Argus 风控要求 ``uifid`` / ``verifyFp`` / ``fp``
|
||||
三个参数,缺一时直接 403,响应体为
|
||||
``Blocked by ArgusSecurityPlugin Uifid Not Found``(补上 uifid 但 verifyFp 不对时
|
||||
换成 ``... Signature Not Found``)。原先只传 aweme_id,于是详情全线失败,
|
||||
连带媒体也拿不到。
|
||||
|
||||
这里不发起任何网络请求,只锁定这三个参数确实被带上、且取值来自浏览器 cookie。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from media_platform.douyin.client import DouYinClient
|
||||
|
||||
AWEME_ID = "7525538910311632128"
|
||||
|
||||
|
||||
class _StubPage:
|
||||
"""占位 page,仅用于构造 client;本测试不会走到 playwright 调用"""
|
||||
|
||||
async def evaluate(self, expression): # noqa: ANN001
|
||||
return {}
|
||||
|
||||
|
||||
def _make_client(cookie_dict: dict) -> DouYinClient:
|
||||
return DouYinClient(
|
||||
headers={
|
||||
"User-Agent": "test-user-agent",
|
||||
"Cookie": "a=1",
|
||||
"Origin": "https://www.douyin.com/",
|
||||
},
|
||||
playwright_page=_StubPage(),
|
||||
cookie_dict=cookie_dict,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aweme_detail_sends_uifid_and_verify_fp(monkeypatch):
|
||||
"""uifid 与 verifyFp/fp 必须成套出现,且都取自 cookie"""
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_get(uri, params=None, headers=None): # noqa: ANN001
|
||||
captured["uri"] = uri
|
||||
captured["params"] = dict(params or {})
|
||||
return {"aweme_detail": {"aweme_id": AWEME_ID}}
|
||||
|
||||
client = _make_client(
|
||||
{"s_v_web_id": "verify_test_fp", "UIFID": "uifid-from-cookie"}
|
||||
)
|
||||
monkeypatch.setattr(client, "get", fake_get)
|
||||
|
||||
await client.get_video_by_id(AWEME_ID)
|
||||
|
||||
assert captured["uri"] == "/aweme/v1/web/aweme/detail/"
|
||||
assert captured["params"]["aweme_id"] == AWEME_ID
|
||||
assert captured["params"]["uifid"] == "uifid-from-cookie"
|
||||
# verifyFp 与 fp 必须同源,且用 cookie 里的 s_v_web_id(自生成的会被判 Signature Not Found)
|
||||
assert captured["params"]["verifyFp"] == "verify_test_fp"
|
||||
assert captured["params"]["fp"] == "verify_test_fp"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aweme_detail_falls_back_to_uifid_temp(monkeypatch):
|
||||
"""没有 UIFID 时退到 UIFID_TEMP"""
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_get(uri, params=None, headers=None): # noqa: ANN001
|
||||
captured["params"] = dict(params or {})
|
||||
return {"aweme_detail": {}}
|
||||
|
||||
client = _make_client({"s_v_web_id": "fp", "UIFID_TEMP": "temp-only"})
|
||||
monkeypatch.setattr(client, "get", fake_get)
|
||||
|
||||
await client.get_video_by_id(AWEME_ID)
|
||||
|
||||
assert captured["params"]["uifid"] == "temp-only"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aweme_detail_without_cookies_still_requests(monkeypatch):
|
||||
"""cookie 缺失时不能抛异常,参数退化为空串(由服务端决定是否放行)"""
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_get(uri, params=None, headers=None): # noqa: ANN001
|
||||
captured["params"] = dict(params or {})
|
||||
return {"aweme_detail": {}}
|
||||
|
||||
client = _make_client({})
|
||||
monkeypatch.setattr(client, "get", fake_get)
|
||||
|
||||
await client.get_video_by_id(AWEME_ID)
|
||||
|
||||
assert captured["params"]["uifid"] == ""
|
||||
assert captured["params"]["verifyFp"] == ""
|
||||
assert captured["params"]["fp"] == ""
|
||||
Reference in New Issue
Block a user