绘制你的市场地图
输入产品描述、类别关键词或竞品名称。搜索 Reddit、HN、GitHub Issues、G2 和 Google Trends,以寻找真实的痛点信号。对它们进行评分和聚类。构建一个完整的定位框架:ICP 定义、按优先级排序的痛点主题(附原文引用)、市场规模信号,以及基于人们实际使用的语言推导出的营销角度。
关键规则: 输出中的每条痛点引述必须直接存在于脚本收集的原始数据中。市场地图中的每个供应商名称必须来自 G2 抓取结果或 GitHub 搜索结果。市场规模必须使用“信号表明”的表述——绝不从模糊的间接指标估算具体金额。如果某个来源返回 0 条结果,则如实报告 0——不要用虚构的示例来补充。
常见错误
| 代理可能会… | 为什么这是错误的 |
|---|---|
| 编造痛点或市场规模数字 | 每条痛点引述必须来自原始数据。市场规模必须引用找到的信号。绝不要估算“典型”市场规模。 |
| 根据帖子数量而不是 pain_score 进行评分 | 一个有 2,000 个赞的关于定价的帖子比 50 个只有 10 个赞的帖子更有说服力。使用 references/pain-scoring.md 中的 pain_score 公式。 |
| 对每个类别使用相同的 subreddits | r/politics 会给 DevOps 搜索增加噪音。在搜索之前,根据类别和竞品名称自动检测相关的 subreddits。 |
| 将所有原始信号不经评分就发送给 AI | 先在本地评分。只发送评分最高的前 60 个痛点信号给 AI 聚类。这样既节省 token,也提高聚类质量。 |
| 跳过从帖子元数据中提取 ICP 信息 | Subreddit、标签、作者简介 (HN) 和 GitHub 组织类型是比帖子内容更丰富的 ICP 信号。务必捕获并报告它们。 |
| 混淆供应商数量与市场规模 | “G2 上有 47 个供应商”意味着竞争激烈,而非市场规模大。将所有信号作为方向性指标呈现,而非确切的数字。 |
步骤 1:环境检查
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-未设置 —— 未认证的 GitHub Issues 搜索限制为 60 次/小时}"
echo "无需其他 API 密钥。"
echo ""
echo "本次运行将使用的数据源:"
echo " Reddit 公共 JSON (无需认证, 10 次请求/分钟)"
echo " HN Algolia API (无需认证, 免费)"
echo " GitHub Issues API (${GITHUB_TOKEN:+已认证,}60-5000 次请求/小时)"
echo " G2 类别抓取 (无需认证, HTML 解析)"
echo " Google Trends (无需认证, 非官方接口)"
如果 GITHUB_TOKEN 未设置:继续运行。未认证的 GitHub 搜索限制为 60 次/小时 —— 足以应对标准运行。如需重复使用,请在 github.com/settings/tokens 添加一个 token(公共仓库无需任何权限)。
步骤 2:解析输入
从对话中收集:
category—— 描述市场空间的关键词(例如 "developer observability", "B2B analytics", "devops tooling")competitors—— 可选:竞品名称或域名列表(例如 "Datadog, New Relic, Grafana")product_context—— 可选:用户的产品功能(有助于定制营销角度)
如果用户只提供了产品描述而没有类别关键词:请自行从中提取 2-3 个类别关键词。
如果用户只提供了竞品名称而没有类别:请通过查找竞品来推断类别。
写入解析后的输入:
python3 << 'PYEOF'
import json, os
data = {
"category": "CATEGORY_HERE",
"competitors": ["COMP_1", "COMP_2"],
"product_context": "PRODUCT_CONTEXT_HERE"
}
with open("/tmp/mym-input.json", "w") as f:
json.dump(data, f, indent=2)
print("输入已写入 /tmp/mym-input.json")
print(f"类别: {data['category']}")
print(f"竞品: {', '.join(data['competitors']) if data['competitors'] else '未提供'}")
PYEOF
步骤 3:运行独立的数据收集脚本
该脚本处理所有数据收集。首先检查它是否存在:
ls scripts/fetch.py 2>/dev/null && echo "脚本可用" || echo "未找到"
如果可用,运行它:
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
"$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(d['category'])")" \
--competitors "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(','.join(d['competitors']))")" \
--context "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(d['product_context'])")" \
--output /tmp/mym-raw.json
等待完成(最多允许 4 分钟 —— Reddit + HN 搜索总共耗时约 90 秒)。
验证输出:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
print(f'Reddit 信号: {d[\"market_signals\"][\"reddit_signals_found\"]}')
print(f'HN 信号: {d[\"market_signals\"][\"hn_signals_found\"]}')
print(f'GitHub 信号: {d[\"market_signals\"][\"github_issue_signals\"]}')
print(f'G2 供应商: {d[\"market_signals\"][\"vendor_count_g2\"]}')
print(f'趋势: {d[\"market_signals\"][\"trends_direction\"]}')
print(f'总信号数: {d[\"summary\"][\"total_pain_signals\"]}')
"
如果总信号数 < 10:停止运行。告知用户:“该类别找到的痛点信号少于 10 个。这个市场可能过于小众,在 Reddit/HN 上覆盖不足,或者类别关键词需要调整。请尝试更广泛的关键词或添加竞品名称。”
步骤 4:AI 疼痛聚类
打印前 60 个痛点信号供 AI 分析:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
top60 = sorted(d['raw_pains'], key=lambda x: x['pain_score'], reverse=True)[:60]
print(json.dumps(top60, indent=2))
"
现在你拥有了前 60 个痛点信号。分析它们并生成痛点聚类。
AI 分析指令:
- 识别跨所有来源的 5-7 个反复出现的痛点主题
- 对于每个主题:选择一个使用市场自身语言的名称(而非你的措辞)
- 汇总每个聚类中所有信号的 pain_score
- 为每个主题选择 3-5 条最佳原文引用(评分最高、语言最具体)
- 注意每个主题集中在哪些来源和 subreddits
- 标记仅出现在单个来源的任何主题(置信度较低)
将聚类结果写入 /tmp/mym-clusters.json:
{
"clusters": [
{
"theme": "exact language from the data",
"total_score": 847,
"signal_count": 34,
"sources": {"reddit": 18, "hn": 12, "github_issue": 4},
"top_subreddits": ["devops", "sysadmin"],
"verbatim_quotes": [
{"text": "exact quote", "source": "reddit", "score": 234, "url": "..."},
{"text": "exact quote", "source": "hn", "score": 87, "url": "..."}
],
"who_has_this_pain": "description of who is posting about this"
}
]
}
python3 -c "
import json, os
# Confirm clusters file was written
with open('/tmp/mym-clusters.json') as f:
d = json.load(f)
print(f'已写入聚类: {len(d[\"clusters\"])}')
for c in d['clusters']:
print(f' {c[\"theme\"]} -- 分数: {c[\"total_score\"]}, 信号数: {c[\"signal_count\"]}')
"
步骤 5:ICP 画像分析
从原始数据中打印 ICP 信号:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
print('ICP 信号:')
print(json.dumps(d['icp_signals'], indent=2))
print()
print('Subreddit 分布:')
sub_counts = {}
for p in d['raw_pains']:
s = p.get('subreddit', '')
if s:
sub_counts[s] = sub_counts.get(s, 0) + 1
for sub, count in sorted(sub_counts.items(), key=lambda x: -x[1])[:10]:
print(f' r/{sub}: {count} 个信号')
"
使用上面的 ICP 信号和 subreddit 分布,综合生成 ICP 画像。通过添加 icp 键将其写入 /tmp/mym-clusters.json:
{
"icp": {
"who_they_are": "2-3 sentence profile using language from the data",
"where_they_live": ["r/devops (89 posts)", "r/sysadmin (67 posts)", "HN ask-hn (34 threads)"],
"what_they_say": ["verbatim quote 1", "verbatim quote 2", "verbatim quote 3"],
"what_they_have_tried": ["alternative tools or approaches mentioned in the data"],
"confidence": "high|medium|low -- based on signal volume and source diversity"
}
}
步骤 6:市场规模综合
打印市场信号:
python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
d = json.load(f)
ms = d['market_signals']
print('市场信号:')
print(f' G2 供应商: {ms[\"vendor_count_g2\"]}')
print(f' 趋势方向: {ms[\"trends_direction\"]}')
print(f' HN 信号 (12 个月): {ms[\"hn_signals_found\"]}')
print(f' Reddit 信号: {ms[\"reddit_signals_found\"]}')
print(f' G2 头部供应商: {json.dumps(ms.get(\"top_vendors\", []), indent=4)}')
"
仅使用这些信号,综合生成一个方向性的市场规模评估。不要估算具体金额。使用以下语言:
- “信号表明这是一个竞争激烈、正在增长的市场”(供应商多 + 趋势向上)
- “信号表明这是一个早期市场”(供应商少 + 信号量低)
- “信号表明这是一个饱和的市场”(供应商多 + 趋势平缓/向下)
将评估结果作为 market_size 键添加到 /tmp/mym-clusters.json 中。
步骤 7:定位框架
使用聚类结果(步骤 4)、ICP(步骤 5)和市场规模(步骤 6),生成定位框架。
指令:
- 选择排名前 3 的痛点聚类作为主要定位角度
- 对于每个角度:使用数据中的原文,编写一句定位陈述(不要转述)
- 生成 3 个使用痛点数据中人们确切用语的着陆页标题
- 生成 3 个基于痛点语言的冷邮件主题行
- 不要使用禁用词:强大的、稳健的、无缝的、创新的、变革性的、精简的、利用、转变、革新
将完整的定位框架写入 /tmp/mym-output.json:
{
"positioning_angles": [
{
"pain": "theme name",
"statement": "one-line positioning using market language",
"headline": "landing page headline using verbatim pain language",
"cold_email_subject": "subject line"
}
],
"icp_card": {
"one_liner": "one sentence: who they are + what they care about",
"where_to_find_them": [...],
"how_to_talk_to_them": "tone + vocabulary notes from the data"
},
"market_map": [...top vendors from G2 with positioning notes...]
}
步骤 8:自我质量检查和保存输出
运行自我质量检查:
python3 -c "
import json
# Load all outputs
with open('/tmp/mym-raw.json') as f:
raw = json.load(f)
with open('/tmp/mym-clusters.json') as f:
clusters = json.load(f)
with open('/tmp/mym-output.json') as f:
output = json.load(f)
raw_texts = set()
for p in raw['raw_pains']:
raw_texts.add(p.get('title', ''))
raw_texts.add(p.get('body_excerpt', ''))
# Check 1: No em dashes
import json as j
full_text = j.dumps(output)
if '—' in full_text:
print('FAIL: em dash found in output')
else:
print('PASS: no em dashes')
# Check 2: No banned words
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'FAIL: banned words found: {found}')
else:
print('PASS: no banned words')
# Check 3: Market size language check
if 'billion' in full_text.lower() or 'trillion' in full_text.lower() or 'worth \$' in full_text.lower():
print('FAIL: hard market size estimate found -- use directional language only')
else:
print('PASS: no hard market size estimates')
# Check 4: Signal counts match
total = raw['summary']['total_pain_signals']
print(f'PASS: {total} total pain signals in raw data')
print('Self-QA complete.')
"
在保存之前修复任何失败。
保存最终报告:
python3 << 'PYEOF'
import json, re
from datetime import datetime
with open('/tmp/mym-input.json') as f:
inp = json.load(f)
with open('/tmp/mym-raw.json') as f:
raw = json.load(f)
with open('/tmp/mym-clusters.json') as f:
clusters = json.load(f)
with open('/tmp/mym-output.json') as f:
output = json.load(f)
slug = re.sub(r'[^a-z0-9]+', '-', inp['category'].lower()).strip('-')
date = datetime.now().strftime('%Y-%m-%d')
outpath_md = f"docs/market-maps/{slug}-{date}.md"
outpath_json = f"docs/market-maps/{slug}-{date}.json"
# Build markdown report
ms = raw['market_signals']
icp = clusters.get('icp', {})
market_assessment = clusters.get('market_size', {})
angles = output.get('positioning_angles', [])
icp_card = output.get('icp_card', {})
market_map = output.get('market_map', [])
lines = [
f"# Market Map: {inp['category'].title()}",
f"Date: {date} | Signals analyzed: {raw['summary']['total_pain_signals']} | Sources: Reddit ({ms['reddit_signals_found']}) + HN ({ms['hn_signals_found']}) + GitHub Issues ({ms['github_issue_signals']})",
"",
"---",
"",
"## Market Size Signals",
f"Vendors on G2: {ms['vendor_count_g2']} | Google Trends: {ms['trends_direction'].upper()} | Market stage: {market_assessment.get('stage', 'see signals below')}",
"",
market_assessment.get('summary', ''),
"",
"---",
"",
"## Your ICP",
"",
f"**Who they are:** {icp.get('who_they_are', '')}",
"",
f"**Where they live:** {', '.join(icp.get('where_they_live', []))}",
"",
"**What they say:**",
]
for q in icp.get('what_they_say', []):
lines.append(f'> "{q}"')
lines += ["", "---", "", "## Top Pains (ranked by signal strength)", ""]
for i, c in enumerate(clusters.get('clusters', []), 1):
lines.append(f"### Pain {i}: {c['theme']} [score: {c['total_score']}]")
sources = c.get('sources', {})
source_str = " + ".join(f"{src} ({cnt})" for src, cnt in sources.items())
lines.append(f"{c['signal_count']} signals | Sources: {source_str}")
lines.append(f"Who has this pain: {c.get('who_has_this_pain', '')}")
lines.append("")
lines.append("Verbatim:")
for q in c.get('verbatim_quotes', [])[:4]:
lines.app...
... [truncated for translation]
PYEOF