#!/usr/bin/env python3 """ 知乎问题页抓取脚本 (bypass zse-ck) 用法: python3 zhihu_question_fetch.py [output.txt] 示例: python3 zhihu_question_fetch.py https://www.zhihu.com/question/1947309256454956878 python3 zhihu_question_fetch.py 1947309256454956878 /tmp/q.txt 原理: playwright 真实浏览器, 先访问首页种 cookie(d_c0), 再访问问题页, 等待 zse-ck JS 验证, 提取: 问题标题 / 问题描述 / 回答列表(每条回答作者+正文)。 """ import sys, re, json, argparse 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 = 45000, max_answers: int = 8) -> 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 # 回答列表: 展开"阅读全文"后逐条抓 for sel in ["button:has-text('阅读全文')", "button:has-text('展开')", ".RichContent-collapsedText"]: try: btns = page.query_selector_all(sel) for b in btns[:max_answers]: try: b.click(timeout=2000) page.wait_for_timeout(1500) except Exception: pass except Exception: pass # 按回答卡片提取 cards = page.query_selector_all(".List-item .AnswerCard, .List-item") for card in cards[:max_answers]: 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 result["answers"].append({"author": author, "text": body}) if not result["answers"]: # 兜底: 直接抓所有 RichText 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)") args = ap.parse_args() r = fetch(args.url_or_id) 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"]) 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)} 字符 -> {args.output}", file=sys.stderr) else: print(text) else: print("FAILED: 未提取到内容, title=", r["title"], file=sys.stderr) sys.exit(1)