#!/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()