run_search.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. """用生成的 query 真实搜帖子/视频(抖音 + 微信),媒体存本地 data/,写 search_results.json。
  2. 每法取前 N 条 query,每条 query 每个渠道取 top-K,让三种打法的搜索结果真正拉开差距:
  3. 抖音:search(limit=K) → 逐个 detail → 下载视频+封面到 data/search/douyin/<id>/
  4. 微信:search(limit=K) → 下载每个封面到 data/search/weixin/<hash>/(正文需 detail+token,本期只存封面)
  5. 失败/空按渠道记 error,不中断。抖音对突发调用有强冷却,所有抖音调用走一个 12s 统一闸。
  6. 产出每条记录:{method, query, douyin:{ok:[...],error}, weixin:{ok:[...],error}}。
  7. 用法:PYTHONPATH=. python scripts/run_search.py
  8. """
  9. from __future__ import annotations
  10. import hashlib
  11. import json
  12. from pathlib import Path
  13. from acquisition.crawler import RateLimiter, fetch_post_detail
  14. from acquisition.search import search_keyword, search_weixin
  15. from core.config import Settings
  16. from creation_knowledge.integrations.video_extract import _default_download
  17. ROOT = Path(__file__).resolve().parent.parent
  18. DATA = ROOT / "data"
  19. DEMO = DATA / "queries" / "demo.json"
  20. OUT = DATA / "queries" / "search_results.json"
  21. N = 6 # 每法取前 N 条 query
  22. K_DOUYIN = 1 # 抖音每条 query 只取第 1 个视频(1 搜索+1 详情,躲限流)
  23. K_WEIXIN = 5 # 微信每条 query 取前 K 个封面
  24. _NOOP = RateLimiter(min_interval_seconds=0.0) # 让搜索/详情内部限流让位,统一走外层闸
  25. def _dl(url: str, platform: str, dst: Path, public: str):
  26. """下载到 dst,返回公开路径;失败返回 None。"""
  27. try:
  28. dst.parent.mkdir(parents=True, exist_ok=True)
  29. dst.write_bytes(_default_download(url, platform))
  30. return public
  31. except Exception:
  32. return None
  33. def douyin_topk(query: str, settings: Settings, gate: RateLimiter) -> dict:
  34. """抖音搜 top-K 视频:返回 {ok:[{title,url,cover,video}], error}。"""
  35. try:
  36. gate.wait("douyin")
  37. ids = search_keyword(query, platform="douyin", content_type="视频", limit=K_DOUYIN,
  38. settings=settings, rate_limiter=_NOOP)
  39. except Exception as exc:
  40. return {"ok": [], "error": f"搜索失败: {str(exc)[:50]}"}
  41. if not ids:
  42. return {"ok": [], "error": "未搜到"}
  43. recs = []
  44. for vid in ids[:K_DOUYIN]:
  45. try:
  46. gate.wait("douyin")
  47. post = fetch_post_detail(vid, settings=settings, rate_limiter=_NOOP)
  48. except Exception:
  49. continue # 单条详情失败跳过,不毁整条
  50. base, pub = DATA / "search" / "douyin" / post.id, f"/data/search/douyin/{post.id}"
  51. rec = {"title": post.title, "url": post.url, "cover": None, "video": None}
  52. if post.image_urls:
  53. rec["cover"] = _dl(post.image_urls[0], "douyin", base / "cover.jpg", pub + "/cover.jpg")
  54. if post.video_urls:
  55. rec["video"] = _dl(post.video_urls[0], "douyin", base / "video.mp4", pub + "/video.mp4")
  56. if rec["video"] or rec["cover"]:
  57. recs.append(rec)
  58. return {"ok": recs, "error": None if recs else "详情/下载均失败"}
  59. def weixin_topk(query: str, settings: Settings, rl: RateLimiter) -> dict:
  60. """微信搜 top-K 文章封面:返回 {ok:[{title,url,nick,cover}], error}。"""
  61. try:
  62. arts = search_weixin(query, limit=K_WEIXIN, settings=settings, rate_limiter=rl)
  63. except Exception as exc:
  64. return {"ok": [], "error": f"搜索失败: {str(exc)[:50]}"}
  65. if not arts:
  66. return {"ok": [], "error": "未搜到"}
  67. recs = []
  68. for a in arts[:K_WEIXIN]:
  69. h = hashlib.md5(a["url"].encode()).hexdigest()[:16]
  70. base, pub = DATA / "search" / "weixin" / h, f"/data/search/weixin/{h}"
  71. rec = {"title": a["title"], "url": a["url"], "nick": a["nick_name"], "cover": None}
  72. if a["cover_url"]:
  73. rec["cover"] = _dl(a["cover_url"], "weixin", base / "cover.jpg", pub + "/cover.jpg")
  74. recs.append(rec)
  75. return {"ok": recs, "error": None}
  76. def main() -> None:
  77. settings = Settings.from_env()
  78. demo = json.loads(DEMO.read_text("utf-8"))
  79. dy_gate = RateLimiter(min_interval_seconds=12.0) # 抖音统一闸(躲冷却)
  80. wx_rl = RateLimiter(min_interval_seconds=1.0)
  81. results = []
  82. for tac in ("tactic1", "tactic2", "tactic4"):
  83. t = demo.get(tac) or {}
  84. name = t.get("name", tac)
  85. for it in (t.get("items") or [])[:N]:
  86. q = it.get("query")
  87. if not q:
  88. continue
  89. print(f"[{name}] {q}")
  90. dy = douyin_topk(q, settings, dy_gate)
  91. wx = weixin_topk(q, settings, wx_rl)
  92. print(f" 抖音 {len(dy['ok'])} 视频{' / ' + dy['error'] if dy['error'] else ''}"
  93. f" 微信 {len(wx['ok'])} 封面{' / ' + wx['error'] if wx['error'] else ''}")
  94. results.append({"method": name, "query": q, "douyin": dy, "weixin": wx})
  95. OUT.parent.mkdir(parents=True, exist_ok=True)
  96. OUT.write_text(json.dumps(results, ensure_ascii=False, indent=1), encoding="utf-8")
  97. dy_total = sum(len(r["douyin"]["ok"]) for r in results)
  98. wx_total = sum(len(r["weixin"]["ok"]) for r in results)
  99. print(f"\nwrote {len(results)} 条 query(抖音 {dy_total} 视频 / 微信 {wx_total} 封面)→ {OUT}")
  100. if __name__ == "__main__":
  101. main()