
Product Research
- 29 installs
- 658 repo stars
- Updated July 8, 2026
- liangdabiao/amazon-sorftime-research-mcp-skill
Helps with ai & agent building tasks.
About
product-research is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- product-research
- AI & Agent Building
- AI-coding skill
Product Research by the numbers
- 29 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #9,413 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/liangdabiao/amazon-sorftime-research-mcp-skill --skill product-researchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 658 |
| Last updated | July 8, 2026 |
| Repository | liangdabiao/amazon-sorftime-research-mcp-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
选品分析器 (Product Research - LLM Agent 驱动版)
定位
基于 Sorftime MCP + LLM Agent 的深度选品调研。LLM 直接执行分析逻辑,脚本仅负责数据采集和报告渲染。
核心特点:
- LLM 驱动:分析、洞察、决策全部由 LLM 完成
- 交互式执行:逐步推进,用户可中途干预
- 轻量脚本:仅用于 API 调用和 Dashboard 渲染
---
Script Directory
| 脚本 | 用途 | 何时调用 |
|---|---|---|
run_analysis.py | 主入口脚本:整合数据采集、分析、报告生成 | 推荐使用 |
collect_data.py | Sorftime 数据采集(类目、Top100、关键词、趋势) | Step 1 |
get_reviews.py | 竞品差评数据采集 | Step 4 |
api_client.py | Sorftime API 调用 + SSE 解析 + 编码修复 | 每次 API 调用 |
render_dashboard.py | 生成 Dashboard 可视化看板(v3.1 修复版) | 报告生成阶段 |
fix_data_json.py | 数据验证和修复脚本:校验并自动修复 data.json | Dashboard 生成前 |
validate_data.py | 数据验证脚本:校验 data.json 字段命名和数据一致性 | 报告生成前 |
脚本职责:
- 不做分析判断:所有分析由 LLM 完成
- 不做复杂计算:交叉分析让 LLM 从数据中发现
- 仅做数据搬运:API → 结构化数据
推荐使用方式:
# 阶段1:数据采集(基础版 Dashboard)
python scripts/run_analysis.py "earbuds" US
# 阶段2:LLM 分析完成后,生成最终版报告
python scripts/run_analysis.py "earbuds" US --final
# 其他选项
python scripts/run_analysis.py "earbuds" US --collect-only # 仅数据采集
python scripts/run_analysis.py "earbuds" US --no-reviews # 跳过差评采集---
执行流程(两阶段)
重要:选品分析分为两个阶段,数据采集由脚本自动完成,LLM 分析需要人工参与。
阶段1:数据采集(脚本自动)
python scripts/run_analysis.py "keyword" US输出:
data.json- 基础数据结构(不含分析结论)dashboard.html- 基础版看板(不含决策评分、VOC 等)raw/- 原始数据文件
脚本自动完成: 1. 类目搜索 → 获取 nodeId 2. Top100 产品数据采集 3. 关键词数据采集 4. 类目趋势数据采集 5. 竞品差评采集 6. 市场分析(价格区间、品牌分布)
阶段2:LLM 分析(交互式)
必须完成的 LLM 分析任务:
| 步骤 | 任务 | 输出到 data.json |
|---|---|---|
| 1 | 属性标注 | product_types、dimensions_analysis |
| 2 | 交叉分析 | cross_analysis |
| 3 | VOC 分析 | voc_analysis.dimensions |
| 4 | 壁垒评估 | barriers |
| 5 | 决策评估 | decision (overall_score, verdict) |
完成后运行:
python scripts/run_analysis.py "keyword" US --final--final 参数会: 1. ✅ 验证分析数据完整性 2. ✅ 更新 data.json 3. ✅ 生成完整版 Dashboard(含决策评分、VOC 等) 4. ✅ 如果数据不完整,会提示缺失的字段
数据完整性检查
也可以单独检查数据完整性:
python scripts/render_dashboard.py data.json --check输出示例:
✓ 数据完整,可以渲染完整版 Dashboard
包含: decision.overall_score, voc_analysis.dimensions, barriers, cross_analysis或
⚠️ 数据不完整,缺少以下字段:
- decision.overall_score
- voc_analysis.dimensions
ℹ️ 请先完成 LLM 分析,然后重新运行渲染---
执行流程(交互式)
Step 0: 信息收集
📋 选品分析 - 信息确认
1. 产品/类目关键词:[用户提供]
2. 目标站点:[US/GB/DE/FR/IT/ES/CA/JP,默认US]
3. 选品场景:[新手入门/蓝海发现/季节性/品牌打造/定向品类]
4. 约束条件(可选):
- 价格区间:如 $10-40
- 月销量:如 > 1000
- 预算:如 10万人民币Step 1: 数据采集(增强版 v3.0)
API 调用顺序:
| 步骤 | API | 输出 | 说明 | 优先级 |
|---|---|---|---|---|
| 0.5 | search_categories_broadly | blue_ocean_categories.json | 【新增】蓝海市场发现 | 📋 按需 |
| 1.1 | category_name_search | category_info.json | 按产品名搜索类目(使用 searchName 参数) | ⛔ 必调 |
| 1.2 | category_report | top100.json | Top100 产品数据 | ⛔ 必调 |
| 1.3 | keyword_detail × 3+ | keywords.json | 多维度关键词对比 | ⛔ 必调 |
| 1.4 | category_trend | trend.json | 新品占比趋势 | ⛔ 必调 |
| 1.5 | keyword_extends | keyword_extends.json | 【新增】关键词延伸词(维度发现) | 📋 推荐 |
| 1.6 | potential_product | potential_products.json | 【新增】潜力产品发现 | 📋 推荐 |
| 1.7 | product_detail × 6-10 | products.json | 竞品详情(按需) | 📋 按需 |
| 1.8 | product_reviews × 6-10 | reviews.json | 竞品差评(按需) | 📋 按需 |
⚠️ 重要:API 参数说明(v3.0)
category_name_search参数:{"amzSite": "US", "searchName": "bluetooth speaker"}- 正确的类目搜索 API,参数名是 searchName
search_categories_broadly参数(蓝海发现):{"amzSite": "US", "top3Product_sales_share": 0.4}potential_product参数(潜力产品):{"amzSite": "US", "monthlySales_min": 500}keyword_extends参数(延伸词):{"amzSite": "US", "keyword": "bluetooth speaker"}category_report参数:{"amzSite": "US", "nodeId": "7073956011"}- nodeId 是字符串类型
脚本调用方式:
# 方法1: 使用 collect_data.py (推荐)
from scripts.collect_data import collect_data
result = collect_data("bluetooth speaker", "US")
# 方法2: 使用 api_client.py
from scripts.api_client import SorftimeClient
client = SorftimeClient()
# 获取类目ID(正确的方式)
category = client.search_category_by_product_name("US", "bluetooth speaker")
node_id = category[0]['nodeId']
# 获取Top100
top100 = client.get_category_report("US", node_id)
# 获取关键词详情
keywords = client.get_keyword_detail("US", "bluetooth speaker")Step 2: 属性标注(LLM 驱动)
LLM 任务:从 Top100 标题中提取关键差异化维度
## 属性标注任务
基于以下 Top100 产品标题,提取 3-6 个关键差异化维度:
### 标题样本
[提供 Top20-30 标题作为样本]
### 提取要求
1. 识别差异化维度(如:功率、防水、续航、形态等)
2. 为每个产品标注维度值
3. 标注置信度(高/中/低)
### 输出格式
| ASIN | 功率 | 防水 | 续航 | ... | 置信度 |对低置信度产品:调用 product_detail 补充验证
Step 3: 交叉分析(LLM 直接发现)
LLM 任务:从标注数据中发现供需缺口
## 交叉分析任务
基于以下已标注的 Top100 产品数据,执行交叉分析:
### 数据
[提供标注后的产品数据]
### 分析要求
1. 选择 2-3 对有意义的维度组合(如:功率×价格、防水×场景)
2. 识别:空白点(0产品)、薄供给(≤2产品)、高需求低供给
3. 分析每个缺口的原因(技术限制?需求不存在?被忽视?)
4. 按机会价值排序
### 输出格式
| 维度组合 | 状态 | 产品数 | 月销量 | 原因分析 | 机会评级 |关键点:让 LLM 直接从数据中发现规律,而不是用 Python 脚本计算
Step 4: 竞品与 VOC 分析
竞品选择逻辑表(LLM 按细分段选择):
| ASIN | 品牌 | 选择理由 | 类型 | 覆盖维度 |
|---|---|---|---|---|
| [LLM 选择 6-10 个代表性竞品] |
⛔ 差评维度归类(关键步骤)
必须按维度归类,禁止按 ASIN 组织
LLM 任务:将竞品差评按属性维度归类,并映射到品牌能力和产品方案
输入:competitor_reviews.json(按 ASIN 组织的原始差评) 输出:data.json 中的 voc_analysis 字段(按维度归类)
归类要求: 1. 识别主要维度(3-6 个)- 基于差评内容提取痛点类别 2. 每个维度包含:
dimension: 维度名称(如:音质/音量、舒适度、续航)pain_point: 痛点描述frequency: 提及频次percentage: 占比(如 "32%")affected_brands: 涉及品牌列表brand_opportunity: 品牌/供应链能力如何解决product_solution: 具体产品改进方向
输出格式示例:
{
"voc_analysis": {
"dimensions": [
{
"dimension": "音质/音量",
"pain_point": "音量太小,户外听不清",
"frequency": 45,
"percentage": "32%",
"affected_brands": ["SHOKZ", "JLab"],
"brand_opportunity": "有14.2mm大动圈供应链",
"product_solution": "14.2mm动圈+音量增强模式"
}
],
"summary": "主要痛点集中在音质(32%)、舒适度(28%)、续航(18%)"
}
}禁止的输出方式:
- ❌ 按 ASIN 组织:
{"B0XXX": {"reviews": [...]}} - ❌ 缺少频次/占比数据
- ❌ 缺少品牌机会和产品方案映射
Step 5: 评估与决策
进入壁垒评估:
| 壁垒类型 | 等级 | 数据锚点 | 预估成本 | 缓解方案 |
|---|---|---|---|---|
| Review 壁垒 | 中/高 | Top10 均值 XXX 评论 | $XXX | Vine + PPC |
| 资金壁垒 | 中/高 | 首批备货 + 广告 | ¥XX | 控制首批 MOQ |
| ... | ... | ... | ... | ... |
选品决策评估(五维评分):
| 维度 | 权重 | 评分(1-10) | 加权分 | 依据 |
|---|---|---|---|---|
| 市场规模 | 20% | [LLM 评分] | X.X | [数据依据] |
| 竞争格局 | 25% | [LLM 评分] | X.X | [数据依据] |
| ... | ... | ... | ... | ... |
| 总分 | 100% | - | X.XX | 决策结论 |
决策结论映射:
- 7.5-10分 → 建议进入 (优先推进)
- 6.0-7.4分 → 谨慎进入 (需精准定位,明确准入条件)
- 4.0-5.9分 → 暂缓观望 (需更多数据验证)
- 0-3.9分 → 不建议进入 (风险大于机会)
产品矩阵(Tier 1 必填具体规格):
### Tier 1: [产品定位]
**目标市场**:[维度组合空白/机会]
**决策理由**:[数据依据]
| 维度 | 规格 | 决策依据 |
|------|------|----------|
| [维度1] | [具体值] | [为什么] |
| [维度2] | [具体值] | [为什么] |
**目标定价**:$XX.XX
**差异化主张**:[一句话]
**对标竞品**:[ASIN] — [我们的优势]
**预估月销潜力**:XX-XX 件Step 6: 报告输出
输出文件:
product-research-reports/
└── {category}_{site}_{YYYYMMDD}/
├── report.md # Markdown 完整报告(LLM 直接输出)
├── data.json # 结构化数据(供 Dashboard 使用)
├── dashboard.html # 可视化看板(脚本渲染)
└── raw/ # 数据文件
├── category_info.json # 类目信息
├── top100.json # Top100 产品数据
├── trend.json # 趋势数据
└── keywords.json # 关键词数据data.json 结构(简化版):
{
"metadata": {
"category": "bluetooth speaker",
"site": "US",
"date": "20260319"
},
"market_overview": {
"top100_monthly_sales": 55000,
"top100_monthly_revenue": 5200000,
"avg_price": 95,
"top3_product_concentration": 0.2578,
"top3_brand_concentration": 0.5058,
"top10_brand_concentration": 0.8234
},
"dimensions": [...],
"cross_analysis": [...],
"competitors": [...],
"voc_analysis": {
"dimensions": [
{
"dimension": "音质/音量",
"pain_point": "音量太小,户外听不清",
"frequency": 45,
"percentage": "32%",
"affected_brands": ["SHOKZ", "JLab"],
"brand_opportunity": "采用更大驱动单元",
"product_solution": "14.2mm动圈+音量增强模式"
}
],
"summary": "主要痛点集中在音质(32%)、舒适度(28%)、续航(18%)"
},
"barriers": [...],
"go_nogo": {...}
}⛔ 重要:数据字段命名规范
| 字段名 | 说明 | 示例 |
|---|---|---|
top3_product_concentration | Top3 产品销量占 Top100 总销量的比例 | 0.2578 = 25.78% |
top3_brand_concentration | Top3 品牌销量占 Top100 总销量的比例 | 0.5058 = 50.58% |
top10_brand_concentration | Top10 品牌销量占 Top100 总销量的比例 | 0.8234 = 82.34% |
new_product_share | 新品(上架<6个月)销量占比 | 0.26 = 26% |
禁止模糊命名:
- ❌
top3_concentration(不明确是产品还是品牌) - ✅
top3_product_concentration或top3_brand_concentration
⛔ 重要:VOC 分析数据结构
voc_analysis 字段必须包含按维度归类的差评分析,而非按 ASIN 组织:
| 字段 | 类型 | 说明 |
|---|---|---|
dimension | string | 痛点维度(如:音质/音量、舒适度、续航等) |
pain_point | string | 痛点描述 |
frequency | number | 提及频次 |
percentage | string | 占比(如 "32%") |
affected_brands | array | 涉及的品牌列表 |
brand_opportunity | string | 品牌/供应链能力如何解决 |
product_solution | string | 具体产品改进方案 |
---
Dashboard 渲染规范
render_dashboard.py 负责将 data.json 渲染为可视化看板。关键渲染规则:
产品维度分布
- 必须使用表格形式,禁止使用柱状图
- 双栏布局:左侧价格区间分布,右侧产品形态分布
- 每行显示:维度值、产品数、占比(带颜色标签)
- 占比标签颜色规则:≥30%蓝色、≥20%绿色、≥10%黄色、<10%灰色
交叉分析(价格区间 × 产品形态)
- 必须使用矩阵表格形式,禁止使用图表
- 行:价格区间($0-30 到 $200+)
- 列:产品形态(骨传导、夹耳式、开放式挂耳、入耳式)
- 单元格:产品数量
- 特殊标记:
- 竞争激烈(≥15款):红色"红海"标签
- 市场空白(0款)且为机会点:绿色"机会"标签
- 底部必须有洞察提示框,说明红海和机会区域
示例输出
<!-- 维度分布:双表格布局 -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
<!-- 价格区间表格 -->
<!-- 产品形态表格 -->
</div>
<!-- 交叉分析:矩阵表格 -->
<table>
<thead><!-- 表头:产品形态 --></thead>
<tbody><!-- 行:价格区间,列:产品数 --></tbody>
</table>
<div class="insight-box">洞察:...</div>---
LLM Prompt 模板库
详细 Prompt 模板请参考:references/prompt_templates.md
包含 6 个模板: 1. 属性标注 - 从产品标题提取差异化维度 2. 交叉分析 - 发现供需缺口 3. 竞品选择 - 选择代表性竞品 4. 差评归类 - 按属性维度归类痛点 5. 选品决策评估 - 五维加权决策 6. 产品矩阵规划 - Tier 1/2/3 具体规格
---
硬性规则(⛔ 不可省略)
1. ⛔ Top100 必须完整 100 条 2. ⛔ 关键词至少 3 个维度对比 3. ⛔ 竞品选择 6-10 个,覆盖量级标杆/功能差异/价格带/痛点 4. ⛔ 差评必须按维度归类(非按 ASIN 归类),输出到 data.json 的 voc_analysis 字段 5. ⛔ VOC 分析必须包含:频次、占比、涉及品牌、品牌机会、产品方案 6. ⛔ 选品决策评估必须量化评分 7. ⛔ Tier 1 产品必须具体到规格(禁止"待确认"占位) 8. ⛔ 每个数据表后有"关键洞察"段落 9. ⛔ 空白/薄供给必须附带原因分析 10. ⛔ 数据字段命名必须清晰:使用 top3_product_concentration / top3_brand_concentration,禁止模糊的 top3_concentration 11. ⛔ 数据一致性校验:报告生成前必须校验 data.json 中的数值与报告文本一致
---
常见场景策略
场景1:新手入门(预算<15万)
- 价格 $10-20
- 轻小件
- 无售后风险
- 中国卖家占比 > 70%
场景2:蓝海发现
- Top3 集中度 < 30%
- 新品占比 > 15%
- 关键词首页评论 < 500
场景3:定向品类分析(用户已指定)
- 跳过类目扫描,直接进入数据采集
- ⛔ 必须执行属性标注
- ⛔ 必须执行交叉分析
- ⛔ 必须执行选品决策评估(五维评分)
---
与其他 Skills 的关系
category-selection (品类筛选五维评分)
↓
product-research (深度选品调研) ← 本 Skill
↓
amazon-analyse (竞品 Listing 深挖)
↓
review-analysis (评论深度分析)区别:
category-selection:品类级别的快速筛选,五维评分product-research:指定品类的深度调研,多维度分析 + 选品决策评估amazon-analyse:单个竞品 Listing 的详细分析review-analysis:评论的深度痛点分析
---
支持的站点
US, GB, DE, FR, IT, ES, CA, JP, MX, AE, AU, BR, SA
---
注意事项
1. API Key:自动从 .mcp.json 读取 2. 数据时效:Sorftime 数据可能有 1-7 天延迟 3. API 限流:每批最多 8 个并发请求 4. 编码问题:脚本自动处理 Unicode-escape 和 Mojibake 5. 原始数据:所有 API 响应保存在 raw/ 目录供验证
---
故障排查
常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
HTTP Error 406: Not Acceptable | API参数错误 | 检查参数名是否为 searchName 而非 productName |
An error occurred invoking 'xxx' | API工具不存在 | 检查 TOOLS 映射表中的工具名称 |
未查询到对应产品 | ASIN无效或站点错误 | 验证ASIN格式, 确认产品在该站点销售 |
Authentication required | API Key错误 | 检查 .mcp.json 中的 key 参数 |
| 中文乱码 | Mojibake编码 | 脚本自动修复, 或运行 fix_encoding.py |
IndentationError: unexpected indent | Windows 命令行问题 | 使用脚本文件而非 python -c |
输出目录路径错误 | 相对路径问题 | 使用 run_analysis.py,自动处理路径 |
NameError: name 'xxx' is not defined | 缺少 datetime 导入 | 检查脚本 import 语句 |
| Dashboard 渲染问题 | ||
| Dashboard 显示空白 | data.json 结构不匹配 | v3.5 已修复:run_analysis.py 自动渲染 Dashboard |
| Dashboard 未自动生成 | 旧版本未集成渲染 | v3.5 已修复:数据采集完成后自动渲染 |
PermissionError: [Errno 13] | 传递目录路径而非文件路径 | 使用绝对路径调用:python render_dashboard.py -o output.html data.json |
unrecognized arguments | 参数顺序错误 | 正确格式:python render_dashboard.py -o dashboard.html data.json |
| Dashboard 缺少 VOC 数据 | LLM 未生成完整 voc_analysis | 确保 LLM 生成包含 voc_analysis.dimensions 的完整 data.json |
| Dashboard 交叉分析为空 | price_type_matrix 数据缺失 | 确保数据采集包含价格区间分析 |
KeyError: 'xxx' | 字段名不一致 | v3.4 已修复:支持新旧字段名兼容 |
AttributeError: 'str' object has no attribute 'get' | data.json 格式问题 | v3.4 已修复:自动转换为列表格式 |
Dashboard 手动渲染方法
如果自动渲染失败,可以手动调用:
# 从输出目录调用
python .claude/skills/product-research/scripts/render_dashboard.py \
-o product-research-reports/{keyword}_{site}_{date}/dashboard.html \
product-research-reports/{keyword}_{site}_{date}/data.json
# 或者使用绝对路径
python "D:\amazon-mcp\.claude\skills\product-research\scripts\render_dashboard.py" \
-o "D:\amazon-mcp\product-research-reports\{keyword}_{site}_{date}\dashboard.html" \
"D:\amazon-mcp\product-research-reports\{keyword}_{site}_{date}\data.json"API 工具名称对照表
| 功能 | 工具名称 | 参数 |
|---|---|---|
| 类目搜索 | category_name_search | amzSite, searchName |
| 类目报告 | category_report | amzSite, nodeId |
| 类目趋势 | category_trend | amzSite, nodeId, trendIndex |
| 关键词详情 | keyword_detail | amzSite, keyword |
| 产品详情 | product_detail | amzSite, asin |
| 产品评论 | product_reviews | amzSite, asin, reviewType |
调试技巧
1. 启用详细输出: 在脚本中添加 print() 调试信息 2. 检查原始响应: 查看 SSE 响应的实际内容 3. 分步执行: 使用 Python 交互式环境逐行调试 4. 验证API Key: curl "https://mcp.sorftime.com?key=YOUR_KEY" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
---
版本: v3.6 (两阶段工作流 + 数据验证) | 最后更新: 2026-03-19
Product Research Skill
基于 Sorftime MCP + LLM Agent 的 Amazon 选品深度调研技能。
核心特点
- LLM 驱动:分析、洞察、决策全部由 LLM 完成
- 交互式执行:逐步推进,用户可中途干预
- 轻量脚本:仅用于 API 调用和 Dashboard 渲染
- 简化数据:不用复杂的 unified payload 结构
使用方法
/product-research [产品关键词] [站点]示例:
/product-research "bluetooth speaker" US/product-research laptop backpack GB
执行流程
Step 0: 信息收集(确认站点、场景、约束)
↓
Step 1: 数据采集(Top100、关键词、趋势、竞品)
↓
Step 2: 属性标注(LLM 从标题提取维度)
↓
Step 3: 交叉分析(LLM 发现供需缺口)
↓
Step 4: 竞品与 VOC(LLM 选择竞品、归类差评)
↓
Step 5: 评估决策(壁垒评估 + 选品决策评分)
↓
Step 6: 报告输出(Markdown + Dashboard)输出
report.md- Markdown 完整报告(LLM 直接撰写)data.json- 结构化数据(供 Dashboard 使用)dashboard.html- 可视化看板(脚本渲染)
与其他 Skills 的关系
category-selection (品类筛选五维评分)
↓
product-research (深度选品调研) ← 本技能
↓
amazon-analyse (竞品 Listing 深挖)
↓
review-analysis (评论深度分析)脚本架构(极简)
scripts/
├── api_client.py # Sorftime API 调用 + SSE 解析
└── render_dashboard.py # Dashboard 可视化渲染设计原则:
- 脚本不做分析判断(由 LLM 完成)
- 脚本不做复杂计算(让 LLM 从数据中发现)
- 脚本仅做数据搬运(API → 结构化数据)
版本历史
| 版本 | 日期 | 变更 |
|---|---|---|
| v2.0 | 2026-03-19 | LLM Agent 驱动,简化脚本架构 |
| v1.0 | 2026-03-19 | 初始版本 |
许可证
MIT License
Sorftime MCP API 快速参考
品类选品分析常用接口
1. category_name_search - 搜索类目
curl -s -X POST "https://mcp.sorftime.com?key={API_KEY}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"category_name_search","arguments":{"amzSite":"US","searchName":"Sofas"}}}'返回关键数据: NodeId (用于后续调用)
---
2. category_report - 类目报告 (核心)
curl -s -X POST "https://mcp.sorftime.com?key={API_KEY}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"category_report","arguments":{"amzSite":"US","nodeId":"3733551"}}}'返回数据:
Top100产品[]: 产品列表 (ASIN, 标题, 价格, 月销量, 星级, 品牌, 评论数, 卖家来源等)类目统计报告: 统计数据
关键统计字段:
| 字段名 | 说明 | 用途 |
|---|---|---|
top100产品月销量 | Top100 总销量 | 市场规模 |
top100产品月销额 | Top100 总销额 | 市场规模 |
average_price | 平均价格 | 定价参考 |
top3_brands_sales_volume_share | Top3 品牌占比 | 竞争集中度 |
amazonOwned_sales_volume_share | Amazon 自营占比 | 平台压力 |
low_reviews_sales_volume_share | 低评论产品占比 | 新品机会 |
---
3. product_detail - 产品详情
curl -s -X POST "https://mcp.sorftime.com?key={API_KEY}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"product_detail","arguments":{"amzSite":"US","asin":"B0DDTCQGTR"}}}'返回关键数据: 标题, 主图URL, 价格, 星级, 评论数, 品牌, 上线日期, 月销量, 产品描述等
---
4. category_keywords - 类目关键词
curl -s -X POST "https://mcp.sorftime.com?key={API_KEY}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"category_keywords","arguments":{"amzSite":"US","nodeId":"3733551","page":1}}}'返回关键数据:
关键词: 关键词周搜索排名: 搜索排名月搜索量: 月搜索量cpc精准竞价: PPC 竞价
---
SSE 响应处理
响应格式
event: message
data: {"result":{"content":[{"type":"text","text":"..."}}]}Python 解码示例
import codecs
# 解码 Unicode 转义
decoded = codecs.decode(encoded_text, 'unicode-escape')---
支持的站点
| 代码 | 站点 |
|---|---|
| US | 美国 |
| GB | 英国 |
| DE | 德国 |
| FR | 法国 |
| CA | 加拿大 |
| JP | 日本 |
| ES | 西班牙 |
| IT | 意大利 |
Sorftime API 快速参考 (Product-Research)
API 端点
https://mcp.sorftime.com?key={API_KEY}请求格式
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "{工具名称}",
"arguments": {
"amzSite": "US",
...
}
}
}响应格式 (SSE)
event: message
data: {"result":{"content":[{\"type\":\"text\",\"text\":\"{数据}\"}],\"isError\":false},"id":1,\"jsonrpc\":\"2.0\"}
---
常用 API 工具
1. category_name_search
用途: 按名称搜索类目,获取 NodeId
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | ✓ | 站点代码 (US, GB, DE, etc.) |
| searchName | string | ✓ | 类目名称关键词 |
示例:
curl -s -X POST "https://mcp.sorftime.com?key={KEY}" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "category_name_search",
"arguments": {
"amzSite": "US",
"searchName": "bluetooth speaker"
}
}
}'响应:
[
{
"nodeId": "7073956011",
"Name": "Portable Bluetooth Speakers"
},
{
"nodeId": "12097477011",
"Name": "Outdoor Speakers"
}
]---
2. category_report
用途: 获取类目 Top100 产品和统计数据
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | ✓ | 站点代码 |
| nodeId | string | ✓ | 类目 Node ID |
示例:
client.get_category_report("US", "7073956011")响应结构:
{
"Top100产品": [
{
"ASIN": "B0XXXXXXXX",
"标题": "...",
"月销量": "10000",
"月销额": "500000.00",
"品牌": "JBL",
"价格": 49.99,
"评论数": 5000,
"星级": 4.7
}
],
"类目统计报告": {
"nodeid": "7073956011",
"类目名称": "Portable Bluetooth Speakers",
"top100产品月销量": "279733",
"top100产品月销额": "19842968.40",
"top3_product_sales_volume_share": "19.66%"
}
}---
3. category_trend
用途: 获取类目趋势数据
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | ✓ | 站点代码 |
| nodeId | string | ✓ | 类目 Node ID |
| trendIndex | string | ✗ | 趋势类型 (默认: NewProductSalesAmountShare) |
trendIndex 选项:
NewProductSalesAmountShare- 新品销量占比NewProductProductShare- 新品数量占比BrandConcentration- 品牌集中度PriceDistribution- 价格分布
示例:
trend = client.get_category_trend("US", "7073956011", "NewProductSalesAmountShare")响应:
[
"2024年03月=3.32",
"2024年04月=1.98",
...
]---
4. keyword_detail
用途: 获取关键词详情
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | ✓ | 站点代码 |
| keyword | string | ✓ | 关键词 |
示例:
detail = client.get_keyword_detail("US", "bluetooth speaker")响应结构:
{
"搜索量": "50000",
"CPC": "1.50",
"竞价": "8",
"自然位产品": [...]
}---
5. product_detail
用途: 获取单个产品详情
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | ✓ | 站点代码 |
| asin | string | ✓ | 产品 ASIN |
---
6. product_reviews
用途: 获取产品评论
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | ✓ | 站点代码 |
| asin | string | ✓ | 产品 ASIN |
| reviewType | string | ✗ | 评论类型 (Both/Positive/Negative) |
---
Python 客户端使用
基本用法
from api_client import SorftimeClient
client = SorftimeClient()
# 搜索类目
categories = client.search_category_by_product_name("US", "bluetooth speaker")
node_id = categories[0]['nodeId']
# 获取 Top100
top100 = client.get_category_report("US", node_id)
products = top100.get('Top100产品', [])
# 获取趋势
trend = client.get_category_trend("US", node_id)
# 获取关键词详情
keyword_data = client.get_keyword_detail("US", "bluetooth speaker")批量调用
# 并发获取多个产品详情
asins = ["B0XXX1", "B0XXX2", "B0XXX3"]
details = []
for asin in asins:
try:
detail = client.get_product_detail("US", asin)
details.append(detail)
except Exception as e:
print(f"Failed for {asin}: {e}")---
支持的站点
| 代码 | 市场 |
|---|---|
| US | 美国亚马逊 |
| GB | 英国亚马逊 |
| DE | 德国亚马逊 |
| FR | 法国亚马逊 |
| IT | 意大利亚马逊 |
| ES | 西班牙亚马逊 |
| CA | 加拿大亚马逊 |
| JP | 日本亚马逊 |
| MX | 墨西哥亚马逊 |
| AE | 阿联酋亚马逊 |
| AU | 澳大利亚亚马逊 |
| BR | 巴西亚马逊 |
| SA | 沙特阿拉伯亚马逊 |
---
数据类型说明
月销量/月销额
- 类型:
string(需要转换为数字) - 示例:
"28908","1443954.60" - 转换:
float(value)
价格
- 类型:
float或string - 示例:
49.95,"29.99"
评论数
- 类型:
int或string - 示例:
14558,"5000"
---
错误代码
| HTTP 状态 | 含义 | 解决方案 |
|---|---|---|
| 200 | 成功 | - |
| 406 | 参数错误 | 检查参数名称和格式 |
| 401 | 认证失败 | 检查 API Key |
| 500 | 服务器错误 | 稍后重试 |
---
最后更新: 2026-03-19
LLM Prompt 模板库
本文档提供 product-research Skill 中使用的 Prompt 模板,供 LLM 执行各分析步骤时参考。
---
模板 1: 属性标注
使用场景
Step 2: 属性标注阶段 - LLM 从 Top100 产品标题中提取关键差异化维度
Prompt 模板
你是一位产品分析专家,擅长从产品标题中识别关键差异化维度。
## 任务目标
分析以下 Top100 产品标题,提取 3-6 个关键差异化维度。
## 分析样本
### 前 20 个产品标题样本:
{titles_sample}
### 分析要求
1. **识别差异化维度**(3-6 个)
- 维度应该是该品类的关键差异化因素
- 如:电子产品的功率、容量、防水等级;家居产品的材质、尺寸、风格等
- 避免通用维度(如颜色、包装)
2. **为每个产品标注维度值**
- 从标题中提取信息
- 如果标题中缺失,标注为"未知"
- 标注置信度:高(标题明确)、中(需要推断)、低(缺失/不确定)
3. **维度值分类**
- 每个维度的值应该是可分类的
- 如:功率 -> 20W/30W/65W/100W+
- 如:防水 -> IPX7/IPX5/无
## 输出格式
### 维度定义表
| 维度名称 | 说明 | 候的分类 |
|---------|------|---------|
| 功率 | 输出功率 | 20W以下 / 20-30W / 30-45W / 45-65W / 65W+ |
| 防水 | 防水等级 | IPX7+ / IPX5-6 / 无 |
| ... | ... | ... |
### 标注结果示例
| ASIN | 标题 | [维度1] | [维度2] | ... | 置信度 |
|------|------|---------|---------|-----|--------|
| B0XXX | 产品标题... | 20W | IPX7 | ... | 高 |---
模板 2: 交叉分析
使用场景
Step 3: 交叉分析阶段 - LLM 从已标注数据中发现供需缺口
Prompt 模板
你是一位市场机会分析师,擅长从数据中发现供需缺口和市场机会。
## 任务目标
基于已标注的 Top100 产品数据,执行交叉分析,发现未被满足的市场需求。
## 输入数据
### 已标注产品数据(部分样本)
{annotated_products_sample}
### 市场概况
- Top100 月销量: {monthly_sales}
- Top100 月销额: {monthly_revenue}
- 平均价格: ${avg_price}
## 分析要求
### 1. 选择维度组合
- 选择 2-3 对有意义的维度组合
- 考虑因素:
- 维度之间的关联性(如:功率×价格、防水×场景)
- 市场需求合理性
- 数据完整性
### 2. 识别供需状态
对每个维度组合,识别:
- **空白点**:产品数 = 0
- **薄供给**:产品数 ≤ 2
- **高需求低供给**:月销量高但产品数少
### 3. 原因分析
对每个空白/薄供给,分析:
- 技术限制(无法实现或成本过高)
- 需求不存在(消费者不需要)
- 被市场忽视(存在但未被满足)
- 供应链难度
### 4. 机会评级
按三维评估排序:
- 市场规模(40%):月销额 $100K+ = 高,$50K-100K = 中,<$50K = 低
- 技术可行性(30%):现有产品线 = 高,需新模具 = 中,需研发 = 低
- 品牌匹配(30%):核心优势 = 高,部分匹配 = 中,全新领域 = 低
## 输出格式
### 交叉分析矩阵
| 维度A × 维度B | 状态 | 产品数 | 月销量 | 均价 | 原因分析 | 机会评级 |
|--------------|------|--------|--------|------|----------|---------|
| 65W × $50-80 | 薄供给 | 1 | 2000 | $70 | 技术可行但被忽视 | 高 |
| ... | ... | ... | ... | ... | ... | ... |
### 关键发现
- 哪些组合是市场主力?(高供给 + 高需求)
- 哪些组合存在明显空白?
- 哪些空白值得进入?---
模板 3: 竞品选择逻辑
使用场景
Step 4: 竞品与 VOC 分析 - LLM 按细分段选择代表性竞品
Prompt 模板
你是一位竞品分析专家,需要选择代表性竞品进行深度分析。
## 任务目标
从 Top100 产品中选择 6-10 个代表性竞品,用于差评分析和策略制定。
## 输入数据
### Top100 产品数据(部分样本)
{top100_sample}
### 已标注维度
{dimensions_summary}
## 选择要求
### 必须覆盖的细分段
1. **量级标杆**(1-2 个)
- Top3-5 销量产品
- 代表市场标准
2. **功能差异代表**(每个主要维度 1 个)
- 各维度的头部产品
- 如:高功率代表、防水等级代表
3. **价格带覆盖**(高/中/低各 1 个)
- 高价段:> 平均价 30%
- 中价段:平均价 ± 20%
- 低价段:< 平均价 30%
4. **痛点参考**(1-2 个)
- 差评率高或评分低的产品
- 用于挖掘改进机会
## 输出格式
| ASIN | 品牌 | 选择理由 | 竞品类型 | 覆盖维度 | 价格 | 月销量 | 评论数 |
|------|------|----------|----------|----------|------|--------|--------|
| B0XXX | BrandA | 类目 Top3,覆盖主力价格带 | 量级标杆 | 价格-中 | $XX | XXXX | XXX |
| ... | ... | ... | ... | ... | ... | ... | ... |---
模板 4: 差评维度归类
使用场景
Step 4: 竞品与 VOC 分析 - LLM 按属性维度归类差评痛点
Prompt 模板
你是一位产品开发顾问,擅长从用户评论中挖掘产品痛点和改进机会。
## 任务目标
将竞品差评按属性维度归类,并映射到品牌能力和产品方案。
## 输入数据
### 竞品选择逻辑表
{competitor_selection}
### 差评样本(部分)
{reviews_sample}
## 归类要求
### 按属性维度(而非按产品)归类
1. **识别主要维度**(3-6 个)
- 基于差评内容提取痛点类别
- 如:续航/电池、功率/充电、数显、线材、外观、质量等
2. **每个维度包含**
- 痛点描述:用户不满的具体问题
- 频次/占比:涉及多少条差评
- 涉及竞品:哪些品牌/产品有此问题
- 品牌机会:我们的品牌/供应链能如何解决
- 产品方案:具体的产品改进方向
### 痛点→方案映射
| 要素 | 说明 | 示例 |
|------|------|------|
| 痛点描述 | 用户不满的具体问题 | "电池容量虚标,实际续航不足 50%" |
| 数据支撑 | 差评频次/占比 | "涉及 45 条差评,占比 32%" |
| 品牌机会 | 品牌/供应链能力 | "有高密度电芯供应链" |
| 产品方案 | 具体改进方案 | "4000mAh 实标 + 实测视频营销" |
## 输出格式
### 差评维度归类表
| 维度 | 痛点 | 频次 | 占比 | 涉及竞品 | 品牌机会 | 产品方案 |
|------|------|------|------|----------|----------|----------|
| 续航/电池 | 容量虚标 | 45 | 32% | BrandA,B | 高密度电芯 | 4000mAh 实标 |
| ... | ... | ... | ... | ... | ... | ... |---
模板 5: 选品决策评估(五维评分)
使用场景
Step 5: 评估与决策 - LLM 进行量化评分并给出决策
Prompt 模板
你是一位投资决策专家,需要基于市场数据进行选品决策量化评分。
## 任务目标
对选品机会进行五维加权评分,给出明确的进入决策建议。
## 评分体系
| 维度 | 权重 | 评分标准 (1-10) | 数据来源 |
|------|------|---------------|----------|
| 市场规模 | 20% | 月销额>$10M=10, >$5M=8, >$1M=6, 其他=4 | Top100 月销额 |
| 竞争格局 | 25% | CR3<30%=10, <50%=7, 其他=4 | CR3 + 新品占比 |
| 需求清晰度 | 15% | 关键词+交叉分析明确=10, 较明确=7, 模糊=4 | 关键词数据 + 交叉分析 |
| 进入壁垒(反) | 20% | 低壁垒=10, 中=6, 高=3 | 六类壁垒评估 |
| 盈利能力 | 20% | 毛利>40%=10, >30%=8, >20%=6, 其他=4 | 成本测算 |
## 决策矩阵
| 加权总分 | 决策结论 | 详细说明 |
|----------|----------|----------|
| 7.5-10 | **建议进入** | 优先推进,快速执行 |
| 6.0-7.4 | **谨慎进入** | 需精准定位细分市场,明确准入条件 |
| 4.0-5.9 | **暂缓观望** | 需更多数据验证,或等待时机 |
| 0-3.9 | **不建议进入** | 风险大于机会,放弃 |
## 输入数据
### 市场概况
{market_overview}
### 竞争格局
{competition_summary}
### 交叉分析
{cross_analysis_summary}
### 进入壁垒
{barriers_summary}
### 成本测算
| 项目 | 金额 |
|------|------|
| 采购成本 | $XX |
| FBA 费用 | $XX |
| 头程物流 | $XX |
| 预估毛利 | $XX |
| 预估毛利率 | XX% |
## 输出格式
### 选品决策评估表
| 维度 | 权重 | 评分(1-10) | 加权分 | 依据 |
|------|------|-----------|--------|------|
| 市场规模 | 20% | [评分] | [分数] | [数据依据] |
| 竞争格局 | 25% | [评分] | [分数] | [数据依据] |
| ... | ... | ... | ... | ... |
| **总分** | 100% | - | **[总分]** | - |
### 决策结论
- **决策**: 建议进入 / 谨慎进入 / 暂缓观望 / 不建议进入
- **综合得分**: [X.XX]/10
- **核心洞察**: [2-3句话总结]
### 详细说明
**准入条件** (如适用):
1. [必须满足的条件1]
2. [必须满足的条件2]
...
**禁止进入**:
- [明确禁止的场景]
**风险提示**:
- [关键风险及缓解方案]---
模板 6: 产品矩阵规划
使用场景
Step 5: 评估与决策 - LLM 规划具体产品矩阵
Prompt 模板
你是一位产品经理,需要基于分析结果规划具体的产品矩阵。
## 任务目标
规划 Tier 1(必须)和 Tier 2/3(可选)产品的具体规格。
## 输入数据
### 机会优先级
{opportunities_ranking}
### 供需缺口
{gaps_summary}
### 痛点分析
{pain_points_summary}
## 产品矩阵要求
### Tier 1 产品(必须完整具体)
### 结构模板
Tier 1: [产品定位一句话]
目标市场:[维度组合空白/机会,如:65W + 数显 + $50-80 价格带] 决策理由:[基于 cross_analysis ch04 + pain_points ch06 的数据]
| 维度 | 规格 | 决策依据 |
|---|---|---|
| [维度1] | [具体值] | [为什么选这个值 - 引用数据] |
| [维度2] | [具体值] | [为什么选这个值 - 引用数据] |
| ... | ... | ... |
目标定价:$XX.XX(基于 Step 5 测算,毛利率 XX%) 差异化主张:[一句话核心卖点,区别于竞品] 对标竞品:[ASIN] [品牌] $XX — 我们的优势:[具体差异] 预估月销潜力:XX-XX 件/月(基于同组合竞品表现推算)
### ⛔ 硬性要求
1. **禁止占位语**:
- ❌ "待确认"、"待定"、"建议进一步调研"
- ✅ 具体数值和明确依据
2. **必须包含的字段**:
- 目标市场(维度组合空白)
- 决策理由(数据依据)
- 完整规格表(维度×规格×依据)
- 目标定价(基于成本测算)
- 差异化主张(一句话)
- 对标竞品(具体 ASIN)
- 预估月销潜力(基于数据推算)
### Tier 2/3 产品(可选)
如果有多个高价值机会,规划 Tier 2/3:
- 简化规格(只列出关键差异化维度)
- 预估优先级(何时进入)---
使用指南
在 SKILL.md 中引用
### Step 2: 属性标注
使用 LLM Prompt 模板 [属性标注] 进行维度提取:
> 请参考 `references/prompt_templates.md` 中的 [模板 1: 属性标注] 对以下产品标题进行维度标注...
### Step 3: 交叉分析
使用 LLM Prompt 模板 [交叉分析] 发现供需缺口:
> 请参考 `references/prompt_templates.md` 中的 [模板 2: 交叉分析] 对已标注数据进行分析...动态调整
根据品类特征调整模板:
- 电子产品:维度通常包括功率、容量、防水、接口类型等
- 家居产品:维度通常包括材质、尺寸、风格、颜色等
- 服装配饰:维度通常包括材质、尺码、风格、季节等
---
版本: v1.1 (中文决策术语) | 最后更新: 2026-03-19
更新日志
v1.1 (2026-03-19)
- ✅ Go/No-Go 评分 → 选品决策评估(五维评分)
- ✅ 决策结论更清晰:建议进入/谨慎进入/暂缓观望/不建议进入
- ✅ 输出格式新增:准入条件、禁止进入、风险提示
v1.0 (2026-03-19)
Sorftime MCP API 接口文档
调用方式
curl -s -X POST "https://mcp.sorftime.com?key={API_KEY}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":N,"method":"tools/call","params":{"name":"TOOL_NAME","arguments":{...}}}'---
一、产品相关接口
1.1 产品详情 (product_detail)
调用消耗: 1
用途: 查询亚马逊电商平台上产品的详情数据
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 US/GB/DE/FR/IN/CA/JP/ES/IT/MX/AE/AU/BR/SA |
| asin | string | 是 | 产品ASIN |
返回数据: 标题、价格、评分、评论数、品牌、类目、排名、销量等
---
1.2 产品子体明细 (product_variations)
调用消耗: 1
用途: 查询亚马逊电商平台产品的子体明细
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| asin | string | 是 | 产品ASIN(仅支持单ASIN) |
---
1.3 产品历史趋势 (product_trend)
调用消耗: 1
用途: 查询产品的历史趋势数据,支持月销量/月销额/价格/排名趋势
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| asin | string | 是 | 产品ASIN |
| productTrendType | string | 否 | 月销量趋势/月销额趋势/价格趋势/所属大类排名趋势 |
---
1.4 产品评论 (product_reviews)
调用消耗: 1
用途: 查询产品近一年的用户留评,最多返回100条
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| asin | string | 是 | 产品ASIN |
| reviewType | string | 否 | 全部(不限星级)/积极评论(4-5星)/消极评论(1-3星) |
---
1.5 产品流量关键词 (product_traffic_terms)
调用消耗: 1
用途: 产品反查关键词,返回产品在哪些关键词前3页中曝光
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| asin | string | 是 | 产品ASIN |
| page | int | 否 | 页码索引,默认第1页,每页50条 |
---
1.6 竞品关键词布局 (competitor_product_keywords)
调用消耗: 1
用途: 获取竞品在各核心关键词下的曝光位置(自然曝光)
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| asin | string | 是 | 产品ASIN |
| page | int | 否 | 页码索引,默认第1页 |
---
1.7 产品关键词排名趋势 (product_keyword_rank_trend)
调用消耗: 1
用途: 产品在指定关键词下曝光的排名趋势
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| asin | string | 是 | 产品ASIN |
| keyword | string | 是 | 关键词 |
| page | int | 否 | 页码索引,默认第1页 |
---
1.8 产品搜索 (product_search)
调用消耗: 1
用途: 搜索或筛选亚马逊产品,支持多维度筛选实现选品功能
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchName | string | 否 | 搜索产品名称 |
| brand | string | 否 | 筛选品牌 |
| delivery_type | string | 否 | 发货方式 |
| month_sales_volume_range | string | 否 | 月销量范围[x,y] |
| price_range | string | 否 | 价格范围[x,y] |
| property_name | string | 否 | 标题或属性包含词 |
| ratings_count_range | string | 否 | 评论数量范围[x,y] |
| ratings_range | string | 否 | 星级范围[x,y] |
| seasonal_popular_product | string | 否 | 热销旺季产品 |
| seller_name | string | 否 | 卖家名称 |
| subcategory_rank_range | string | 否 | 细分类目排名范围[x,y] |
| variation_count_range | string | 否 | 子体数量范围[x,y] |
| sortby_potential_index | string | 否 | 按潜力指数排序 |
---
1.9 潜力产品搜索 (potential_product_search)
调用消耗: 1
用途: 搜索亚马逊平台上的潜力产品
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 支持的站点 US/GB/DE |
| searchName | string | 否 | 产品名称 |
| price_range | string | 否 | 价格范围[x,y] |
| month_sales_volume_range | string | 否 | 月销量范围[x,y] |
| delivery_type | string | 否 | 发货方式 |
---
二、类目相关接口
2.1 类目名称搜索 (category_name_search)
调用消耗: 1
用途: 基于名称查询细分类目市场,返回nodeid和name
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchName | string | 是 | 类目市场名称 |
---
2.2 类目树结构 (category_tree)
调用消耗: 5
用途: 查询类目产品的特点
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchName | string | 是 | 类目名称 |
---
2.3 细分类目报告 (category_report)
调用消耗: 1
用途: 细分类目实时数据报告,基于Top100产品统计
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| nodeId | string | 否 | 细分类目nodeid |
---
2.4 细分类目历史报告 (category_history_report)
调用消耗: 1
用途: 细分类目历史指定时间段数据报告
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| nodeId | string | 否 | 细分类目nodeid |
| startDate | string | 是 | 起始时间(yyyy-MM-dd) |
| endDate | string | 否 | 截止时间,最长40天 |
---
2.5 类目趋势 (category_trend)
调用消耗: 1
用途: 查询类目市场趋势数据,基于Top100统计
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| nodeId | string | 是 | 细分类目nodeid |
| trendIndex | string | 是 | 趋势类型(见下方) |
趋势类型 (trendIndex):
- 类目月销量趋势
- 品牌数量趋势
- 卖家数量趋势
- 平均售价趋势
- 平均评论数量趋势
- 平均星级趋势
- 上架3个月内新品销量占比趋势
- 亚马逊自营销量占比趋势
- 销量前3的产品销量占比趋势
- 销量前3的品牌销量占比趋势
- 销量前3的卖家销量占比趋势
---
2.6 类目市场搜索 (category_market_search)
调用消耗: 1
用途: 查询或搜索细分类目市场
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchName | string | 否 | 类目市场名称 |
| month_sales_volume_range | string | 否 | 月销量范围[x,y] |
| ratings_range | string | 否 | 星级范围[x,y] |
| ratings_count_range | string | 否 | 评论数范围[x,y] |
| price_range | string | 否 | 平均销售价范围[x,y] |
| seasonal_popular_product | string | 否 | 热销旺季 |
| top3Product_sales_share | string | 否 | Top3产品销量占比x,y |
| amazonOwned_sales_share | string | 否 | 亚马逊自营占比x,y |
| top100_top400_sales_share | string | 否 | Top100在Top400占比x,y |
| newproduct_sales_share | string | 否 | 新品销量占比x,y |
---
2.7 类目核心关键词 (category_keywords)
调用消耗: 1
用途: 查询细分类目市场的核心关键词
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| nodeId | string | 是 | 细分类目nodeid |
| page | int | 否 | 页码索引,默认第1页 |
---
三、关键词相关接口
3.1 关键词详情 (keyword_detail)
调用消耗: 1
用途: 查询热搜关键词详情
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| keyword | string | 是 | 查询的关键词 |
---
3.2 关键词搜索结果 (keyword_search_result)
调用消耗: 1
用途: 查询关键词搜索结果自然位产品清单
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchKeyword | string | 是 | 查询的关键词 |
| page | int | 否 | 页码索引,默认第1页 |
---
3.3 关键词历史趋势 (keyword_trend)
调用消耗: 1
用途: 查询关键词历史趋势(搜索量/搜索排名/CPC价格)
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchKeyword | string | 是 | 查询的关键词 |
---
3.4 关键词延伸词 (keyword_related_words)
调用消耗: 1
用途: 查询关键词的延伸词,用于发现长尾词
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | 亚马逊站点 |
| searchKeyword | string | 是 | 查询的关键词 |
| page | int | 否 | 页码索引,默认第1页 |
---
四、关键词词库管理接口
4.1 添加关键词收藏 (add_keyword)
调用消耗: 1
参数: site, keyword, dict(可选)
---
4.2 移动关键词到收藏夹 (move_keyword)
调用消耗: 1
参数: site, keyword, toDict, fromDict(可选)
---
4.3 删除关键词收藏 (remove_keyword)
调用消耗: 1
参数: site, keyword, dict(可选)
---
4.4 查询收藏夹列表 (query_keyword_dict_list)
调用消耗: 1
参数: site, page
---
4.5 查询收藏的词 (query_keyword_dict)
调用消耗: 1
参数: site, dict(可选,all查询全部), page
---
五、1688 供货平台接口
5.1 1688产品搜索 (products_1688)
调用消耗: 1
用途: 通过1688平台找产品的采购货源,分析产品采购成本价
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| searchName | string | 是 | 查询的产品名称 |
| page | int | 否 | 页码索引,默认第1页,每页50条 |
---
六、TikTok 电商平台接口
6.1 TikTok产品搜索 (tiktok_product_search)
调用消耗: 1
用途: 查询产品在TikTok平台上的相似产品,分析销售情况
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| searchName | string | 是 | 查询的产品名称 |
| page | int | 是 | 页码索引,默认第1页,每页50条 |
---
6.2 TikTok产品详情 (tiktok_product_detail)
调用消耗: 1
用途: 查询TikTok平台产品详情
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| productId | string | 是 | 产品ID |
---
6.3 TikTok带货视频 (tiktok_product_videos)
调用消耗: 1
用途: 查询TikTok平台产品的带货视频
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| productId | string | 是 | 产品ID |
| page | int | 是 | 页码索引,默认第1页,每页50条 |
---
6.4 TikTok带货达人分析 (tiktok_product_influencers)
调用消耗: 1
用途: TikTok平台产品的带货达人分析
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| productId | string | 是 | 产品ID |
---
6.5 TikTok产品趋势 (tiktok_product_trend)
调用消耗: 1
用途: 查询TikTok平台产品趋势,返回销量、价格、星级、评论数量、新增带货视频数、新增带货达人数
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| productId | string | 是 | 产品ID |
---
6.6 TikTok达人搜索 (tiktok_influencer_search)
调用消耗: 1
用途: 按产品名称搜索相关带货达人
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| searchName | string | 是 | 搜索的产品名称 |
| page | int | 是 | 页码索引,默认第1页,每页50条 |
---
6.7 TikTok类目搜索 (tiktok_category_name_search)
调用消耗: 1
用途: 按名称搜索TikTok上相关类目市场,返回类目市场名称和nodeid
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| searchName | string | 是 | 搜索的产品名称 |
---
6.8 TikTok类目报告 (tiktok_category_report)
调用消耗: 1
用途: 查询TikTok电商平台指定类目的类目数据报告
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amzSite | string | 是 | TikTok站点 US/GB/MY/PH/VN/ID |
| nodeId | string | 是 | 类目市场nodeid,可通过tiktok_category_name_search获得 |
---
支持的平台站点
亚马逊 (14个站点)
US, GB, DE, FR, IN, CA, JP, ES, IT, MX, AE, AU, BR, SA
TikTok (6个站点)
US, GB, MY, PH, VN, ID
1688 供货平台
国内批发采购平台
调用限制
- 大部分接口调用消耗: 1
- category_tree: 5
- 返回数据为SSE格式,需解析
---
最后更新: 2026-03-03
Product-Research 故障排查指南
快速诊断流程
问题发生
↓
是 API 调用错误? → 查看第2节
↓
是数据解析错误? → 查看第3节
↓
是编码问题? → 查看第4节
↓
其他问题 → 查看第5节---
1. 数据采集失败
问题: 类目搜索返回 406 错误
症状: HTTP Error 406: Not Acceptable
原因: API 参数名称错误
解决方案:
# ❌ 错误写法
client._call('category_search_from_product_name', {
'amzSite': 'US',
'productName': 'bluetooth speaker' # 错误!
})
# ✅ 正确写法
client.search_category_by_product_name('US', 'bluetooth speaker')
# 或直接调用
client._call('category_name_search', {
'amzSite': 'US',
'searchName': 'bluetooth speaker' # 正确!
})问题: 找不到类目
症状: 返回空列表或 "未查询到对应类目"
诊断步骤: 1. 检查关键词拼写 2. 尝试更通用的关键词 (如 "speaker" 而非 "portable bluetooth speaker") 3. 检查站点是否支持该类目
解决方案:
# 尝试多个关键词
keywords = ['bluetooth speaker', 'portable speaker', 'wireless speaker', 'speaker']
for kw in keywords:
result = client.search_category_by_product_name('US', kw)
if result:
break---
2. API 调用错误
问题: "An error occurred invoking 'xxx'"
原因: 工具名称不存在
常用工具名称对照:
| 功能 | 正确名称 | 错误名称 |
|---|---|---|
| 类目搜索 | category_name_search | category_search_from_product_name ❌ |
| 类目报告 | category_report | - |
| 关键词详情 | keyword_detail | - |
| 产品详情 | product_detail | - |
问题: 认证失败
症状: Authentication required
检查:
# 验证 API Key
curl "https://mcp.sorftime.com?key=YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'解决方案: 1. 检查 .mcp.json 文件 2. 确认 URL 格式: https://mcp.sorftime.com?key=XXX 3. 获取新 API Key: https://sorftime.com/zh-cn/mcp
---
3. 数据解析错误
问题: Top100 数据解析失败
症状: KeyError: 'Top100产品' 或产品列表为空
原因: Sorftime 返回格式可能有多种变体
解决方案:
def safe_extract_products(data):
"""安全提取产品列表"""
if not isinstance(data, dict):
return []
# 尝试多个可能的键名
products = (
data.get('Top100产品') or
data.get('top100_products') or
data.get('products') or
data.get('productList') or
data.get('product_list') or
[]
)
return products问题: SSE 响应解析失败
症状: API 返回数据解析失败
调试方法:
# 保存原始响应用于调试
import os
debug_file = os.path.join(output_dir, 'raw_response.txt')
with open(debug_file, 'w', encoding='utf-8') as f:
f.write(response)
# 检查响应格式
print("原始响应前500字符:")
print(response[:500])---
4. 编码问题
问题: 中文显示为乱码
症状: 产å 或类似字符
解决方案: 使用 api_client.py 中的修复函数
from api_client import fix_mojibake
fixed_text = fix_mojibake(bad_text)问题: Unicode 转义未解码
症状: \u4ea7\u54c1 格式
解决方案:
import codecs
decoded = codecs.decode(escaped_text, 'unicode-escape')---
5. 其他常见问题
问题: 模块导入失败
症状: ModuleNotFoundError: No module named 'xxx'
解决方案:
# 确保脚本目录在 Python 路径中
import sys
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from api_client import SorftimeClient问题: 文件保存失败
症状: FileNotFoundError 或权限错误
解决方案:
# 确保目录存在
os.makedirs(output_dir, exist_ok=True)
# 使用绝对路径
output_path = os.path.abspath(output_dir)---
6. 调试技巧
启用详细日志
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 在代码中添加日志
logger.debug(f"API 请求: {method_name} {arguments}")
logger.info(f"获取到 {len(products)} 个产品")分步测试
# 测试 API 连接
client = SorftimeClient()
result = client._call('category_name_search', {
'amzSite': 'US',
'searchName': 'speaker'
})
print(json.dumps(result, ensure_ascii=False, indent=2))使用 curl 直接测试
# 测试类目搜索
curl -s -X POST "https://mcp.sorftime.com?key=YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "category_name_search",
"arguments": {
"amzSite": "US",
"searchName": "speaker"
}
}
}'---
7. 获取帮助
1. 检查 SKILL.md 中的执行流程说明 2. 查看 api_client.py 中的方法文档 3. 参考 category-selection skill 的类似实现 4. 在项目根目录运行测试命令验证环境
---
最后更新: 2026-03-19
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sorftime API 客户端 - 统一的数据采集接口
v2.2 - 修复大文件 JSON 解析问题
为 product-research Skill 提供简洁的 API 调用方法:
- 自动从 .mcp.json 读取 API Key
- SSE 响应解析
- Mojibake 编码修复
- 控制字符转义(在 Unicode 解码后执行)
- 返回干净的 Python dict
使用示例:
from scripts.api_client import SorftimeClient
client = SorftimeClient()
# 获取类目 Top100
top100 = client.get_category_report(site="US", node_id=12345)
# 获取关键词详情
keyword = client.get_keyword_detail(site="US", keyword="your keyword")
# 获取产品详情
product = client.get_product_detail(site="US", asin="B0XXXXXXXX")
# 获取产品评论
reviews = client.get_product_reviews(site="US", asin="B0XXXXXXXX", review_type="Negative")
"""
import os
import json
import re
import codecs
import subprocess
import sys
from datetime import datetime
from typing import Optional, Dict, List, Any
from pathlib import Path
# ============================================================================
# API 配置
# ============================================================================
def get_project_root():
"""获取项目根目录(.claude 的父目录)"""
path = os.path.abspath(__file__)
while path != os.path.dirname(path):
if os.path.basename(path) == '.claude':
return os.path.dirname(path)
path = os.path.dirname(path)
return os.getcwd()
def get_api_key():
"""
从 .mcp.json 读取 Sorftime API Key
Returns:
str: API Key
"""
project_root = get_project_root()
mcp_config_path = os.path.join(project_root, '.mcp.json')
if os.path.exists(mcp_config_path):
try:
with open(mcp_config_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
config = json.loads(content)
# 从 URL 中提取 API key: https://mcp.sorftime.com?key=XXX
sorftime_url = config.get('mcpServers', {}).get('sorftime', {}).get('url', '')
if 'key=' in sorftime_url:
api_key = sorftime_url.split('key=')[-1]
if api_key:
return api_key
except Exception as e:
print(f"⚠ 读取 .mcp.json 失败: {e}")
# 尝试环境变量
api_key = os.environ.get('SORFTIME_API_KEY', '')
if api_key:
return api_key
raise ValueError(
"API Key 未找到。请确保:\n"
"1. .mcp.json 文件存在并包含 sorftime 配置,或\n"
"2. 设置环境变量 SORFTIME_API_KEY"
)
# ============================================================================
# 数据处理工具函数
# ============================================================================
def safe_int(value, default=0):
"""安全转换为整数"""
if isinstance(value, (int, float)):
return int(value)
if isinstance(value, str):
cleaned = re.sub(r'[^\d.-]', '', value)
try:
return int(float(cleaned)) if cleaned else default
except ValueError:
return default
return default
def safe_float(value, default=0.0):
"""安全转换为浮点数"""
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
cleaned = re.sub(r'[^\d.-]', '', value)
try:
return float(cleaned) if cleaned else default
except ValueError:
return default
return default
def fix_mojibake(text):
"""
修复 Mojibake 编码问题 (UTF-8/Latin-1 双重编码)
问题: UTF-8 字节被错误解释为 Latin-1
解决: 将错误编码的字符串重新编码为 Latin-1,然后用 UTF-8 解码
"""
if isinstance(text, str):
try:
return text.encode('latin-1').decode('utf-8')
except:
return text
elif isinstance(text, dict):
return {fix_mojibake(k): fix_mojibake(v) for k, v in text.items()}
elif isinstance(text, list):
return [fix_mojibake(item) for item in text]
return text
def escape_control_chars_in_json_strings(json_str):
"""
转义 JSON 字符串值中的控制字符
问题: API 返回的 JSON 字符串值中包含原始的换行符、制表符等控制字符
解决: 在保持 JSON 结构不变的情况下,只转义字符串值内的控制字符
"""
result = []
i = 0
in_string = False
escape_next = False
while i < len(json_str):
c = json_str[i]
if escape_next:
result.append(c)
escape_next = False
i += 1
continue
if c == '\\':
result.append(c)
escape_next = True
i += 1
continue
if c == '"':
in_string = not in_string
result.append(c)
i += 1
continue
if in_string:
if c == '\n':
result.append('\\n')
elif c == '\r':
result.append('\\r')
elif c == '\t':
result.append('\\t')
elif ord(c) < 32:
result.append(' ')
else:
result.append(c)
else:
result.append(c)
i += 1
return ''.join(result)
def extract_json_object(text):
"""
从文本中提取完整的 JSON 对象
使用括号匹配算法,支持嵌套结构
"""
stack = []
start_idx = None
for i, char in enumerate(text):
if char in '{[':
if not stack:
start_idx = i
stack.append(char)
elif char in '}]':
if stack:
expected = '}' if char == '}' else ']'
opening = '{' if expected == '}' else '['
if stack[-1] == opening:
stack.pop()
if not stack:
json_str = text[start_idx:i+1]
try:
return json.loads(json_str)
except json.JSONDecodeError:
continue
return None
def decode_sse_response(content):
"""
解码 Sorftime SSE 响应
处理流程:
1. 清理控制字符
2. 解析 SSE 格式 (event: message, data: {...})
3. Unicode 解码
4. Mojibake 修复
5. 提取 JSON 对象
Args:
content: SSE 响应内容(字符串)
Returns:
dict: 解码后的数据
"""
# 清理控制字符
content = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\x9f]', '', content)
for line in content.split('\n'):
if line.startswith('data: '):
json_text = line[6:] # 去掉 'data: ' 前缀
try:
data = json.loads(json_text)
result_text = data.get('result', {}).get('content', [{}])[0].get('text', '')
if result_text:
# Unicode 解码
decoded = codecs.decode(result_text, 'unicode-escape')
# Mojibake 修复
decoded = fix_mojibake(decoded)
# 转义 JSON 字符串值内的控制字符(关键步骤!)
decoded = escape_control_chars_in_json_strings(decoded)
# 清理剩余的控制字符
decoded = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\x9f]', '', decoded)
# 提取 JSON
json_obj = extract_json_object(decoded)
if json_obj:
return json_obj
except Exception:
continue
# 如果 SSE 解析失败,尝试直接解析
try:
return json.loads(content)
except:
pass
return None
# ============================================================================
# Sorftime API 客户端
# ============================================================================
class SorftimeClient:
"""
Sorftime API 客户端
提供简洁的方法调用 Sorftime MCP API
"""
# API 工具名称映射
TOOLS = {
# 类目相关
'search_categories_broadly': 'search_categories_broadly', # 多维度广泛搜索类目
'category_name_search': 'category_name_search', # 按类目名称搜索(使用 searchName 参数)
'category_report': 'category_report',
'category_trend': 'category_trend',
'category_keywords': 'category_keywords',
# 关键词相关
'keyword_detail': 'keyword_detail',
'keyword_search_results': 'keyword_search_results',
'keyword_extends': 'keyword_extends',
'keyword_trend': 'keyword_trend',
# 产品相关
'product_detail': 'product_detail',
'product_reviews': 'product_reviews',
'product_traffic_terms': 'product_traffic_terms',
'product_trend': 'product_trend',
'product_search': 'product_search',
# 选品相关
'potential_product': 'potential_product',
'competitor_product_keywords': 'competitor_product_keywords',
# 供应链
'ali1688': 'ali1688_similar_product',
}
def __init__(self, api_key: Optional[str] = None):
"""
初始化客户端
Args:
api_key: Sorftime API Key,如果不提供则从 .mcp.json 读取
"""
self.api_key = api_key or get_api_key()
self.api_url = f'https://mcp.sorftime.com?key={self.api_key}'
self.request_id = 0
def _call(self, tool_name: str, arguments: Dict[str, Any]) -> tuple:
"""
调用 Sorftime API
Args:
tool_name: API 工具名称
arguments: API 参数
Returns:
tuple: (解析后的数据 dict, 原始响应 str)
"""
self.request_id += 1
payload = {
"jsonrpc": "2.0",
"id": self.request_id,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments
}
}
try:
result = subprocess.run(
['curl', '-s', '-X', 'POST', self.api_url,
'-H', 'Content-Type: application/json',
'-H', 'Accept: application/json, text/event-stream',
'-d', json.dumps(payload)],
capture_output=True,
text=True,
timeout=60,
check=True
)
# 返回原始响应和解析后的数据
raw_response = result.stdout
data = decode_sse_response(raw_response)
if data is None:
# 即使解析失败,也返回原始响应供调试
return None, raw_response
return data, raw_response
except subprocess.CalledProcessError as e:
raise RuntimeError(f"API 调用失败: {e}")
except subprocess.TimeoutExpired:
raise RuntimeError(f"API 调用超时")
# ========================================================================
# 类目相关 API
# ========================================================================
def search_category_by_product_name(
self,
site: str,
product_name: str
) -> Dict[str, Any]:
"""
按产品名称搜索类目
Args:
site: 站点 (US, GB, DE, FR, IT, ES, CA, JP, etc.)
product_name: 产品名称
Returns:
dict: 搜索结果,包含类目列表
"""
return self._call(
self.TOOLS['category_name_search'],
{"amzSite": site, "searchName": product_name} # 注意: 参数是 searchName
)
def search_category_by_name(
self,
site: str,
category_name: str
) -> Dict[str, Any]:
"""
按类目名称搜索(别名方法,与 search_category_by_product_name 相同)
Args:
site: 站点
category_name: 类目名称
Returns:
dict: 搜索结果
"""
return self.search_category_by_product_name(site, category_name)
def search_categories_broadly(
self,
site: str,
filters: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
多维度广泛搜索类目(新增 - 用于蓝海发现)
Args:
site: 站点 (US, GB, DE, FR, IT, ES, CA, JP, etc.)
filters: 筛选条件(可选)
- top3Product_sales_share: Top3 产品销量占比上限(如 0.4 表示<40%)
- top3Brands_sales_share: Top3 品牌销量占比上限
- newProductSalesAmountShare: 新品销量占比下限(如 0.15 表示>15%)
- brandCount: 品牌数量下限(如 80 表示>80 个品牌)
- priceRange_min: 价格范围下限
- priceRange_max: 价格范围上限
- monthlySales_min: 月销量下限
- monthlySales_max: 月销量上限
Returns:
dict: 类目列表,包含:
- categories: 类目列表
- total: 总数
"""
params = {"amzSite": site}
if filters:
params.update(filters)
return self._call(
self.TOOLS['search_categories_broadly'],
params
)
def get_category_report(
self,
site: str,
node_id: int
) -> Dict[str, Any]:
"""
获取类目 Top100 报告
Args:
site: 站点
node_id: 类目 Node ID
Returns:
dict: Top100 产品数据
"""
return self._call(
self.TOOLS['category_report'],
{"amzSite": site, "nodeId": str(node_id)}
)
def get_category_trend(
self,
site: str,
node_id: int,
trend_index: str = "NewProductSalesAmountShare"
) -> Dict[str, Any]:
"""
获取类目趋势数据
Args:
site: 站点
node_id: 类目 Node ID
trend_index: 趋势类型
- NewProductSalesAmountShare: 新品销量占比
- NewProductProductShare: 新品数量占比
- etc.
Returns:
dict: 结构化趋势数据
{
"trend_data": [
{"date": "2024-03", "value": 33.35},
...
],
"metric": "新品占比",
"node_id": "99530371011"
}
"""
raw_data, raw_response = self._call(
self.TOOLS['category_trend'],
{"amzSite": site, "nodeId": str(node_id), "trendIndex": trend_index}
)
# 转换原始格式为结构化格式
# 原始格式: ["2024年03月=33.35", "2024年04月=27.94", ...]
# 目标格式: {"trend_data": [{"date": "2024-03", "value": 33.35}, ...]}
if isinstance(raw_data, list):
trend_data = []
for item in raw_data:
if isinstance(item, str) and '=' in item:
# 解析 "2024年03月=33.35" 格式
date_str, value_str = item.split('=', 1)
# 转换日期格式: "2024年03月" -> "2024-03"
date_match = re.search(r'(\d{4})年(\d{2})月', date_str)
if date_match:
year, month = date_match.groups()
formatted_date = f"{year}-{month}"
try:
value = float(value_str)
trend_data.append({
"date": formatted_date,
"value": value
})
except ValueError:
continue
# 指标名称映射
metric_names = {
"NewProductSalesAmountShare": "新品销量占比",
"NewProductProductShare": "新品数量占比",
}
return {
"trend_data": trend_data,
"metric": metric_names.get(trend_index, trend_index),
"node_id": str(node_id),
"site": site
}
return raw_data
def get_category_keywords(
self,
site: str,
node_id: int,
page: int = 1
) -> Dict[str, Any]:
"""
获取类目关键词
Args:
site: 站点
node_id: 类目 Node ID
page: 页码
Returns:
dict: 关键词数据
"""
return self._call(
self.TOOLS['category_keywords'],
{"amzSite": site, "nodeId": str(node_id), "page": page}
)
# ========================================================================
# 关键词相关 API
# ========================================================================
def get_keyword_detail(
self,
site: str,
keyword: str
) -> Dict[str, Any]:
"""
获取关键词详情
Args:
site: 站点
keyword: 关键词
Returns:
dict: 关键词详情(搜索量、CPC、自然位产品等)
"""
return self._call(
self.TOOLS['keyword_detail'],
{"amzSite": site, "keyword": keyword}
)
def get_keyword_search_results(
self,
site: str,
keyword: str
) -> Dict[str, Any]:
"""
获取关键词搜索结果(自然位产品)
Args:
site: 站点
keyword: 关键词
Returns:
dict: 自然位产品列表
"""
return self._call(
self.TOOLS['keyword_search_results'],
{"amzSite": site, "searchKeyword": keyword}
)
def get_keyword_extends(
self,
site: str,
keyword: str
) -> Dict[str, Any]:
"""
获取关键词延伸词
Args:
site: 站点
keyword: 关键词
Returns:
dict: 延伸词列表
"""
return self._call(
self.TOOLS['keyword_extends'],
{"amzSite": site, "keyword": keyword}
)
# ========================================================================
# 产品相关 API
# ========================================================================
def get_product_detail(
self,
site: str,
asin: str
) -> Dict[str, Any]:
"""
获取产品详情
Args:
site: 站点
asin: 产品 ASIN
Returns:
dict: 产品详情
"""
return self._call(
self.TOOLS['product_detail'],
{"amzSite": site, "asin": asin}
)
def get_product_reviews(
self,
site: str,
asin: str,
review_type: str = "Both"
) -> Dict[str, Any]:
"""
获取产品评论
Args:
site: 站点
asin: 产品 ASIN
review_type: 评论类型 (Both, Positive, Negative)
Returns:
dict: 评论列表
"""
return self._call(
self.TOOLS['product_reviews'],
{"amzSite": site, "asin": asin, "reviewType": review_type}
)
def get_product_traffic_terms(
self,
site: str,
asin: str
) -> Dict[str, Any]:
"""
获取产品流量关键词(反查)
Args:
site: 站点
asin: 产品 ASIN
Returns:
dict: 流量关键词列表
"""
return self._call(
self.TOOLS['product_traffic_terms'],
{"amzSite": site, "asin": asin}
)
def get_product_trend(
self,
site: str,
asin: str
) -> Dict[str, Any]:
"""
获取产品趋势
Args:
site: 站点
asin: 产品 ASIN
Returns:
dict: 趋势数据
"""
return self._call(
self.TOOLS['product_trend'],
{"amzSite": site, "asin": asin}
)
def search_products(
self,
site: str,
search_name: str,
**filters
) -> Dict[str, Any]:
"""
搜索产品
Args:
site: 站点
search_name: 搜索关键词
**filters: 筛选条件
Returns:
dict: 搜索结果
"""
params = {"amzSite": site, "searchName": search_name}
params.update(filters)
return self._call(self.TOOLS['product_search'], params)
# ========================================================================
# 选品相关 API
# ========================================================================
def get_potential_products(
self,
site: str,
search_name: str,
**filters
) -> Dict[str, Any]:
"""
获取潜力产品
Args:
site: 站点
search_name: 搜索关键词
**filters: 筛选条件
Returns:
dict: 潜力产品列表
"""
params = {"amzSite": site, "searchName": search_name}
params.update(filters)
return self._call(self.TOOLS['potential_product'], params)
def get_competitor_keywords(
self,
site: str,
asin: str
) -> Dict[str, Any]:
"""
获取竞品关键词布局
Args:
site: 站点
asin: 产品 ASIN
Returns:
dict: 竞品关键词布局
"""
return self._call(
self.TOOLS['competitor_product_keywords'],
{"amzSite": site, "asin": asin}
)
# ========================================================================
# 供应链 API
# ========================================================================
def get_1688_products(
self,
search_name: str
) -> Dict[str, Any]:
"""
获取 1688 相似产品
Args:
search_name: 搜索关键词
Returns:
dict: 1688 产品列表
"""
return self._call(
self.TOOLS['ali1688'],
{"searchName": search_name}
)
# ============================================================================
# 便捷函数
# ============================================================================
def create_client() -> SorftimeClient:
"""创建 Sorftime 客户端(便捷函数)"""
return SorftimeClient()
# ============================================================================
# 命令行接口
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Sorftime API 客户端")
parser.add_argument("tool", choices=[
"category_report", "keyword_detail", "product_detail",
"product_reviews", "category_trend"
], help="API 工具名称")
parser.add_argument("--site", default="US", help="站点")
parser.add_argument("--node-id", type=int, help="类目 Node ID")
parser.add_argument("--keyword", help="关键词")
parser.add_argument("--asin", help="产品 ASIN")
parser.add_argument("--output", "-o", help="输出文件路径")
args = parser.parse_args()
client = SorftimeClient()
if args.tool == "category_report":
if not args.node_id:
parser.error("--node-id 是必需的")
result = client.get_category_report(args.site, args.node_id)
elif args.tool == "keyword_detail":
if not args.keyword:
parser.error("--keyword 是必需的")
result = client.get_keyword_detail(args.site, args.keyword)
elif args.tool == "product_detail":
if not args.asin:
parser.error("--asin 是必需的")
result = client.get_product_detail(args.site, args.asin)
elif args.tool == "product_reviews":
if not args.asin:
parser.error("--asin 是必需的")
result = client.get_product_reviews(args.site, args.asin)
elif args.tool == "category_trend":
if not args.node_id:
parser.error("--node-id 是必需的")
result = client.get_category_trend(args.site, args.node_id)
# 输出结果
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"✓ 结果已保存到: {args.output}")
else:
print(json.dumps(result, ensure_ascii=False, indent=2))
#!/usr/bin/env python3
"""
数据采集脚本 - product-research 技能
优化版本 v3.1 - 完全通用化(移除硬编码类别词)
使用方法:
python collect_data.py "your keyword" US
或直接导入:
from collect_data import collect_data
result = collect_data("your keyword", "US")
"""
import sys
import os
import json
import re
from datetime import datetime
# 添加脚本目录到路径
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from api_client import SorftimeClient
def create_output_dir(keyword, site):
"""创建输出目录(使用项目根目录)"""
date_str = datetime.now().strftime('%Y%m%d')
safe_keyword = keyword.replace(' ', '_').replace('/', '_')
# 获取项目根目录
# 脚本路径:.claude/skills/product-research/scripts/collect_data.py
# 需要向上四级:scripts → product-research → skills → .claude → amazon-mcp
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_dir))))
output_dir = os.path.join(project_root, 'product-research-reports', f'{safe_keyword}_{site}_{date_str}')
raw_dir = os.path.join(output_dir, 'raw')
os.makedirs(raw_dir, exist_ok=True)
return output_dir, raw_dir, date_str
def save_json(data, filepath):
"""安全保存 JSON 文件"""
try:
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f" ✗ 保存失败:{e}")
return False
def discover_blue_ocean_categories(client, site, keyword, max_categories=5):
"""
【新增】蓝海市场发现 - 使用 search_categories_broadly
Args:
client: SorftimeClient 实例
site: 站点
keyword: 产品关键词(用于筛选相关类目)
max_categories: 返回的类目数量
Returns:
list: 符合条件的类目列表
"""
print("\n[Step 0.5] 蓝海市场发现...")
# 筛选条件:适合新卖家的蓝海市场
filters = {
# 低集中度
"top3Product_sales_share": 0.4, # Top3 产品销量占比 < 40%
"top3Brands_sales_share": 0.5, # Top3 品牌销量占比 < 50%
# 新品活跃
"newProductSalesAmountShare": 0.15, # 新品销量占比 > 15%
# 市场分散
"brandCount": 50, # 品牌数量 > 50
# 价格适中
"priceRange_min": 10,
"priceRange_max": 50,
# 有一定规模
"monthlySales_min": 5000,
}
try:
result, _ = client.search_categories_broadly(site, filters)
if result and isinstance(result, dict):
categories = result.get('categories', [])
# 过滤与关键词相关的类目
if keyword:
keyword_lower = keyword.lower()
related_categories = []
for cat in categories:
cat_name = cat.get('categoryName', '').lower()
if keyword_lower in cat_name or keyword_lower in cat.get('description', '').lower():
related_categories.append(cat)
categories = related_categories[:max_categories]
else:
categories = categories[:max_categories]
print(f" ✓ 发现 {len(categories)} 个潜力类目:")
for i, cat in enumerate(categories, 1):
print(f" {i}. {cat.get('categoryName', 'N/A')} "
f"(新品占比:{cat.get('newProductSalesAmountShare', 0)*100:.1f}%, "
f"Top3 占比:{cat.get('top3Product_sales_share', 0)*100:.1f}%)")
return categories
else:
print(f" ⚠ 未找到符合条件的类目")
return []
except Exception as e:
print(f" ⚠ 蓝海发现失败:{e} (非关键,继续执行)")
return []
def find_potential_products(client, site, keyword, max_products=20):
"""
【新增】潜力产品发现 - 使用 potential_product
Args:
client: SorftimeClient 实例
site: 站点
keyword: 产品关键词
max_products: 返回的产品数量
Returns:
list: 潜力产品列表
"""
print(f"\n[Step 1.3] 潜力产品发现:{keyword}...")
# 筛选条件:有潜力的新品
filters = {
"monthlySales_min": 500, # 月销量 > 500
"price_min": 10, # 价格 > $10
"price_max": 50, # 价格 < $50
"rating_min": 4.0, # 评分 > 4.0
"daysOnMarket_max": 180, # 上架时间 < 6 个月
}
try:
result, _ = client.get_potential_products(site, keyword, **filters)
if result and isinstance(result, dict):
products = result.get('products', []) or result.get('productList', [])
if not products:
# 尝试不同的返回格式
products = result.get('list', [])
print(f" ✓ 发现 {len(products)} 个潜力产品")
# 显示 Top 5
for i, p in enumerate(products[:5], 1):
asin = p.get('ASIN', 'N/A')
brand = p.get('品牌', 'N/A')
sales = p.get('月销量', 'N/A')
price = p.get('价格', 'N/A')
rating = p.get('星级', 'N/A')
days = p.get('上线天数', 'N/A')
print(f" {i}. {asin} | {brand} | 月销{sales} | ${price} | {rating}星 | {days}天")
return products[:max_products]
else:
print(f" ⚠ 未找到潜力产品")
return []
except Exception as e:
print(f" ⚠ 潜力产品发现失败:{e} (非关键,继续执行)")
return []
def get_keyword_extends_data(client, site, keyword):
"""
【新增】获取关键词延伸词 - 用于维度发现
Args:
client: SorftimeClient 实例
site: 站点
keyword: 关键词
Returns:
dict: 延伸词数据
"""
print(f"\n[Step 1.4] 获取关键词延伸词:{keyword}...")
try:
result, _ = client.get_keyword_extends(site, keyword)
if result:
# 解析延伸词
extends = result.get('extends', []) or result.get('keywords', []) or result.get('list', [])
if isinstance(extends, list) and len(extends) > 0:
print(f" ✓ 获取 {len(extends)} 个延伸词")
# 提取高频修饰词(用于维度发现)
modifiers = []
for item in extends:
if isinstance(item, dict):
word = item.get('keyword', item.get('word', ''))
search_volume = item.get('searchVolume', item.get('monthly_search', 0))
else:
word = str(item)
search_volume = 0
# 过滤掉品类通用词
if word and keyword.lower() not in word.lower():
modifiers.append({
'word': word,
'search_volume': search_volume
})
# 按搜索量排序
modifiers.sort(key=lambda x: x['search_volume'], reverse=True)
print(f" ✓ 提取 {len(modifiers)} 个修饰词(用于维度发现)")
if modifiers:
print(f" Top 5 修饰词:{', '.join([m['word'] for m in modifiers[:5]])}")
return {
'extends': extends,
'modifiers': modifiers[:20] # 保留 Top 20
}
print(f" ⚠ 延伸词数据为空")
return {}
except Exception as e:
print(f" ⚠ 延伸词获取失败:{e} (非关键,继续执行)")
return {}
def collect_data(keyword, site='US', max_keywords=3, use_blue_ocean=False):
"""
执行完整的数据采集流程
Args:
keyword: 产品/类目关键词
site: 站点代码 (US, GB, DE, etc.)
max_keywords: 采集关键词数量
use_blue_ocean: 是否启用蓝海发现模式
Returns:
dict: 采集结果摘要
"""
print(f"🔍 选品数据采集:{keyword} ({site})")
print("=" * 60)
# 初始化
client = SorftimeClient()
output_dir, raw_dir, date_str = create_output_dir(keyword, site)
# 结果摘要
result = {
'keyword': keyword,
'site': site,
'date': date_str,
'category_name': None,
'node_id': None,
'steps_completed': [],
'errors': [],
'blue_ocean_categories': [],
'potential_products': [],
'keyword_extends': {}
}
# ========== Step 0.5: 蓝海市场发现(可选) ==========
if use_blue_ocean:
blue_ocean_cats = discover_blue_ocean_categories(client, site, keyword)
if blue_ocean_cats:
result['blue_ocean_categories'] = blue_ocean_cats
save_json(blue_ocean_cats, os.path.join(raw_dir, 'blue_ocean_categories.json'))
result['steps_completed'].append('blue_ocean_discovery')
# ========== Step 1: 搜索类目 ==========
print("\n[Step 1] 搜索类目...")
category_result = None
used_keyword = keyword
try:
print(f" 搜索: '{keyword}'...", end=' ')
category_result, raw = client.search_category_by_product_name(site, keyword)
if category_result and isinstance(category_result, list) and len(category_result) > 0:
print(f"✓ 找到 {len(category_result)} 个类目")
else:
error_msg = f"类目搜索失败:未找到与 '{keyword}' 匹配的类目。请使用该类别最通用的核心名词(如使用 'camera' 而非 'digital wireless camera')"
print(f" ✗ {error_msg}")
raise Exception(error_msg)
except Exception as e:
print(f" ✗ 错误: {str(e)}")
raise
# 使用找到的类目
first_cat = category_result[0]
node_id = first_cat.get('nodeId') or first_cat.get('NodeId')
category_name = first_cat.get('categoryName') or first_cat.get('Name')
result['category_name'] = category_name
result['node_id'] = str(node_id)
result['searched_keyword'] = used_keyword # 记录实际使用的搜索词
print(f" ✓ 最终类目:{category_name}")
print(f" ✓ Node ID: {node_id}")
if used_keyword != keyword:
print(f" ℹ 使用搜索词: '{used_keyword}' (原词: '{keyword}')")
save_json(category_result, os.path.join(raw_dir, 'category_info.json'))
result['steps_completed'].append('category_search')
# ========== Step 2: 获取 Top100 ==========
print(f"\n[Step 2] 获取 Top100 产品数据...")
top100 = None
try:
top100, raw_response = client.get_category_report(site, result['node_id'])
# 检查返回的数据是否有效
if top100 is None or not isinstance(top100, dict) or len(top100) == 0:
raise ValueError("category_report 返回无效数据")
products = top100.get('Top100产品', []) or top100.get('Top100 产品', []) or top100.get('products', [])
stats = top100.get('类目统计报告', {})
print(f" ✓ 产品数量:{len(products)}")
if stats:
monthly_revenue = stats.get('top100 产品月销额', 0)
print(f" ✓ 类目月销额:${monthly_revenue}")
save_json(top100, os.path.join(raw_dir, 'top100.json'))
result['steps_completed'].append('top100')
except Exception as e:
# category_report 不可用时,使用 product_search 作为替代
print(f" ⚠ category_report 不可用,尝试使用 product_search 替代...")
try:
# 使用 product_search 工具获取产品数据
search_result, _ = client._call('product_search', {
'amzSite': site,
'searchName': keyword,
'page': 1
})
if isinstance(search_result, list) and len(search_result) > 0:
# 构造类似 top100 的数据结构
products = search_result
# 计算类目统计数据
total_monthly_sales = sum(p.get('月销量', 0) for p in products)
total_monthly_revenue = sum(p.get('月销额', 0) for p in products)
avg_price = total_monthly_revenue / len(products) if products else 0
top100_data = {
'Top100产品': products,
'类目统计报告': {
'top100 产品月销额': total_monthly_revenue,
'top100 产品月销量': total_monthly_sales,
'平均价格': avg_price,
'产品数量': len(products),
'数据来源': 'product_search (替代 category_report)'
}
}
print(f" ✓ 产品数量:{len(products)}")
print(f" ✓ 类目月销额:${total_monthly_revenue:,.2f}")
print(f" ℹ 注意:使用 product_search 数据(非完整 Top100)")
save_json(top100_data, os.path.join(raw_dir, 'top100.json'))
result['steps_completed'].append('top100')
else:
error_msg = f"product_search 返回空数据"
print(f" ✗ {error_msg}")
result['errors'].append(error_msg)
except Exception as e2:
error_msg = f"Top100 获取失败(category_report 和 product_search 都失败):{e}, {e2}"
print(f" ✗ {error_msg}")
result['errors'].append(error_msg)
# ========== Step 3: 获取趋势数据 ==========
print(f"\n[Step 3] 获取类目趋势...")
try:
trend = client.get_category_trend(site, result['node_id'])
if trend:
print(f" ✓ 趋势数据已获取")
save_json(trend, os.path.join(raw_dir, 'trend.json'))
result['steps_completed'].append('trend')
else:
print(f" ⚠ 趋势数据为空(非关键)")
except Exception as e:
error_msg = f"趋势获取失败:{e}"
print(f" ⚠ {error_msg} (非关键)")
result['errors'].append(error_msg)
# ========== Step 4: 获取关键词详情(通用关键词生成) ==========
print(f"\n[Step 4] 获取关键词详情...")
keywords_data = {}
def generate_keyword_variants(base_kw, max_count=5):
"""
通用关键词变体生成策略
策略:
1. 原始词
2. 尝试生成复数形式
3. 添加常见修饰前缀
"""
variants = [base_kw]
# 复数形式生成(通用规则)
# 规则1: 添加 's'
if not base_kw.endswith('s'):
variants.append(base_kw + 's')
# 规则2: 以 y 结尾,变 'ies'
if base_kw.endswith('y') and len(base_kw) > 1:
variants.append(base_kw[:-1] + 'ies')
# 规则3: 以 s, x, ch, sh 结尾,添加 'es'
if base_kw.endswith(('s', 'x', 'ch', 'sh')):
variants.append(base_kw + 'es')
# 添加常见修饰前缀(完全通用)
common_prefixes = ['portable', 'wireless', 'digital', 'smart']
for prefix in common_prefixes:
variants.append(f"{prefix} {base_kw}")
# 去重并限制数量
seen = set()
unique_variants = []
for v in variants:
v_lower = v.lower().strip()
if v_lower and v_lower not in seen and len(unique_variants) < max_count:
seen.add(v_lower)
unique_variants.append(v)
return unique_variants
base_keywords = generate_keyword_variants(keyword, max_keywords)
for kw in base_keywords:
try:
print(f" - {kw}...", end=' ', flush=True)
kw_data, _ = client.get_keyword_detail(site, kw)
if kw_data:
keywords_data[kw] = kw_data
print("✓")
else:
print("✗ (空响应)")
except Exception as e:
print(f"✗ ({str(e)[:50]})")
if keywords_data:
save_json(keywords_data, os.path.join(raw_dir, 'keywords.json'))
result['steps_completed'].append('keywords')
print(f" ✓ 成功:{len(keywords_data)}/{len(base_keywords)} 个关键词")
# ========== Step 5: 获取关键词延伸词(新增) ==========
extends_data = get_keyword_extends_data(client, site, keyword)
if extends_data:
result['keyword_extends'] = extends_data
save_json(extends_data, os.path.join(raw_dir, 'keyword_extends.json'))
result['steps_completed'].append('keyword_extends')
# ========== Step 6: 发现潜力产品(新增) ==========
potential_products = find_potential_products(client, site, keyword)
if potential_products:
result['potential_products'] = potential_products
save_json(potential_products, os.path.join(raw_dir, 'potential_products.json'))
result['steps_completed'].append('potential_products')
# ========== Step 7: 保存汇总数据 ==========
print(f"\n[Step 7] 保存汇总数据...")
summary = {
"metadata": {
"keyword": keyword,
"site": site,
"date": date_str,
"node_id": result['node_id'],
"category_name": result['category_name'],
"collected_at": datetime.now().isoformat()
},
"files": {
"category_info": "raw/category_info.json",
"top100": "raw/top100.json",
"trend": "raw/trend.json",
"keywords": "raw/keywords.json",
"keyword_extends": "raw/keyword_extends.json" if extends_data else None,
"potential_products": "raw/potential_products.json" if potential_products else None,
"blue_ocean_categories": "raw/blue_ocean_categories.json" if result['blue_ocean_categories'] else None
},
"status": "success" if len(result['errors']) == 0 else "partial",
"steps_completed": result['steps_completed'],
"errors": result['errors'],
# 预留 Dashboard 需要的数据结构(初始为空,由后续分析填充)
"market_overview": {},
"price_ranges": [],
"product_types": [],
"cross_analysis": {"price_type_matrix": []},
"top_brands": [],
"competitors": [],
"voc_analysis": {"dimensions": [], "summary": ""},
"barriers": [],
"decision": {},
"trend_data": [],
"keywords": {}
}
save_json(summary, os.path.join(output_dir, 'data.json'))
# ========== 完成 ==========
print("\n" + "=" * 60)
print(f"✓ 数据采集完成!")
print(f" 输出目录:{output_dir}")
print(f" 完成步骤:{', '.join(result['steps_completed'])}")
if result['errors']:
print(f"\n⚠ 错误 ({len(result['errors'])}):")
for err in result['errors']:
print(f" - {err}")
print("=" * 60)
return result
# ============================================================================
# 命令行接口
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="product-research 数据采集脚本(通用版本)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python collect_data.py "speaker" US
python collect_data.py "sofa" DE --keywords 5
python collect_data.py "mat" US --blue-ocean # 启用蓝海发现
"""
)
parser.add_argument('keyword', help='产品/类目关键词')
parser.add_argument('site', nargs='?', default='US', help='站点代码 (默认:US)')
parser.add_argument('--keywords', '-k', type=int, default=3, help='采集关键词数量 (默认:3)')
parser.add_argument('--blue-ocean', action='store_true', help='启用蓝海发现模式')
args = parser.parse_args()
collect_data(args.keyword, args.site, args.keywords, use_blue_ocean=args.blue_ocean)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据验证和修复脚本 - 确保 data.json 结构正确
用法:
python fix_data_json.py path/to/data.json
python fix_data_json.py path/to/data.json --fix
"""
import sys
import os
import json
import argparse
from datetime import datetime
from pathlib import Path
def validate_data(data: dict) -> tuple[bool, list[str]]:
"""验证数据结构"""
errors = []
warnings = []
# 必需字段检查
required_fields = ['metadata', 'market_overview']
for field in required_fields:
if field not in data:
errors.append(f"缺少必需字段: {field}")
# metadata 检查
if 'metadata' in data:
metadata = data['metadata']
required_metadata = ['category', 'site', 'date']
for field in required_metadata:
if field not in metadata:
warnings.append(f"metadata 缺少字段: {field}")
# market_overview 检查
if 'market_overview' in data:
mo = data['market_overview']
required_mo = ['top100_monthly_sales', 'top100_monthly_revenue', 'avg_price']
for field in required_mo:
if field not in mo:
warnings.append(f"market_overview 缺少字段: {field}")
# go_nogo 检查
if 'go_nogo' not in data:
errors.append("缺少 go_nogo 字段")
else:
gogono = data['go_nogo']
if 'overall_score' not in gogono and 'total_score' not in gogono:
warnings.append("go_nogo 缺少评分字段")
if 'decision' not in gogono and 'verdict' not in gogono:
warnings.append("go_nogo 缺少决策字段")
# dimensions 检查
if 'dimensions' in data and data['dimensions']:
# 检查每个维度是否有正确的结构
for i, dim in enumerate(data['dimensions']):
if 'dimension' not in dim and 'name' not in dim:
warnings.append(f"dimensions[{i}] 缺少 'dimension' 或 'name' 字段")
# voc_analysis 检查
if 'voc_analysis' in data and data['voc_analysis']:
voc = data['voc_analysis']
if 'dimensions' not in voc:
warnings.append("voc_analysis 缺少 'dimensions' 字段")
is_valid = len(errors) == 0
return is_valid, errors + warnings
def fix_data(data: dict) -> dict:
"""修复常见的数据结构问题"""
# 修复 go_nogo 字段名称
if 'go_nogo' in data:
gogono = data['go_nogo']
if 'verdict' in gogono and 'decision' not in gogono:
gogono['decision'] = gogono['verdict']
if 'total_score' in gogono and 'overall_score' not in gogono:
gogono['overall_score'] = gogono['total_score']
# 确保必需字段存在
if 'market_overview' not in data:
data['market_overview'] = {}
mo = data['market_overview']
if 'top3_product_concentration' not in mo and 'top3_concentration' in mo:
mo['top3_product_concentration'] = mo['top3_concentration']
return data
def main():
parser = argparse.ArgumentParser(description="数据验证和修复脚本")
parser.add_argument("data_file", help="data.json 文件路径")
parser.add_argument("--fix", action="store_true", help="自动修复问题")
parser.add_argument("--output", "-o", help="输出文件路径(默认覆盖原文件)")
args = parser.parse_args()
data_path = Path(args.data_file)
if not data_path.exists():
print(f"✗ 文件不存在: {data_path}")
return 1
# 读取数据
print(f"读取数据: {data_path}")
with open(data_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 验证数据
is_valid, messages = validate_data(data)
print("\n验证结果:")
for msg in messages:
prefix = "✗" if "错误" in msg or "缺少" in msg else "⚠"
print(f" {prefix} {msg}")
if is_valid:
print("\n✓ 数据结构验证通过")
else:
print("\n✗ 数据结构存在问题")
if not args.fix:
print(" 提示: 使用 --fix 参数尝试自动修复")
return 1
# 修复数据
if args.fix:
print("\n修复数据...")
data = fix_data(data)
# 重新验证
is_valid_after, messages_after = validate_data(data)
if is_valid_after:
print("✓ 数据修复成功")
else:
print("⚠ 部分问题无法自动修复")
# 保存
output_path = Path(args.output) if args.output else data_path
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"✓ 已保存: {output_path}")
return 0 if is_valid else 1
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
获取竞品差评数据(通用版本)
使用方法:
python get_reviews.py --output-dir "product-research/xxx_YYYYMMDD"
注意:此脚本从 top100.json 中自动选择代表性竞品
"""
import json
import os
import sys
from datetime import datetime
# 添加脚本目录到路径
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from api_client import SorftimeClient
def get_project_root():
"""获取项目根目录"""
current_dir = os.path.dirname(os.path.abspath(__file__))
# 从 scripts/ 向上四级到达项目根目录
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_dir))))
def main():
import argparse
parser = argparse.ArgumentParser(description="获取竞品差评数据(通用版本)")
parser.add_argument("--output-dir", "-o", required=True, help="输出目录(包含 top100.json 的目录)")
parser.add_argument("--site", default="US", help="站点代码")
parser.add_argument("--max-reviews", type=int, default=6, help="最大竞品数量")
args = parser.parse_args()
# 检查 top100.json 是否存在
top100_path = os.path.join(args.output_dir, 'raw', 'top100.json')
if not os.path.exists(top100_path):
print(f"✗ 错误:找不到 {top100_path}")
print(" 请确保输出目录中存在 raw/top100.json 文件")
return 1
client = SorftimeClient()
# 读取 Top100 数据
with open(top100_path, 'r', encoding='utf-8') as f:
data = json.load(f)
products = data.get('Top100产品', []) or data.get('Top100 产品', [])
if not products:
print("✗ 错误:top100.json 中没有产品数据")
return 1
print(f"📊 从 {len(products)} 个产品中选择代表性竞品...")
# 按销量排序
sorted_products = sorted(products, key=lambda x: float(x.get('月销量', 0)), reverse=True)
# 选择策略:Top3 + 不同价格带代表
competitors = []
# 量级标杆(Top3)
for i, p in enumerate(sorted_products[:3]):
competitors.append((p['ASIN'], f"Top{i+1} - {p.get('品牌', 'Unknown')}"))
# 按价格分组选择
price_groups = {
'low': [p for p in sorted_products if float(p.get('价格', 0)) < 30],
'mid': [p for p in sorted_products if 30 <= float(p.get('价格', 0)) < 60],
'high': [p for p in sorted_products if float(p.get('价格', 0)) >= 60]
}
# 各价位代表
for price_name, price_list in [('低价', price_groups['low']), ('中价', price_groups['mid']), ('高价', price_groups['high'])]:
for p in price_list:
if p['ASIN'] not in [c[0] for c in competitors]:
competitors.append((p['ASIN'], f"{price_name}代表 - {p.get('品牌', 'Unknown')}"))
break
# 去重
seen = set()
competitors = [x for x in competitors if not (x[0] in seen or seen.add(x[0]))]
# 限制数量
competitors = competitors[:args.max_reviews]
print(f" 选择了 {len(competitors)} 个竞品进行差评分析")
all_reviews = {}
for asin, desc in competitors:
print(f" - {asin} ({desc})...", end=' ', flush=True)
try:
reviews, raw = client.get_product_reviews(args.site, asin, 'Negative')
if reviews:
if isinstance(reviews, list):
review_count = len(reviews)
sample = reviews[:20] if len(reviews) > 20 else reviews
else:
review_count = 'data'
sample = reviews
all_reviews[asin] = {
'description': desc,
'review_count': review_count,
'reviews': sample
}
print(f"✓ {review_count}条")
else:
print("✗ 无数据")
except Exception as e:
print(f"✗ {str(e)[:40]}")
# 保存结果
if all_reviews:
reviews_path = os.path.join(args.output_dir, 'raw', 'competitor_reviews.json')
os.makedirs(os.path.dirname(reviews_path), exist_ok=True)
with open(reviews_path, 'w', encoding='utf-8') as f:
json.dump(all_reviews, f, ensure_ascii=False, indent=2)
print(f"\n✓ 差评数据已保存: {reviews_path}")
return 0
else:
print("\n✗ 未获取到任何差评数据")
return 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据验证脚本 - 校验 data.json 的字段命名和数据一致性
使用方式:
python scripts/validate_data.py path/to/data.json
验证项:
1. 字段命名规范(禁止模糊的命名如 top3_concentration)
2. 数据一致性(数值在合理范围内)
3. 必填字段完整性
"""
import json
import sys
from pathlib import Path
from typing import Dict, List, Tuple, Any
class DataValidator:
"""数据验证器"""
# 禁止的模糊字段名
FORBIDDEN_FIELDS = {
'top3_concentration': '请使用 top3_product_concentration 或 top3_brand_concentration',
'top10_concentration': '请使用 top10_product_concentration 或 top10_brand_concentration',
'concentration': '请明确指定是产品还是品牌的集中度',
}
# 必填字段
REQUIRED_FIELDS = {
'metadata': ['category', 'site', 'date'],
'market_overview': [
'top100_monthly_sales',
'top100_monthly_revenue',
'avg_price',
'top3_brand_concentration', # 明确是品牌集中度
],
}
# 数值范围检查
RANGE_CHECKS = {
'top3_product_concentration': (0, 1),
'top3_brand_concentration': (0, 1),
'top10_brand_concentration': (0, 1),
'new_product_share': (0, 1),
'avg_price': (0, 10000),
'top100_monthly_sales': (0, 10000000),
'top100_monthly_revenue': (0, 1000000000),
}
def __init__(self, data_path: str):
"""初始化验证器"""
self.data_path = Path(data_path)
self.errors: List[str] = []
self.warnings: List[str] = []
self.data: Dict = {}
def load_data(self) -> bool:
"""加载数据文件"""
try:
with open(self.data_path, 'r', encoding='utf-8') as f:
self.data = json.load(f)
return True
except FileNotFoundError:
self.errors.append(f"文件不存在: {self.data_path}")
return False
except json.JSONDecodeError as e:
self.errors.append(f"JSON 解析错误: {e}")
return False
def check_field_naming(self) -> bool:
"""检查字段命名规范"""
passed = True
def check_recursive(obj: Any, path: str = ""):
nonlocal passed
if isinstance(obj, dict):
for key in obj.keys():
current_path = f"{path}.{key}" if path else key
# 检查禁止的字段名
if key in self.FORBIDDEN_FIELDS:
self.errors.append(
f"[命名错误] {current_path}: 使用了模糊的字段名 '{key}'。"
f"{self.FORBIDDEN_FIELDS[key]}"
)
passed = False
# 递归检查
check_recursive(obj[key], current_path)
elif isinstance(obj, list):
for i, item in enumerate(obj):
check_recursive(item, f"{path}[{i}]")
check_recursive(self.data)
return passed
def check_required_fields(self) -> bool:
"""检查必填字段"""
passed = True
for section, fields in self.REQUIRED_FIELDS.items():
if section not in self.data:
self.errors.append(f"[缺失] 缺少必要区块: {section}")
passed = False
continue
section_data = self.data[section]
for field in fields:
if field not in section_data:
self.errors.append(f"[缺失] {section}.{field} 是必填字段")
passed = False
return passed
def check_value_ranges(self) -> bool:
"""检查数值范围"""
passed = True
def check_value(obj: Any, path: str = ""):
nonlocal passed
if isinstance(obj, dict):
for key, value in obj.items():
current_path = f"{path}.{key}" if path else key
if key in self.RANGE_CHECKS and isinstance(value, (int, float)):
min_val, max_val = self.RANGE_CHECKS[key]
if not (min_val <= value <= max_val):
self.errors.append(
f"[范围错误] {current_path} = {value},"
f"应在 [{min_val}, {max_val}] 范围内"
)
passed = False
check_value(value, current_path)
elif isinstance(obj, list):
for i, item in enumerate(obj):
check_value(item, f"{path}[{i}]")
check_value(self.data)
return passed
def check_consistency(self) -> bool:
"""检查数据一致性"""
passed = True
market = self.data.get('market_overview', {})
# 检查 Top3 品牌集中度是否合理
top3_brand = market.get('top3_brand_concentration')
top3_product = market.get('top3_product_concentration')
if top3_brand and top3_product:
if top3_brand < top3_product:
self.warnings.append(
f"[一致性警告] top3_brand_concentration ({top3_brand:.2%}) "
f"小于 top3_product_concentration ({top3_product:.2%}),"
f"这通常不合理(品牌集中度应该 >= 产品集中度)"
)
# 检查竞品市场份额之和
competitors = self.data.get('competitors', [])
if competitors:
total_share = 0
for comp in competitors:
share_str = comp.get('market_share', '0%')
try:
share = float(share_str.replace('%', '')) / 100
total_share += share
except (ValueError, AttributeError):
pass
if total_share > 1.0:
self.warnings.append(
f"[一致性警告] 竞品市场份额之和 ({total_share:.1%}) 超过 100%"
)
return passed
def validate(self) -> Tuple[bool, List[str], List[str]]:
"""执行完整验证"""
print(f"🔍 验证数据文件: {self.data_path}")
print("-" * 50)
# 加载数据
if not self.load_data():
return False, self.errors, self.warnings
# 执行各项检查
checks = [
("字段命名规范", self.check_field_naming),
("必填字段", self.check_required_fields),
("数值范围", self.check_value_ranges),
("数据一致性", self.check_consistency),
]
all_passed = True
for check_name, check_func in checks:
passed = check_func()
status = "✓" if passed else "✗"
print(f"{status} {check_name}")
if not passed:
all_passed = False
print("-" * 50)
# 输出警告
if self.warnings:
print("\n⚠️ 警告:")
for warning in self.warnings:
print(f" - {warning}")
# 输出错误
if self.errors:
print("\n❌ 错误:")
for error in self.errors:
print(f" - {error}")
# 总结
if all_passed and not self.warnings:
print("\n✅ 所有验证通过!")
elif all_passed:
print("\n⚠️ 验证通过,但有警告需要关注")
else:
print(f"\n❌ 验证失败,发现 {len(self.errors)} 个错误")
return all_passed, self.errors, self.warnings
def main():
"""命令行入口"""
if len(sys.argv) < 2:
print("用法: python validate_data.py <data.json 路径>")
sys.exit(1)
data_path = sys.argv[1]
validator = DataValidator(data_path)
passed, errors, warnings = validator.validate()
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
验证 Reviews 数据完整性
"""
import json
import os
import sys
def verify_reviews_data(reviews_path):
"""验证 Reviews 数据"""
if not os.path.exists(reviews_path):
print(f"✗ 文件不存在: {reviews_path}")
return False
with open(reviews_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"=== Reviews 数据验证 ===")
print(f"文件路径: {reviews_path}")
print(f"文件大小: {os.path.getsize(reviews_path)/1024:.1f} KB")
print()
print(f"竞品数量: {len(data)}")
total_reviews = 0
for asin, info in data.items():
desc = info.get('description', 'N/A')
count = info.get('review_count', 0)
reviews = info.get('reviews', [])
if isinstance(count, int):
total_reviews += count
review_count = len(reviews) if isinstance(reviews, list) else 0
print(f" - {asin}: {desc} ({count} 条,保存 {review_count} 条)")
print()
print(f"总差评数: {total_reviews}")
print("✓ 数据验证通过")
return True
def find_all_reviews_dirs():
"""查找所有 Reviews 数据目录"""
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 可能的目录位置
search_paths = [
os.path.join(project_root, 'product-research-reports'), # 新的输出目录
# os.path.join(project_root, 'product-research'), # 旧的输出目录(已废弃)
]
print(f"=== 搜索 Reviews 数据目录 ===")
print(f"项目根目录: {project_root}")
print()
found = []
for base_path in search_paths:
if not os.path.exists(base_path):
continue
for root, dirs, files in os.walk(base_path):
if 'competitor_reviews.json' in files:
# 如果在 raw 目录,记录父目录
if os.path.basename(root) == 'raw':
found.append(os.path.dirname(root))
print(f"✓ 找到: {os.path.dirname(root)}")
else:
found.append(root)
print(f"✓ 找到: {root}")
return found
if __name__ == '__main__':
# 查找所有 Reviews 数据
dirs = find_all_reviews_dirs()
if not dirs:
print("\n未找到任何 Reviews 数据")
sys.exit(1)
print(f"\n共找到 {len(dirs)} 个数据目录")
# 验证最新的数据
latest_dir = max(dirs, key=lambda x: os.path.getmtime(x))
reviews_path = os.path.join(latest_dir, 'raw', 'competitor_reviews.json')
print(f"\n验证最新数据: {latest_dir}")
print()
verify_reviews_data(reviews_path)