跳到主内容
快讯直播
AI智模界
教程

用 AI 做竞品分析:抓取整理可视化,5 分钟出报告

这篇能做出什么

跑完这套流程,你会得到一个可以反复执行的竞品分析流水线,一次运行产出四样东西:

1. 一份结构化事实表(CSV + Markdown 表格),横轴是竞品,纵轴是定位、目标用户、核心功能、计费方式、免费额度、宣称差异点。

2. 一张功能覆盖对比图(PNG),把"哪家提到了某个能力"变成可比的矩阵,一眼看出谁缺什么。

3. 一份 800 字以内的分析摘要,包含结论、差异点和机会点,每条结论后面标注依据来自哪家的哪个字段。

4. 可追溯的原始素材:抓下来的公开页面正文和每条结论的原文片段,方便你复核 AI 有没有编。

"5 分钟"指的是第二次以后的运行时间——第一次因为要装环境、挑页面、调字段,大概要半小时到一小时。跑顺之后,改一行 CSV 加个竞品,重跑脚本,几分钟出结果。

需要先说清楚边界:这套流程只处理公开可访问的页面(官网、定价页、帮助中心、更新日志、公开文档)。它不替代真实用户访谈,也不能把 AI 给的判断当客观事实。所有涉及价格、版本、额度的数字,一律以官方页面当前展示为准,报告里要写上采集日期。

前置条件清单

  • 一台能装 Python 的机器(本地或云主机都行,Python 3 的较新版本,具体以官方文档为准)
  • 一个可调用的大模型接口。可以是云厂商的 API,也可以本地跑 Ollama 之类的服务;只要提供 OpenAI 兼容的 HTTP 接口就能接。模型名、计费方式以官方文档为准
  • 3 到 5 个竞品的公开页面清单。少于 3 家看不出差异,多于 5 家第一次做会累
  • 基础的命令行操作能力:会 cd、会跑 pip install、会看报错

关于抓取的合规底线,写在最前面:

  • 只抓公开页面,不碰需要登录、需要付费、有验证码的内容,不绕过任何访问控制
  • 动手前看一眼目标站点的 robots.txt 和服务条款,对方明确禁止采集就换来源(比如公开的更新日志、应用商店页面描述、公开文档)
  • 请求之间加延时,别把人家站点打崩。每秒几十个请求的行为既没礼貌也容易被封

分步骤

第 1 步:建目录、装依赖

```bash

mkdir -p ca/{raw,clean,out}

cd ca

python -m venv .venv

macOS / Linux

source .venv/bin/activate

Windows PowerShell

.venv\Scripts\Activate.ps1

pip install requests beautifulsoup4 pandas tabulate matplotlib python-dotenv openai

```

tabulatepandas.to_markdown() 的依赖,不装会报错。如果你想抓 JS 渲染的页面,再额外装 Playwright:

```bash

pip install playwright

playwright install chromium

```

第 2 步:把竞品清单写成 CSV

新建 targets.csv,一行一个来源。同一家可以有多行(官网首页 + 定价页 + 文档页):

```csv

brand,url,note

产品A,https://example.com/,官网首页

产品A,https://example.com/pricing,定价页

产品B,https://example.org/,官网首页

产品B,https://example.org/docs,文档首页

产品C,https://example.net/,官网首页

```

用 CSV 而不是写死在代码里,是为了后面加竞品时只改数据不动逻辑。

第 3 步:抓取网页正文

新建 fetch.py

```python

fetch.py

import csv, json, time, pathlib, urllib.parse

import requests

from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; ResearchBot/1.0; +contact@example.com)"}

OUT = pathlib.Path("raw")

OUT.mkdir(exist_ok=True)

def clean_html(html: str) -> str:

soup = BeautifulSoup(html, "html.parser")

去掉对正文没帮助、且噪声很大的标签

for tag in soup(["script", "style", "noscript", "svg", "nav", "footer", "form"]):

tag.decompose()

text = soup.get_text("\n", strip=True)

lines = [line.strip() for line in text.splitlines() if line.strip()]

return "\n".join(lines)

def fetch(url: str) -> str:

resp = requests.get(url, headers=HEADERS, timeout=20)

resp.raise_for_status()

resp.encoding = resp.apparent_encoding # 处理部分站点的编码声明不准

return clean_html(resp.text)

rows = list(csv.DictReader(open("targets.csv", encoding="utf-8")))

for i, row in enumerate(rows):

url = row["url"].strip()

try:

text = fetch(url)

host = urllib.parse.urlparse(url).netloc.replace(".", "_")

name = f'{i:02d}_{row["brand"]}_{host}.txt'

(OUT / name).write_text(text, encoding="utf-8")

print(f"OK {name} {len(text)} 字符")

except Exception as exc:

print(f"FAIL {url} {exc}")

time.sleep(2) # 限速,别打崩对方站点

```

跑一下:

```bash

python fetch.py

```

如果某个页面抓回来只有几百字符,多半是 JS 渲染的空壳,看后面的排错部分。

第 4 步:截断与打包素材

大模型有上下文上限,整篇网页塞进去既贵又容易被截断。这里做一层朴素处理:每份素材取前若干字符,保留开头(通常是标题、价值主张、功能概览)。

```python

prep.py

import json, pathlib

MAX_CHARS = 12000

snippets = []

for path in sorted(pathlib.Path("raw").glob("*.txt")):

text = path.read_text(encoding="utf-8")

snippets.append({"source": path.stem, "chars": len(text), "text": text[:MAX_CHARS]})

pathlib.Path("clean").mkdir(exist_ok=True)

pathlib.Path("clean/snippets.json").write_text(

json.dumps(snippets, ensure_ascii=False, indent=2), encoding="utf-8"

)

print(f"打包 {len(snippets)} 份素材")

```

```bash

python prep.py

```

第 5 步:让模型按固定字段抽取

这一步是关键。不要让模型直接写分析报告,先让它把网页变成结构化字段。字段固定了,后面才能做表、做图、做对比。

先建 .env 文件(记得加进 .gitignore,别把密钥提交上去):

```bash

LLM_API_KEY=你的密钥

LLM_BASE_URL=https://your-endpoint/v1

LLM_MODEL=你的模型名

```

```python

extract.py

import json, os, pathlib

from dotenv import load_dotenv

from openai import OpenAI

load_dotenv()

kwargs = {}

if os.environ.get("LLM_BASE_URL"):

kwargs["base_url"] = os.environ["LLM_BASE_URL"]

client = OpenAI(api_key=os.environ["LLM_API_KEY"], **kwargs)

MODEL = os.environ["LLM_MODEL"]

SYSTEM_PROMPT = """你在做竞品资料抽取。只依据给定文本作答,不要脑补,不要用外部知识补充。

按下面的 JSON 结构输出,字段缺失就填 "unknown":

{

"brand": "品牌或产品名",

"positioning": "一句话定位",

"target_user": "目标用户描述",

"core_features": ["核心功能,最多 5 条"],

"pricing_model": "计费方式,如订阅制/按量计费/免费加增值,看不到写 unknown",

"free_tier": "免费额度或试用策略,看不到写 unknown",

"differentiators": ["文本中自称的差异点,最多 3 条"],

"evidence": ["支撑上述判断的原文片段,最多 3 条,每条不超过 40 字"]

}

只输出 JSON,不要输出解释、不要输出 Markdown 代码块。"""

def safe_json(text: str) -> dict:

"""兼容部分接口不支持 json_object 的情况。"""

text = text.strip()

if text.startswith("```"):

text = text.strip("`")

if text.lower().startswith("json"):

text = text[4:]

start, end = text.find("{"), text.rfind("}")

if start == -1 or end == -1:

raise ValueError(f"返回内容里找不到 JSON: {text[:200]}")

return json.loads(text[start:end + 1])

def extract(snippet: dict) -> dict:

resp = client.chat.completions.create(

model=MODEL,

temperature=0,

messages=[

{"role": "system", "content": SYSTEM_PROMPT},

{"role": "user", "content": f"来源:{snippet['source']}\n\n正文:\n{snippet['text']}"},

],

)

data = safe_json(resp.choices[0].message.content)

data["_source"] = snippet["source"]

return data

snippets = json.loads(pathlib.Path("clean/snippets.json").read_text(encoding="utf-8"))

results = []

for s in snippets:

try:

results.append(extract(s))

print(f"OK {s['source']}")

except Exception as exc:

print(f"FAIL {s['source']} {exc}")

pathlib.Path("out").mkdir(exist_ok=True)

pathlib.Path("out/facts.json").write_text(

json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8"

)

print(f"共 {len(results)} 条记录")

```

如果你的接口支持 response_format={"type": "json_object"},加上它能明显降低解析失败率;不支持就删掉这个参数,safe_json 会兜底。

```bash

python extract.py

```

第 6 步:汇总成对比表

```python

table.py

import json, pathlib

import pandas as pd

facts = json.loads(pathlib.Path("out/facts.json").read_text(encoding="utf-8"))

rows = []

for f in facts:

rows.append({

"来源": f.get("_source", ""),

"品牌": f.get("brand", "unknown"),

"定位": f.get("positioning", "unknown"),

"目标用户": f.get("target_user", "unknown"),

"核心功能": "; ".join(f.get("core_features") or []),

"计费方式": f.get("pricing_model", "unknown"),

"免费额度": f.get("free_tier", "unknown"),

"差异点": "; ".join(f.get("differentiators") or []),

})

df = pd.DataFrame(rows)

df.to_csv("out/compare.csv", index=False, encoding="utf-8-sig") # Excel 打开不乱码

pathlib.Path("out/compare.md").write_text(df.to_markdown(index=False), encoding="utf-8")

print(df.to_markdown(index=False))

```

```bash

python table.py

```

到这里,事实表已经能用了。同一家有多行来源时,你会看到多条记录——这是有意的,方便你交叉核对;需要合并时在 table.py 里按品牌 groupby 一下即可。

第 7 步:画功能覆盖图

不要直接让模型"打个分",那属于主观判断。更稳的做法是:先用规则把功能关键词二值化,再画矩阵。

```python

chart.py

import json, pathlib

import pandas as pd

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt

中文字体:按系统里实际装了的字体名来填,装不上就换成英文标签

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "Source Han Sans SC", "SimHei", "Arial Unicode MS"]

plt.rcParams["axes.unicode_minus"] = False

这些词条按你的行业自己改

KEYWORDS = ["导出", "协作", "API", "权限", "模板", "多语言", "离线"]

facts = json.loads(pathlib.Path("out/facts.json").read_text(encoding="utf-8"))

rows = []

for f in facts:

blob = " ".join((f.get("core_features") or []) + (f.get("differentiators") or []))

blob += " " + (f.get("positioning") or "")

rows.append({"来源": f.get("_source", ""), **{k: int(k in blob) for k in KEYWORDS}})

df = pd.DataFrame(rows).set_index("来源")

ax = df.plot(kind="bar", figsize=(12, 5), width=0.8)

ax.set_ylabel("公开页面是否提及(1 = 提到)")

ax.set_title("功能覆盖对比:依据公开页面文本自动提取")

plt.xticks(rotation=30, ha="right")

plt.tight_layout()

pathlib.Path("out").mkdir(exist_ok=True)

plt.savefig("out/feature_matrix.png", dpi=150)

print("已生成 out/feature_matrix.png")

```

```bash

python chart.py

```

注意图注要写清"依据公开页面文本自动提取",避免读者误以为这是实测结论。

第 8 步:生成分析摘要

最后一步才是让模型写报告,输入是上一步的事实表。

```python

report.py

import json, os, pathlib

from dotenv import load_dotenv

from openai import OpenAI

load_dotenv()

kwargs = {}

if os.environ.get("LLM_BASE_URL"):

kwargs["base_url"] = os.environ["LLM_BASE_URL"]

client = OpenAI(api_key=os.environ["LLM_API_KEY"], **kwargs)

MODEL = os.environ["LLM_MODEL"]

table = pathlib.Path("out/compare.md").read_text(encoding="utf-8")

PROMPT = """下面是从各竞品公开页面抽取的结构化事实表。请写一份中文竞品分析摘要。

要求:

1. 结构依次为:一句话结论 / 定位差异 / 计费与免费策略对比 / 功能覆盖差异 / 三条可验证的机会点。

2. 每条结论后面用括号注明依据,格式如(产品A·定价页·计费方式)。

3. 事实表中标注为 unknown 的,写"公开页面未说明",不要推测。

4. 不要复述表格,要给出对比后的判断。

5. 全文不超过 800 字,最后一行写明"以上信息来自公开页面自动抽取,价格与版本以官方页面为准"。

事实表:

{table}

"""

resp = client.chat.completions.create(

model=MODEL,

temperature=0.3,

messages=[{"role": "user", "content": PROMPT}],

)

body = resp.choices[0].message.content

pathlib.Path("out/report.md").write_text(body, encoding="utf-8")

print(body)

```

```bash

python report.py

```

第 9 步:一键重跑

把流程串起来,新建 run.sh

```bash

#!/usr/bin/env bash

set -euo pipefail

python fetch.py

python prep.py

python extract.py

python table.py

python chart.py

python report.py

echo "完成:out/compare.csv, out/feature_matrix.png, out/report.md"

```

```bash

chmod +x run.sh

./run.sh

```

以后加竞品只需要改 targets.csv,重跑一次。

常见坑与排错

抓回来是空壳或只有导航文字。 页面正文由 JS 渲染,requests 拿不到。用 Playwright 打开再取 page.content(),或者直接换一个静态来源(帮助中心、文档站、更新日志通常更容易抓)。

模型编造原文里没有的价格。 三个措施一起上:temperature 设 0、prompt 里明确"看不到写 unknown"、强制输出 evidence 字段并要求是原文片段。拿到结果后随手抽两三条,回原始 txt 里搜一下能不能对上。

返回内容不是合法 JSON。 先看接口是否支持 response_format;不支持时保留 safe_json 兜底。另外,极长的输入容易让模型中途截断输出,把 MAX_CHARS 调小试试。

中文在图表里变成方块。 系统里没装中文字体,或者字体名写错了。用 fc-list :lang=zh(Linux)看一下实际可用的字体名,填进 font.sans-serif;实在不行就把图表标签改成英文。

CSV 用 Excel 打开乱码。 写入时用 encoding="utf-8-sig",上面的代码已经这么做了。

抓取被封或收到警告。 立刻停手,检查 robots.txt 与站点条款,拉长 time.sleep 间隔,减少抓取页数。合规问题不值得为一份报告冒险。

价格数字过期。 抓取时间戳要落到报告里。任何时候都要提醒读者:以官方页面当前展示为准。

把 AI 的主观判断当成结论。 摘要里的每句话都应有字段依据。如果某条结论后面标不出依据,就删掉它。

下一步建议

  • 做变化追踪:把每次的 facts.json 带日期存档,下次运行时对比,自动提示"产品B 的免费额度字段变了"。这比重新读一遍报告有用得多。
  • 扩来源:更新日志能看出迭代节奏,招聘页面能看出投入方向,公开的应用商店评论能反映用户真实抱怨。注意各平台的采集规则。
  • 把字段再细化:比如把"计费方式"拆成计费维度、起付门槛、超额规则三个字段,表格会更好用。
  • 接进日常工具:把 out/report.md 推到团队群或邮件里,改成每周定时跑一次,竞品分析就从"项目"变成了"背景信息"。
  • 加一条人工复核清单:价格、免费额度、功能声称这三类字段必须人工确认,其余可以抽查。自动化省的是整理时间,判断还得人来下。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。