probe_creators.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. """
  2. API 结构探针脚本 v2
  3. 目标:用最低接口成本测通 小红书(JustOne PGY) 和 抖音(TikHub 代理) 创作者搜索。
  4. 注意:TikHub 使用代理地址 https://server.fmode.cn/thapi 规避跨域问题。
  5. 每个测试只调用 1 次,count/pageSize=2,节省 API 成本。
  6. """
  7. import urllib.request
  8. import urllib.parse
  9. import json
  10. import sys
  11. import time
  12. # Windows GBK 终端强制 UTF-8 输出
  13. if hasattr(sys.stdout, 'reconfigure'):
  14. sys.stdout.reconfigure(encoding='utf-8', errors='replace')
  15. if hasattr(sys.stderr, 'reconfigure'):
  16. sys.stderr.reconfigure(encoding='utf-8', errors='replace')
  17. # ─────────────── API Keys & 地址 ───────────────
  18. JUSTONE_TOKEN = "wT0w78sxrwlrpJrQ"
  19. TIKHUB_TOKEN = "oL4nMZwrhXCro1+TQYye0BL8LASyaphtx0LUmGuRCfqnm1CeBFfbdmLAGQ=="
  20. JUSTONE_BASE = "https://api.justoneapi.com"
  21. TIKHUB_PROXY = "https://server.fmode.cn/thapi" # TikHub 代理,规避跨域
  22. KEYWORD = "护肤"
  23. # ─────────────── 工具函数 ───────────────
  24. def get(url: str, headers: dict | None = None, retry301: int = 3) -> dict:
  25. for attempt in range(1, retry301 + 2):
  26. req = urllib.request.Request(url, headers=headers or {})
  27. try:
  28. with urllib.request.urlopen(req, timeout=20) as resp:
  29. body = resp.read().decode("utf-8")
  30. data = json.loads(body)
  31. if data.get("code") == 301 and attempt <= retry301:
  32. print(f" [301] RETRY ({attempt}/{retry301})...")
  33. time.sleep(3)
  34. continue
  35. return data
  36. except urllib.error.HTTPError as e:
  37. body = e.read().decode("utf-8")
  38. print(f" [HTTP {e.code}] {body[:300]}")
  39. return {}
  40. except Exception as ex:
  41. print(f" [ERROR] {ex}")
  42. return {}
  43. return {}
  44. def post(url: str, body: dict, headers: dict | None = None) -> dict:
  45. """POST JSON body 请求"""
  46. payload = json.dumps(body).encode("utf-8")
  47. h = dict(headers or {})
  48. h["Content-Type"] = "application/json"
  49. req = urllib.request.Request(url, data=payload, headers=h, method="POST")
  50. try:
  51. with urllib.request.urlopen(req, timeout=20) as resp:
  52. return json.loads(resp.read().decode("utf-8"))
  53. except urllib.error.HTTPError as e:
  54. body_resp = e.read().decode("utf-8")
  55. print(f" [HTTP {e.code}] {body_resp[:300]}")
  56. return {}
  57. except Exception as ex:
  58. print(f" [ERROR] {ex}")
  59. return {}
  60. def print_section(title: str):
  61. print("\n" + "=" * 60)
  62. print(f" {title}")
  63. print("=" * 60)
  64. def describe_keys(obj: dict, indent: int = 0, max_depth: int = 3):
  65. """递归打印对象的键和值类型(不打印完整值,节省屏幕空间)"""
  66. prefix = " " * indent
  67. for k, v in obj.items():
  68. if isinstance(v, dict):
  69. print(f"{prefix}[dict] {k}:")
  70. if indent < max_depth:
  71. describe_keys(v, indent + 1, max_depth)
  72. elif isinstance(v, list):
  73. print(f"{prefix}[list({len(v)})] {k}")
  74. if v and isinstance(v[0], dict) and indent < max_depth:
  75. print(f"{prefix} └─ 第一个元素 keys:")
  76. describe_keys(v[0], indent + 2, max_depth)
  77. else:
  78. display = repr(v)[:80]
  79. print(f"{prefix}[{type(v).__name__}] {k} = {display}")
  80. # ─────────────── 1. 小红书 JustOne PGY 创作者搜索 ───────────────
  81. def test_xhs_justone():
  82. print_section("1. 小红书 — JustOne 蒲公英 Creator Search")
  83. params = urllib.parse.urlencode({
  84. "token": JUSTONE_TOKEN,
  85. "keyword": KEYWORD,
  86. "page": 1,
  87. "searchType": "NOTE",
  88. "fansNumberLower": 20000,
  89. "fansNumberUpper": 500000,
  90. "gender": "ALL",
  91. })
  92. url = f"{JUSTONE_BASE}/api/xiaohongshu-pgy/api/solar/cooperator/blogger/v2/v1?{params}"
  93. print(f" 请求 URL: {url.replace(JUSTONE_TOKEN, '***')}\n")
  94. data = get(url)
  95. if not data:
  96. print(" ⚠ 无响应")
  97. return
  98. print(f" 顶层 code : {data.get('code')}")
  99. print(f" 顶层 message: {data.get('message')}")
  100. print(f"\n 完整响应结构:")
  101. describe_keys(data)
  102. # 定位列表
  103. d = data.get("data", {})
  104. if d:
  105. for list_key in ("kols", "bloggerList", "list", "items", "result", "authors"):
  106. lst = d.get(list_key)
  107. if lst and isinstance(lst, list):
  108. print(f"\n [OK] 找到列表字段: data.data.{list_key},共 {len(lst)} 条")
  109. if lst:
  110. print(f"\n 第一条完整数据:")
  111. print(json.dumps(lst[0], ensure_ascii=False, indent=2)[:2000])
  112. return
  113. print(f"\n [WARN] 未在 data.data 中找到已知列表字段,data.data keys: {list(d.keys())}")
  114. # ─────────────── 2. 抖音 JustOne 星图 创作者搜索(多接口尝试)───────────────
  115. DOUYIN_JUSTONE_ENDPOINTS = [
  116. # 当前在用(已知返回 authors 但为空)
  117. ("/api/douyin-xingtu/gw/api/gsearch/search_for_author_square/v1",
  118. {"keyword": KEYWORD, "page": 1, "searchType": "CONTENT"},
  119. "Xingtu search_for_author_square (当前)"),
  120. # JustOne 文档 Douyin Xingtu KOL Keyword Search V1
  121. ("/api/douyin-xingtu/gw/api/gsearch/search_kol/v1",
  122. {"keyword": KEYWORD, "page": 1},
  123. "Xingtu search_kol v1"),
  124. # JustOne 文档 Douyin User Search V2
  125. ("/api/douyin/aweme/v1/search/user/",
  126. {"keyword": KEYWORD, "count": 2, "offset": 0},
  127. "Douyin user search v1 (通用)"),
  128. # 另一种可能路径
  129. ("/api/douyin-xingtu/gw/api/search/author/v1",
  130. {"keyword": KEYWORD, "page": 1},
  131. "Xingtu search author v1"),
  132. ]
  133. def test_douyin_justone():
  134. print_section("2. 抖音 — JustOne 星图 KOL 搜索(逐一探测可用接口)")
  135. for endpoint, extra_params, label in DOUYIN_JUSTONE_ENDPOINTS:
  136. print(f"\n ── {label} ──")
  137. params = urllib.parse.urlencode({
  138. "token": JUSTONE_TOKEN,
  139. **extra_params,
  140. })
  141. url = f"{JUSTONE_BASE}{endpoint}?{params}"
  142. print(f" URL: {url.replace(JUSTONE_TOKEN, '***')}")
  143. data = get(url)
  144. if not data:
  145. print(" ⚠ 无响应,跳过")
  146. continue
  147. code = data.get("code")
  148. msg = data.get("message", "")
  149. print(f" code={code} message={msg}")
  150. if code != 0:
  151. print(f" [SKIP] 接口返回错误 code={code}")
  152. continue
  153. # 成功,打印完整结构
  154. print(f"\n [OK] 接口 [{label}] 返回成功!")
  155. print(f" 完整响应结构:")
  156. describe_keys(data)
  157. # 定位列表
  158. d = data.get("data", {})
  159. for list_key in ("authors", "kols", "kol_list", "list", "items", "result", "userList"):
  160. lst = d.get(list_key) if isinstance(d, dict) else None
  161. if lst is None and isinstance(d, dict):
  162. d2 = d.get("data", {})
  163. lst = d2.get(list_key) if isinstance(d2, dict) else None
  164. if lst and isinstance(lst, list):
  165. print(f"\n [LIST] 字段: {list_key},共 {len(lst)} 条")
  166. if lst:
  167. print(f"\n 第一条完整数据:")
  168. print(json.dumps(lst[0], ensure_ascii=False, indent=2)[:2000])
  169. return
  170. print(f" [WARN] 未找到列表字段,data keys = {list(data.get('data', {}).keys()) if isinstance(data.get('data'), dict) else data.keys()}")
  171. return
  172. print("\n [FAIL] 所有 JustOne 抖音接口均无有效响应")
  173. # ─────────────── 3. 抖音 — TikHub 星图 KOL 搜索(代理地址)───────────────
  174. def test_douyin_tikhub_xingtu():
  175. """
  176. 通过代理 https://server.fmode.cn/thapi 调用 TikHub 星图 KOL 搜索。
  177. 代理规避浏览器跨域,Python 直连同样可用。
  178. 只请求 count=2,节省成本。
  179. """
  180. print_section("3. 抖音 — TikHub 星图 KOL 搜索(代理)")
  181. headers = {"Authorization": f"Bearer {TIKHUB_TOKEN}"}
  182. # 按优先级逐一尝试,成功则停止
  183. # 每个条目: (url, body_dict, label, use_post)
  184. candidates = [
  185. (
  186. f"{TIKHUB_PROXY}/api/v1/douyin/xingtu/fetch_kol_search_v1",
  187. {"keyword": KEYWORD, "page": 1, "count": 2},
  188. "Xingtu KOL 搜索 V1 [GET]",
  189. False
  190. ),
  191. (
  192. f"{TIKHUB_PROXY}/api/v1/douyin/xingtu/fetch_kol_search_v1",
  193. {"keyword": KEYWORD, "page": 1, "count": 2},
  194. "Xingtu KOL 搜索 V1 [POST]",
  195. True
  196. ),
  197. (
  198. f"{TIKHUB_PROXY}/api/v1/douyin/xingtu/fetch_kol_advanced_search_v2",
  199. {"keyword": KEYWORD, "page": 1, "count": 2},
  200. "Xingtu KOL 高级搜索 V2 [POST]",
  201. True
  202. ),
  203. (
  204. f"{TIKHUB_PROXY}/api/v1/douyin/xingtu_v2/get_author_market_fields",
  205. {},
  206. "Xingtu V2 达人广场筛选字段 [GET]",
  207. False
  208. ),
  209. ]
  210. for url_base, body, label, use_post in candidates:
  211. print(f"\n ── {label} ──")
  212. if use_post:
  213. print(f" POST {url_base} body={body}")
  214. data = post(url_base, body, headers)
  215. else:
  216. params = urllib.parse.urlencode(body) if body else ""
  217. url = f"{url_base}?{params}" if params else url_base
  218. print(f" GET {url}")
  219. data = get(url, headers)
  220. if not data:
  221. print(" [SKIP] 无响应")
  222. continue
  223. code = data.get("code")
  224. msg = data.get("message", "")
  225. print(f" code={code} message={str(msg)[:100]}")
  226. if code not in (200, 0):
  227. print(f" [SKIP] 非成功状态")
  228. continue
  229. print(f"\n [OK] {label} 通过!")
  230. print(" 响应结构:")
  231. describe_keys(data)
  232. _find_and_print_list(data, label)
  233. return
  234. print("\n [FAIL] TikHub 星图接口均失败,改用 Douyin 用户搜索")
  235. test_douyin_tikhub_user_search()
  236. def test_douyin_tikhub_user_search():
  237. """
  238. 备用:通过代理调用 TikHub Douyin 用户搜索。
  239. docs 中 UserSearchRequest / UserSearchRequestV2 是 POST body schema,
  240. 故改为 POST + JSON body(之前用 GET 是错误的)。
  241. """
  242. print_section("3b. 抖音备用 — TikHub Douyin 用户搜索(POST,代理)")
  243. headers = {"Authorization": f"Bearer {TIKHUB_TOKEN}"}
  244. # (url, body, label, use_post)
  245. candidates = [
  246. (
  247. f"{TIKHUB_PROXY}/api/v1/douyin/search/fetch_user_search_v2",
  248. {"keyword": KEYWORD, "offset": 0, "count": 2},
  249. "Douyin Search 用户搜索 V2 [POST]",
  250. True
  251. ),
  252. (
  253. f"{TIKHUB_PROXY}/api/v1/douyin/search/fetch_user_search",
  254. {"keyword": KEYWORD, "offset": 0, "count": 2},
  255. "Douyin Search 用户搜索 V1 [POST]",
  256. True
  257. ),
  258. (
  259. f"{TIKHUB_PROXY}/api/v1/douyin/web/fetch_user_search",
  260. {"keyword": KEYWORD, "offset": 0, "count": 2},
  261. "Douyin Web 用户搜索 [GET]",
  262. False
  263. ),
  264. (
  265. f"{TIKHUB_PROXY}/api/v1/douyin/web/fetch_user_search",
  266. {"keyword": KEYWORD, "offset": 0, "count": 2},
  267. "Douyin Web 用户搜索 [POST]",
  268. True
  269. ),
  270. ]
  271. for url_base, body, label, use_post in candidates:
  272. print(f"\n ── {label} ──")
  273. if use_post:
  274. print(f" POST {url_base} body={body}")
  275. data = post(url_base, body, headers)
  276. else:
  277. params = urllib.parse.urlencode(body)
  278. url = f"{url_base}?{params}"
  279. print(f" GET {url}")
  280. data = get(url, headers)
  281. if not data:
  282. print(" [SKIP] 无响应")
  283. continue
  284. code = data.get("code")
  285. msg = data.get("message", "")
  286. print(f" code={code} message={str(msg)[:100]}")
  287. if code not in (200, 0):
  288. print(f" [SKIP] 非成功状态")
  289. continue
  290. print(f"\n [OK] {label} 通过!")
  291. print(" 响应结构:")
  292. describe_keys(data)
  293. _find_and_print_list(data, label)
  294. return
  295. print(" [FAIL] 所有 TikHub 抖音用户搜索接口均失败")
  296. def test_douyin_tikhub_billboard():
  297. """
  298. 测试 TikHub Douyin-Billboard-API 账号搜索。
  299. 搜索用户名或抖音号 — 可能不需要高级订阅。
  300. docs 中 Billboard 端点带有 _post 后缀,故先试 POST 再试 GET。
  301. """
  302. print_section("3c. 抖音 — TikHub Billboard 账号搜索")
  303. headers = {"Authorization": f"Bearer {TIKHUB_TOKEN}"}
  304. url = f"{TIKHUB_PROXY}/api/v1/douyin/billboard/fetch_account_search_list"
  305. body = {"keyword": KEYWORD, "count": 2}
  306. for label, use_post in [("[POST]", True), ("[GET]", False)]:
  307. print(f"\n ── Billboard 账号搜索 {label} ──")
  308. if use_post:
  309. print(f" POST {url} body={body}")
  310. data = post(url, body, headers)
  311. else:
  312. params = urllib.parse.urlencode(body)
  313. print(f" GET {url}?{params}")
  314. data = get(f"{url}?{params}", headers)
  315. if not data:
  316. print(" [SKIP] 无响应")
  317. continue
  318. code = data.get("code")
  319. msg = data.get("message", "")
  320. print(f" code={code} message={str(msg)[:100]}")
  321. if code not in (200, 0):
  322. continue
  323. print(f"\n [OK] Billboard 账号搜索 {label} 通过!")
  324. describe_keys(data)
  325. _find_and_print_list(data, "Billboard")
  326. return
  327. print(" [FAIL] Billboard 账号搜索均失败")
  328. def _find_and_print_list(data: dict, label: str):
  329. """在任意层级自动定位列表字段并打印第一条"""
  330. LIST_KEYS = ("kol_list", "user_list", "authors", "kols", "list",
  331. "items", "result", "data", "user_info")
  332. def search(obj, depth=0):
  333. if depth > 4 or not isinstance(obj, dict):
  334. return False
  335. for k, v in obj.items():
  336. if isinstance(v, list) and v and k in LIST_KEYS:
  337. print(f"\n [LIST] 路径 .{k},共 {len(v)} 条")
  338. print(f" 第一条完整数据:")
  339. print(json.dumps(v[0], ensure_ascii=False, indent=2)[:2500])
  340. return True
  341. for k, v in obj.items():
  342. if isinstance(v, dict):
  343. if search(v, depth + 1):
  344. return True
  345. return False
  346. if not search(data):
  347. print(f" [WARN] 未在响应中找到已知列表字段,完整 keys: {list(data.keys())}")
  348. # ─────────────── 4. 字段解析验证(纯 Python 模拟 TS 解析逻辑)───────────────
  349. def parse_xhs_creator(item: dict) -> dict:
  350. """模拟 justone.service.ts 中 parseXhsCreator 的解析逻辑"""
  351. raw_content = item.get("contentTags", [])
  352. content_tags = []
  353. for t in raw_content:
  354. if isinstance(t, str):
  355. content_tags.append(t)
  356. elif isinstance(t, dict):
  357. tag = t.get("taxonomy1Tag")
  358. if isinstance(tag, str):
  359. content_tags.append(tag)
  360. feature_tags = [v for v in item.get("featureTags", []) if isinstance(v, str)]
  361. fans = item.get("fansNum") or item.get("fansCount") or 0
  362. image_price = item.get("picturePrice") or item.get("imagePrice") or 0
  363. video_price = item.get("videoPrice") or 0
  364. min_price = item.get("lowerPrice") or image_price or 0
  365. cooperate = "active" if item.get("cooperateState") == 1 else ""
  366. gender_map = {"女": "female", "男": "male", "FEMALE": "female", "MALE": "male"}
  367. gender = gender_map.get(str(item.get("gender", "")), str(item.get("gender", "")))
  368. return {
  369. "userId": item.get("userId") or item.get("user_id") or "",
  370. "nickname": item.get("name") or item.get("nickname") or "",
  371. "redId": item.get("redId") or "",
  372. "location": item.get("location") or item.get("city") or "",
  373. "fansCount": int(fans),
  374. "imagePrice": float(image_price),
  375. "videoPrice": float(video_price),
  376. "minPrice": float(min_price),
  377. "cooperationStatus": cooperate,
  378. "personalTags": feature_tags,
  379. "contentTags": content_tags,
  380. "gender": gender,
  381. }
  382. def parse_douyin_xingtu_creator(item: dict) -> dict:
  383. """模拟 justone.service.ts 中 parseDouyinXingtuCreator 的解析逻辑"""
  384. attr = item.get("attribute_datas") or {}
  385. fans = int(attr.get("follower") or attr.get("fans_count") or 0)
  386. gender_map = {"1": "male", "2": "female", "0": ""}
  387. gender = gender_map.get(str(attr.get("gender", "0")), "")
  388. # 从 last_10_items 提取视频标题中的 hashtag 作为 contentTags
  389. content_tags = []
  390. try:
  391. last_items = json.loads(attr.get("last_10_items") or "[]")
  392. for it in last_items[:5]:
  393. title = it.get("item_title", "")
  394. tags = [t.lstrip("#") for t in title.split() if t.startswith("#")]
  395. content_tags.extend(tags)
  396. content_tags = list(dict.fromkeys(content_tags))[:10] # 去重限10个
  397. except Exception:
  398. pass
  399. # keyword_association 作为 personalTags
  400. personal_tags = []
  401. try:
  402. kw_map = json.loads(attr.get("author_thin_mid_word_association_index") or "{}")
  403. personal_tags = list(kw_map.keys())
  404. except Exception:
  405. pass
  406. return {
  407. "userId": str(item.get("star_id") or attr.get("id") or attr.get("core_user_id") or ""),
  408. "nickname": str(attr.get("nickname") or attr.get("name") or ""),
  409. "location": str(attr.get("city") or attr.get("province") or ""),
  410. "fansCount": fans,
  411. "imagePrice": 0,
  412. "videoPrice": 0,
  413. "minPrice": 0,
  414. "cooperationStatus": "active" if attr.get("author_status") == "1" else "",
  415. "personalTags": personal_tags,
  416. "contentTags": content_tags,
  417. "gender": gender,
  418. }
  419. def test_parse_validation():
  420. print_section("4. 字段解析验证(Python 模拟解析,不调用接口)")
  421. # ── XHS 验证 ──
  422. print("\n [XHS] 模拟解析第一条 kol 数据...")
  423. xhs_sample = {
  424. "userId": "5f869dc2000000000101db7c",
  425. "name": "imtender.",
  426. "redId": "1011627787",
  427. "location": "山东 潍坊 潍城区",
  428. "fansCount": 0, # 始终为 0,正确字段是 fansNum
  429. "fansNum": 20022,
  430. "picturePrice": 0.0,
  431. "videoPrice": 4000.0,
  432. "lowerPrice": 4000.0,
  433. "cooperateState": 1,
  434. "gender": "女",
  435. "contentTags": [
  436. {"taxonomy1Tag": "护肤", "taxonomy2Tags": ["护肤合集"]},
  437. {"taxonomy1Tag": "个人护理", "taxonomy2Tags": ["身体护理", "头发产品"]},
  438. ],
  439. "featureTags": ["沉浸式", "vlog", "氛围感", "抗老", "控油", "开箱"],
  440. }
  441. parsed_xhs = parse_xhs_creator(xhs_sample)
  442. print(f" nickname = {parsed_xhs['nickname']!r} (期望: 'imtender.')")
  443. print(f" fansCount = {parsed_xhs['fansCount']} (期望: 20022)")
  444. print(f" imagePrice = {parsed_xhs['imagePrice']} (期望: 0.0)")
  445. print(f" videoPrice = {parsed_xhs['videoPrice']} (期望: 4000.0)")
  446. print(f" minPrice = {parsed_xhs['minPrice']} (期望: 4000.0)")
  447. print(f" contentTags = {parsed_xhs['contentTags']} (期望: ['护肤', '个人护理'])")
  448. print(f" personalTags = {parsed_xhs['personalTags'][:3]} (期望: ['沉浸式', 'vlog', '氛围感']...)")
  449. print(f" gender = {parsed_xhs['gender']!r} (期望: 'female')")
  450. print(f" cooperationStatus = {parsed_xhs['cooperationStatus']!r} (期望: 'active')")
  451. xhs_ok = (
  452. parsed_xhs["nickname"] == "imtender."
  453. and parsed_xhs["fansCount"] == 20022
  454. and parsed_xhs["videoPrice"] == 4000.0
  455. and "护肤" in parsed_xhs["contentTags"]
  456. and parsed_xhs["gender"] == "female"
  457. and parsed_xhs["cooperationStatus"] == "active"
  458. )
  459. print(f"\n XHS 解析{'[PASS]' if xhs_ok else '[FAIL]'}")
  460. # ── Douyin 验证 ──
  461. print("\n [Douyin] 模拟解析第一条 author 数据...")
  462. dy_sample = {
  463. "star_id": "7174756942059929657",
  464. "attribute_datas": {
  465. "follower": "2161358",
  466. "gender": "1",
  467. "city": "湖州",
  468. "author_status": "1",
  469. "author_thin_mid_word_association_index": '{"服帖":0.33,"清爽":0.15}',
  470. "last_10_items": json.dumps([
  471. {"item_title": "愿我们都能在梦幻的世界里被童话治愈 #艾莎 #沉浸式护肤 #解压", "like_cnt": "2158"},
  472. {"item_title": "给男友挑战黄色护肤 #沉浸式护肤 #情侣日常", "like_cnt": "3837"},
  473. ]),
  474. },
  475. }
  476. parsed_dy = parse_douyin_xingtu_creator(dy_sample)
  477. print(f" userId = {parsed_dy['userId']!r} (期望: '7174756942059929657')")
  478. print(f" fansCount = {parsed_dy['fansCount']} (期望: 2161358)")
  479. print(f" gender = {parsed_dy['gender']!r} (期望: 'male')")
  480. print(f" location = {parsed_dy['location']!r} (期望: '湖州')")
  481. print(f" contentTags = {parsed_dy['contentTags']} (期望含: '沉浸式护肤')")
  482. print(f" personalTags = {parsed_dy['personalTags']} (期望: ['服帖', '清爽'])")
  483. print(f" cooperationStatus = {parsed_dy['cooperationStatus']!r} (期望: 'active')")
  484. dy_ok = (
  485. parsed_dy["userId"] == "7174756942059929657"
  486. and parsed_dy["fansCount"] == 2161358
  487. and parsed_dy["gender"] == "male"
  488. and any("护肤" in t for t in parsed_dy["contentTags"])
  489. and "服帖" in parsed_dy["personalTags"]
  490. and parsed_dy["cooperationStatus"] == "active"
  491. )
  492. print(f"\n Douyin 解析{'[PASS]' if dy_ok else '[FAIL]'}")
  493. if xhs_ok and dy_ok:
  494. print("\n [PASS] 所有字段解析验证通过,可以更新 justone.service.ts")
  495. else:
  496. print("\n [FAIL] 解析有误,需先修复 Python 逻辑再对应修改 TS 服务")
  497. # ─────────────── 主程序 ───────────────
  498. if __name__ == "__main__":
  499. print("=" * 60)
  500. print(" API 结构探针 — 关键词:", KEYWORD)
  501. print(" 每平台只调用 1 次,节省成本")
  502. print("=" * 60)
  503. test_xhs_justone() # 小红书:JustOne 蒲公英
  504. test_douyin_justone() # 抖音:JustOne 星图(301→重试→成功)
  505. test_douyin_tikhub_xingtu() # 抖音:TikHub 星图(GET+POST,修正方法)
  506. test_douyin_tikhub_billboard() # 抖音:TikHub Billboard 账号搜索(新增)
  507. test_parse_validation() # 纯 Python 字段解析验证(不调接口)
  508. print("\n" + "=" * 60)
  509. print(" 探针完成。根据结果决定抖音数据源:")
  510. print(" - TikHub Xingtu POST 通:使用 TikHub(有定价),成本低")
  511. print(" - TikHub Billboard 通:可搜索用户,但无定价")
  512. print(" - 均失败:继续用 JustOne 星图(需 301 重试,有定价)")
  513. print("=" * 60)