Files
MediaCrawler/tests/test_douyin_aweme_detail_params.py
T
程序员阿江(Relakkes) cf513e70a1 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 视频均下载成功。
2026-09-18 17:00:11 +08:00

108 lines
3.6 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.
# -*- 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"] == ""