#!/usr/bin/env python3 """ 用户痛点自动抓取与分析工具 从 Reddit、Hacker News 等平台采集用户评论,用 LLM 提取痛点。 用法: python3 pain_point_scraper.py --topic "AI写作工具" --sources reddit,hn --limit 20 python3 pain_point_scraper.py --topic "OKX交易" --sources reddit --limit 30 --output report.md """ import argparse import json import sys import time from datetime import datetime from typing import Optional import requests # ───────────────────────────────────────────── # 配置 # ───────────────────────────────────────────── # LLM API (使用本地 Ollama 或 LM Studio) LLM_API_URL = "http://192.168.199.100:11434/v1/chat/completions" LLM_MODEL = "gemma-4-26b-a4b-it-4bit" LLM_API_KEY = "" # 本地服务通常不需要 # 如果本地不可用,fallback到远程 LLM_FALLBACK_URL = "https://token-plan-cn.xiaomimimo.com/v1/chat/completions" LLM_FALLBACK_MODEL = "mimo-v2.5-pro" LLM_FALLBACK_KEY = "tp-cd64i50fo6ifihhiupspq9dypmzeud3u77tia5mlz0rb572u" HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" } # ───────────────────────────────────────────── # Reddit 采集 # ───────────────────────────────────────────── def scrape_reddit(topic: str, limit: int = 20) -> list[dict]: """从 Reddit 搜索相关讨论""" results = [] # 使用 Reddit JSON API (无需认证) url = "https://www.reddit.com/search.json" params = { "q": topic, "sort": "relevance", "t": "month", # 最近一个月 "limit": min(limit, 25), "type": "link" } try: resp = requests.get(url, params=params, headers=HEADERS, timeout=15) resp.raise_for_status() data = resp.json() for post in data.get("data", {}).get("children", []): d = post.get("data", {}) results.append({ "source": "Reddit", "title": d.get("title", ""), "text": d.get("selftext", "")[:500], "score": d.get("score", 0), "comments": d.get("num_comments", 0), "url": f"https://reddit.com{d.get('permalink', '')}", "subreddit": d.get("subreddit", ""), "created": datetime.fromtimestamp(d.get("created_utc", 0)).isoformat() }) print(f" ✅ Reddit: 获取 {len(results)} 条", file=sys.stderr) except Exception as e: print(f" ❌ Reddit 采集失败: {e}", file=sys.stderr) return results # ───────────────────────────────────────────── # Hacker News 采集 # ───────────────────────────────────────────── def scrape_hn(topic: str, limit: int = 20) -> list[dict]: """从 Hacker News 搜索相关讨论""" results = [] url = "https://hn.algolia.com/api/v1/search" params = { "query": topic, "tags": "story", "hitsPerPage": min(limit, 30), "numericFilters": "created_at_i>" + str(int(time.time()) - 30*86400) } try: resp = requests.get(url, params=params, timeout=15) resp.raise_for_status() data = resp.json() for hit in data.get("hits", []): results.append({ "source": "Hacker News", "title": hit.get("title", ""), "text": (hit.get("story_text") or "")[:500], "score": hit.get("points", 0), "comments": hit.get("num_comments", 0), "url": hit.get("url", f"https://news.ycombinator.com/item?id={hit.get('objectID', '')}"), "author": hit.get("author", ""), "created": hit.get("created_at", "") }) print(f" ✅ HN: 获取 {len(results)} 条", file=sys.stderr) except Exception as e: print(f" ❌ HN 采集失败: {e}", file=sys.stderr) return results # ───────────────────────────────────────────── # Product Hunt 采集 (通过搜索) # ───────────────────────────────────────────── def scrape_producthunt(topic: str, limit: int = 10) -> list[dict]: """从 Product Hunt 搜索相关产品评论""" results = [] # 使用 Google 搜索 Product Hunt 上的内容 url = "https://www.google.com/search" params = { "q": f"site:producthunt.com {topic} review complaints", "num": min(limit, 10) } try: resp = requests.get(url, params=params, headers=HEADERS, timeout=15) # Google 可能会block,所以这里简单处理 if resp.status_code == 200: # 简单提取标题 import re titles = re.findall(r']*>(.*?)', resp.text) for t in titles[:limit]: clean = re.sub(r'<[^>]+>', '', t) if clean and len(clean) > 10: results.append({ "source": "Product Hunt", "title": clean, "text": "", "score": 0, "comments": 0, "url": "", "created": "" }) print(f" ✅ Product Hunt: 获取 {len(results)} 条", file=sys.stderr) except Exception as e: print(f" ❌ Product Hunt 采集失败: {e}", file=sys.stderr) return results # ───────────────────────────────────────────── # LLM 分析 # ───────────────────────────────────────────── def call_llm(prompt: str, api_url: str = None, model: str = None, api_key: str = None) -> str: """调用 LLM API 进行分析""" api_url = api_url or LLM_API_URL model = model or LLM_MODEL api_key = api_key or LLM_API_KEY headers = {"Content-Type": "application/json"} if api_key: headers["Authorization"] = f"Bearer {api_key}" payload = { "model": model, "messages": [ {"role": "system", "content": "你是一个专业的用户研究分析师。你擅长从用户评论中提取痛点、需求和机会。请用中文回答。"}, {"role": "user", "content": prompt} ], "temperature": 0.3, "max_tokens": 4000 } try: resp = requests.post(api_url, json=payload, headers=headers, timeout=120) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] except Exception as e: print(f" ⚠️ 主API失败 ({e}),尝试fallback...", file=sys.stderr) # Fallback try: fallback_headers = { "Content-Type": "application/json", "Authorization": f"Bearer {LLM_FALLBACK_KEY}" } payload["model"] = LLM_FALLBACK_MODEL resp = requests.post(LLM_FALLBACK_URL, json=payload, headers=fallback_headers, timeout=120) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] except Exception as e2: return f"❌ LLM 分析失败: {e2}" def analyze_pain_points(items: list[dict], topic: str) -> str: """用 LLM 分析采集到的数据,提取痛点""" # 构建分析prompt reviews_text = "" for i, item in enumerate(items[:30], 1): # 最多30条,避免token超限 reviews_text += f"\n--- [{i}] {item['source']} | 👍{item['score']} 💬{item['comments']} ---\n" reviews_text += f"标题: {item['title']}\n" if item.get('text'): reviews_text += f"内容: {item['text'][:300]}\n" prompt = f"""请分析以下关于"{topic}"的用户评论/讨论,提取用户痛点、需求和机会。 ## 原始数据 {reviews_text} ## 分析要求 请按以下格式输出: ### 📊 概览 - 总评论数:N - 主要痛点数:N - 整体情绪:正面/负面/混合 ### 🔥 TOP 痛点(按严重程度排序) 对每个痛点: 1. **痛点标题**(一句话概括) 2. **用户原话**(引用1-2条最能说明问题的原文) 3. **痛点描述**(为什么这是问题) 4. **影响人群**(谁会遇到这个问题) 5. **现有解决方案**(有没有竞品在解决) 6. **机会评估**(⭐1-5星,这个痛点值不值得做) ### 💡 机会洞察 - 基于以上痛点,有哪些产品/功能机会 - 有哪些未被满足的需求 ### 📈 趋势观察 - 近期讨论的热点变化 - 用户关注点的转移 请用中文回答,保持分析的客观性和实用性。""" return call_llm(prompt) # ───────────────────────────────────────────── # 报告生成 # ───────────────────────────────────────────── def generate_report(topic: str, items: list[dict], analysis: str) -> str: """生成完整的Markdown报告""" sources = {} for item in items: src = item["source"] sources[src] = sources.get(src, 0) + 1 source_summary = "、".join([f"{k}({v}条)" for k, v in sources.items()]) report = f"""# 🔍 痛点分析报告:{topic} > 数据来源:{source_summary} | 采集时间:{datetime.now().strftime('%Y-%m-%d %H:%M')} | 分析评论数:{len(items)} --- {analysis} --- ## 📋 原始数据摘要 | # | 来源 | 标题 | 👍 | 💬 | |---|------|------|-----|-----| """ for i, item in enumerate(items[:20], 1): title = item["title"][:50] + ("..." if len(item["title"]) > 50 else "") report += f"| {i} | {item['source']} | {title} | {item['score']} | {item['comments']} |\n" report += f"\n*共 {len(items)} 条数据,显示前 20 条*\n" return report # ───────────────────────────────────────────── # 主流程 # ───────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="用户痛点自动抓取与分析") parser.add_argument("--topic", "-t", required=True, help="分析主题/关键词") parser.add_argument("--sources", "-s", default="reddit,hn", help="数据源 (逗号分隔: reddit,hn,producthunt)") parser.add_argument("--limit", "-l", type=int, default=20, help="每个源采集数量") parser.add_argument("--output", "-o", help="输出文件路径 (默认stdout)") parser.add_argument("--raw", action="store_true", help="只输出原始数据,不做LLM分析") args = parser.parse_args() print(f"\n🔍 痛点分析:{args.topic}", file=sys.stderr) print(f"📡 数据源:{args.sources}", file=sys.stderr) print(f"📊 采集数量:每个源 {args.limit} 条\n", file=sys.stderr) # 1. 采集数据 all_items = [] source_map = { "reddit": scrape_reddit, "hn": scrape_hn, "producthunt": scrape_producthunt } for source in args.sources.split(","): source = source.strip().lower() if source in source_map: items = source_map[source](args.topic, args.limit) all_items.extend(items) else: print(f" ⚠️ 未知数据源: {source}", file=sys.stderr) if not all_items: print("\n❌ 没有采集到任何数据", file=sys.stderr) sys.exit(1) print(f"\n📊 总计采集: {len(all_items)} 条\n", file=sys.stderr) # 2. 如果只输出原始数据 if args.raw: output = json.dumps(all_items, ensure_ascii=False, indent=2) if args.output: with open(args.output, "w") as f: f.write(output) print(f"✅ 原始数据已保存到: {args.output}", file=sys.stderr) else: print(output) return # 3. LLM 分析 print("🧠 正在分析痛点...", file=sys.stderr) analysis = analyze_pain_points(all_items, args.topic) # 4. 生成报告 report = generate_report(args.topic, all_items, analysis) if args.output: with open(args.output, "w") as f: f.write(report) print(f"\n✅ 报告已保存到: {args.output}", file=sys.stderr) else: print(report) print(f"\n✅ 分析完成!", file=sys.stderr) if __name__ == "__main__": main()