#!/usr/bin/env python3 """ 知乎问题页抓取脚本 v2 (bypass zse-ck, 支持滚动加载全部回答) 用法: python3 zhihu_question_fetch2.py [output.txt] [max_answers] 示例: python3 zhihu_question_fetch2.py 356351510 /tmp/q.txt 10 原理: playwright 真实浏览器, 先访问首页种 cookie(d_c0), 再访问问题页, 等待 zse-ck JS 验证, 滚动加载更多回答, 提取: 问题标题 / 问题描述 / 回答列表。 """ import sys, re, json, argparse, time from playwright.sync_api import sync_playwright CHROME = "/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome" UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36") def extract_question_id(url_or_id: str) -> str: m = re.search(r"/question/(\d+)", url_or_id) if m: return m.group(1) if url_or_id.isdigit(): return url_or_id raise ValueError("无法解析 question id: " + url_or_id) def fetch(url_or_id: str, timeout_ms: int = 60000, max_answers: int = 12) -> dict: qid = extract_question_id(url_or_id) url = f"https://www.zhihu.com/question/{qid}" result = {"question_id": qid, "url": url, "title": "", "detail": "", "answers": [], "error": None} with sync_playwright() as p: browser = p.chromium.launch( executable_path=CHROME, headless=True, args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-blink-features=AutomationControlled"], ) ctx = browser.new_context(user_agent=UA, viewport={"width": 1280, "height": 900}, locale="zh-CN") page = ctx.new_page() try: page.goto("https://www.zhihu.com/", timeout=30000, wait_until="domcontentloaded") page.wait_for_timeout(5000) except Exception as e: result["error"] = f"home: {e}" try: page.goto(url, timeout=timeout_ms, wait_until="domcontentloaded") page.wait_for_timeout(12000) # 问题标题 for sel in ["h1.QuestionHeader-title", "h1", ".QuestionHeader-title"]: el = page.query_selector(sel) if el: t = el.inner_text().strip() if t: result["title"] = t break # 问题描述 for sel in [".QuestionRichText", ".QuestionHeader-detail .RichText", ".QuestionHeader-detail"]: el = page.query_selector(sel) if el: d = el.inner_text().strip() if d: result["detail"] = d break # 滚动加载更多回答: 最多滚 25 次, 每次 3000ms last_count = 0 for i in range(25): page.mouse.wheel(0, 3000) page.wait_for_timeout(1800) cards = page.query_selector_all(".List-item") cur = len(cards) if cur != last_count: last_count = cur if cur >= max_answers + 3: break # 再滚几次确保底部 for i in range(3): page.mouse.wheel(0, 4000) page.wait_for_timeout(1500) # 展开"阅读全文" for sel in ["button:has-text('阅读全文')", "button:has-text('展开')", ".RichContent-collapsedText"]: try: btns = page.query_selector_all(sel) for b in btns[:max_answers + 5]: try: b.click(timeout=2000) page.wait_for_timeout(1000) except Exception: pass except Exception: pass # 按回答卡片提取 seen = set() cards = page.query_selector_all(".List-item .AnswerCard, .List-item") for card in cards: if len(result["answers"]) >= max_answers: break author = "" a = card.query_selector(".AuthorInfo-name, .UserLink-link, .AuthorInfo") if a: author = a.inner_text().strip() rich = card.query_selector(".RichText, .RichContent-inner") if not rich: continue body = rich.inner_text().strip() if len(body) < 30: continue key = body[:80] if key in seen: continue seen.add(key) result["answers"].append({"author": author, "text": body}) if not result["answers"]: for rich in page.query_selector_all(".RichText")[:max_answers]: body = rich.inner_text().strip() if len(body) >= 30: result["answers"].append({"author": "", "text": body}) except Exception as e: result["error"] = f"page: {e}" browser.close() return result if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("url_or_id", help="知乎问题链接或 question_id") ap.add_argument("output", nargs="?", default=None, help="输出文件路径(默认打印到stdout)") ap.add_argument("max_answers", nargs="?", type=int, default=12, help="最多抓取回答数") args = ap.parse_args() r = fetch(args.url_or_id, max_answers=args.max_answers) if r["error"]: print("WARN:", r["error"], file=sys.stderr) lines = [] if r["title"]: lines.append("【问题】" + r["title"]) if r["detail"]: lines.append("【问题描述】\n" + r["detail"]) lines.append(f"【共抓取 {len(r['answers'])} 个回答】") for i, ans in enumerate(r["answers"], 1): lines.append(f"\n===== 回答 {i}" + (f" | {ans['author']}" if ans["author"] else "") + " =====") lines.append(ans["text"]) text = "\n".join(lines) if text: if args.output: with open(args.output, "w", encoding="utf-8") as f: f.write(text) print(f"已保存 {len(text)} 字符, {len(r['answers'])} 个回答 -> {args.output}", file=sys.stderr) else: print(text) else: print("FAILED: 未提取到内容, title=", r["title"], file=sys.stderr) sys.exit(1)