|
|
@@ -23,11 +23,11 @@ ROOT = Path(__file__).resolve().parent.parent
|
|
|
DATA = ROOT / "data"
|
|
|
DEMO = DATA / "queries" / "demo.json"
|
|
|
OUT = DATA / "queries" / "search_results.json"
|
|
|
-DOUYIN_ON = False # 抖音限流暂停:False 时不发抖音请求,沿用上次结果里的抖音
|
|
|
+DOUYIN_ON = True # 抖音走 piaoquantv,真跑
|
|
|
N = 6 # 每法取前 N 条 query
|
|
|
-K_DOUYIN = 1 # 抖音每条 query 只取第 1 个视频(躲限流)
|
|
|
-K_WEIXIN = 5 # 微信公众号每条 query 取前 K 个封面
|
|
|
-K_XHS = 5 # 小红书每条 query 取前 K 个封面
|
|
|
+K_DOUYIN = 3 # 抖音每条 query 取前 3 个视频(每个要 1 详情 + 下 mp4,最重,故取少)
|
|
|
+K_WEIXIN = 10 # 微信公众号每条 query 取前 10 个封面
|
|
|
+K_XHS = 10 # 小红书每条 query 取前 10 帖(逐帖下完整图+正文)
|
|
|
SEARCH_MIN, SEARCH_MAX = 10.0, 12.0 # 每平台两次搜索之间随机间隔(秒)
|
|
|
|
|
|
|
|
|
@@ -86,23 +86,47 @@ def weixin_topk(query: str, settings: Settings, rl: RateLimiter) -> dict:
|
|
|
return {"ok": recs, "error": None}
|
|
|
|
|
|
|
|
|
-def xiaohongshu_topk(query: str, settings: Settings, rl: RateLimiter) -> dict:
|
|
|
- """小红书搜 top-K 帖子封面:返回 {ok:[{title,url,nick,cover}], error}。封面来自搜索回包,无需 detail。"""
|
|
|
+def _dedup(urls):
|
|
|
+ seen, out = set(), []
|
|
|
+ for u in urls:
|
|
|
+ if u and u not in seen:
|
|
|
+ seen.add(u)
|
|
|
+ out.append(u)
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
+def xiaohongshu_topk(query: str, settings: Settings, search_rl: RateLimiter,
|
|
|
+ detail_rl: RateLimiter) -> dict:
|
|
|
+ """小红书搜 top-K → 逐条调详情拉【完整帖子】:下载全部图片 + 存正文到本地。
|
|
|
+ 小红书链接会被反爬封,必须把整帖存档,不能只留封面+链接。返回 {ok:[{title,url,nick,cover,images,body_text}], error}。"""
|
|
|
try:
|
|
|
posts = search_xiaohongshu(query, content_type="图文", limit=K_XHS,
|
|
|
- settings=settings, rate_limiter=rl)
|
|
|
+ settings=settings, rate_limiter=search_rl)
|
|
|
except Exception as exc:
|
|
|
return {"ok": [], "error": f"搜索失败: {str(exc)[:50]}"}
|
|
|
if not posts:
|
|
|
return {"ok": [], "error": "未搜到"}
|
|
|
recs = []
|
|
|
for p in posts[:K_XHS]:
|
|
|
- base, pub = DATA / "search" / "xiaohongshu" / p["id"], f"/data/search/xiaohongshu/{p['id']}"
|
|
|
- rec = {"title": p["title"], "url": p["url"], "nick": p["nick_name"], "cover": None}
|
|
|
- if p["cover_url"]:
|
|
|
- rec["cover"] = _dl(p["cover_url"], "xiaohongshu", base / "cover.jpg", pub + "/cover.jpg")
|
|
|
- recs.append(rec)
|
|
|
- return {"ok": recs, "error": None}
|
|
|
+ cid = p["id"]
|
|
|
+ try:
|
|
|
+ post = fetch_post_detail(cid, settings=settings, rate_limiter=detail_rl)
|
|
|
+ except Exception:
|
|
|
+ post = None
|
|
|
+ base, pub = DATA / "search" / "xiaohongshu" / cid, f"/data/search/xiaohongshu/{cid}"
|
|
|
+ srcs = _dedup((post.image_urls if post else None) or ([p["cover_url"]] if p.get("cover_url") else []))
|
|
|
+ imgs = []
|
|
|
+ for i, u in enumerate(srcs):
|
|
|
+ local = _dl(u, "xiaohongshu", base / f"img_{i}.jpg", f"{pub}/img_{i}.jpg")
|
|
|
+ if local:
|
|
|
+ imgs.append(local)
|
|
|
+ if not imgs:
|
|
|
+ continue # 一张都没下来就丢(链接全失效)
|
|
|
+ recs.append({"title": (post.title if post and post.title else p["title"]),
|
|
|
+ "url": p["url"], "nick": p["nick_name"],
|
|
|
+ "body_text": (post.body_text if post else ""),
|
|
|
+ "cover": imgs[0], "images": imgs})
|
|
|
+ return {"ok": recs, "error": None if recs else "详情/下载均失败"}
|
|
|
|
|
|
|
|
|
def _prev_douyin() -> dict:
|
|
|
@@ -124,6 +148,7 @@ def main() -> None:
|
|
|
dy_detail = RateLimiter(min_interval_seconds=3.0, max_interval_seconds=5.0)
|
|
|
wx_search = RateLimiter(min_interval_seconds=SEARCH_MIN, max_interval_seconds=SEARCH_MAX)
|
|
|
xhs_search = RateLimiter(min_interval_seconds=SEARCH_MIN, max_interval_seconds=SEARCH_MAX)
|
|
|
+ xhs_detail = RateLimiter(min_interval_seconds=3.0, max_interval_seconds=5.0) # 小红书详情(拉完整帖)
|
|
|
prev_dy = {} if DOUYIN_ON else _prev_douyin()
|
|
|
if not DOUYIN_ON:
|
|
|
print(f"(抖音已关闭:沿用上次 {len(prev_dy)} 条 query 的抖音结果,本次只跑微信+小红书)")
|
|
|
@@ -141,10 +166,11 @@ def main() -> None:
|
|
|
else:
|
|
|
dy = prev_dy.get(q) or {"ok": [], "error": "抖音限流暂停,未跑"}
|
|
|
wx = weixin_topk(q, settings, wx_search)
|
|
|
- xhs = xiaohongshu_topk(q, settings, xhs_search)
|
|
|
+ xhs = xiaohongshu_topk(q, settings, xhs_search, xhs_detail)
|
|
|
+ xhs_imgs = sum(len(r.get("images") or []) for r in xhs["ok"])
|
|
|
print(f" 抖音 {len(dy['ok'])} 视频{_err(dy)}"
|
|
|
f" 微信 {len(wx['ok'])} 封面{_err(wx)}"
|
|
|
- f" 小红书 {len(xhs['ok'])} 封面{_err(xhs)}")
|
|
|
+ f" 小红书 {len(xhs['ok'])} 帖/{xhs_imgs} 图{_err(xhs)}")
|
|
|
results.append({"method": name, "query": q, "douyin": dy, "weixin": wx, "xiaohongshu": xhs})
|
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
|
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=1), encoding="utf-8")
|