用 Perplexity Search API 做一个竞品新闻监控:设定关键词和只看的网站,每天搜索一次最近一周的结果,新文章存进 SQLite(同一篇文章不会重复记录),随时导出成 Excel 能打开的 CSV。
先看清成品
成品是
monitor.py加watch.json配置。本文已用模拟客户端实际运行测试并通过:网址去掉跟踪参数后去重,两个关键词搜到同一篇文章只记一次,第二次运行没有新结果,请求带上了网站过滤和时间过滤参数,CSV 表头正确。另外用真实 SDK 和无效密钥发了请求,服务器返回 401“Invalid API key”,SDK 能接受这些参数;但 401 发生在参数校验之前,参数本身只按官方文档核对过。本文环境没有有效的 Perplexity API 密钥,没有拿到真实搜索结果。
注意:Perplexity API 按用量计费,价格以官方价格页为准,每次响应的
usage字段会给出实际费用。请在服务可用的地区使用,不要绕过地区限制。搜索结果和 AI 总结可能出错,发布前要点开来源核对;转载他人文章要遵守版权规定。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
| 监控配置 | 关键词、网站白名单、时间范围 |
| 原始搜索结果 | Search API 只返回结果,不生成 AI 答案 |
| 去重入库 | 网址规范化后作为主键 |
| CSV 导出 | 带 BOM,Excel 不乱码 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Perplexity API 密钥 | 在 API 控制台创建并充值 | 保存为环境变量 PERPLEXITY_API_KEY |
| Python | 3.10 或更高 | |
| SDK | pip install perplexityai |
本文使用 0.44.0 |
第一步:Search API 和 Agent API 有什么区别?
| 对比 | Search API | Agent API |
|---|---|---|
| 接口 | POST https://api.perplexity.ai/search |
POST https://api.perplexity.ai/v1/agent |
| 返回 | 排好序的网页结果(标题、网址、摘录) | AI 生成的答案加引用 |
| 适合 | 自己处理数据、监控、入库 | 直接要答案 |
官方文档说明,max_results 取值 1~20,默认 10;model、preset 这类 Agent API 参数不能用在搜索请求里。本文用到的过滤参数都列在官方 API 参考中:search_domain_filter(网站)、search_recency_filter(时间范围)。
第二步:代码怎么写?
1. watch.json
{
"queries": ["某品牌 新品 发布", "某品牌 价格 调整"],
"domains": ["36kr.com", "ithome.com", "sspai.com"],
"recency": "week",
"max_results": 10
}
把“某品牌”换成你要关注的公司或产品。
2. monitor.py
"""竞品新闻监控:用 Perplexity Search API 定期搜索,新结果存入 SQLite 并导出 CSV。
用法:python monitor.py # 搜索并记录新结果
python monitor.py --export # 导出全部记录到 news.csv
"""
import csv
import json
import sqlite3
import sys
from datetime import datetime
from urllib.parse import urlsplit
DB = "news.db"
def init(db):
db.execute("""CREATE TABLE IF NOT EXISTS news (
url TEXT PRIMARY KEY, title TEXT, query TEXT, date TEXT, snippet TEXT, found_at TEXT)""")
def normalize(url: str) -> str:
"""去掉 #片段 和常见跟踪参数,避免同一篇文章重复入库。"""
p = urlsplit(url)
q = "&".join(x for x in p.query.split("&") if x and not x.startswith(("utm_", "spm=", "from=")))
return f"{p.scheme}://{p.netloc.lower()}{p.path}" + (f"?{q}" if q else "")
def run(client, cfg, db):
init(db)
new = []
for q in cfg["queries"]:
res = client.search.create(
query=q,
max_results=cfg.get("max_results", 10),
search_domain_filter=cfg.get("domains") or None,
search_recency_filter=cfg.get("recency", "week"),
)
for r in res.results:
url = normalize(r.url)
cur = db.execute("INSERT OR IGNORE INTO news VALUES (?, ?, ?, ?, ?, ?)",
(url, r.title, q, getattr(r, "date", None), (getattr(r, "snippet", "") or "")[:300],
datetime.now().isoformat(timespec="seconds")))
if cur.rowcount:
new.append((q, r.title, url))
db.commit()
return new
def export(db, path="news.csv"):
init(db)
rows = db.execute("SELECT found_at, query, date, title, url FROM news ORDER BY found_at DESC").fetchall()
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["发现时间", "关键词", "发布日期", "标题", "链接"])
w.writerows(rows)
return len(rows)
def main(argv=None, client=None):
argv = sys.argv[1:] if argv is None else argv
db = sqlite3.connect(DB)
if "--export" in argv:
print(f"已导出 {export(db)} 条到 news.csv")
return
if client is None:
from perplexity import Perplexity
client = Perplexity()
cfg = json.load(open("watch.json", encoding="utf-8"))
new = run(client, cfg, db)
for q, title, url in new:
print(f"[新] {q}|{title}\n {url}")
print(f"本次新增 {len(new)} 条")
if __name__ == "__main__":
main()
| 要点 | 说明 |
|---|---|
normalize |
去掉 utm_ 等跟踪参数,域名转小写 |
INSERT OR IGNORE |
已存在的网址自动跳过 |
| 摘录只存 300 字 | 只做索引,不保存全文 |
第三步:怎样测试和运行?
1. 离线测试
import sqlite3
from types import SimpleNamespace as NS
import monitor
CFG = {"queries": ["甲", "乙"], "domains": ["36kr.com"], "recency": "week", "max_results": 5}
class Fake:
def __init__(self):
self.calls = []
self.search = NS(create=self._create)
def _create(self, **kw):
self.calls.append(kw)
return NS(results=[NS(title="新品发布", url="https://36KR.com/p/1?utm_source=x&id=9#top", date="2026-10-20", snippet="……"),
NS(title="价格调整", url="https://36kr.com/p/2", date=None, snippet=None)])
def test_dedupe_and_params(tmp_path):
db = sqlite3.connect(tmp_path / "n.db")
c = Fake()
first = monitor.run(c, CFG, db)
assert len(first) == 2 # 两个关键词返回相同文章,只记一次
assert first[0][2] == "https://36kr.com/p/1?id=9"
assert c.calls[0]["search_domain_filter"] == ["36kr.com"] and c.calls[0]["search_recency_filter"] == "week"
assert monitor.run(c, CFG, db) == [] # 第二次运行没有新结果
n = monitor.export(db, tmp_path / "n.csv")
assert n == 2
assert (tmp_path / "n.csv").read_text(encoding="utf-8-sig").startswith("发现时间,关键词")
python -m pytest -q
本文实际输出 1 passed。
2. 真实运行
export PERPLEXITY_API_KEY="你的密钥"
python monitor.py
python monitor.py --export
期望输出:
[新] 某品牌 新品 发布|(文章标题)
https://…
本次新增 N 条
已导出 N 条到 news.csv
第二天再运行,只会列出新出现的文章。
3. 每天自动运行
0 9 * * * cd /home/me/news-monitor && PERPLEXITY_API_KEY=你的密钥 .venv/bin/python monitor.py >> monitor.log 2>&1
出问题时该怎么调?
1. 结果太少
去掉网站白名单试试,或把 recency 改成 month。
2. 结果不相关
关键词写得更具体,加上产品型号;网站白名单只保留可靠来源(官方 API 参考说明 search_domain_filter 最多 20 个域名)。
3. 同一篇文章仍然重复
不同网址指向同一篇文章(例如手机版和电脑版)。在 normalize 里加入你发现的规则。
常见问题怎么处理?
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 401 | 密钥错误 | 重新复制 |
max_results 报错 |
超过 20 | 改成 1~20 |
传了 preset 报错 |
搜索接口没有这个参数 | 删除 |
| CSV 用 Excel 打开乱码 | 编码 | 已用 utf-8-sig |
| 想要 AI 总结 | Search API 不生成答案 | 把结果交给 Agent API 或参考本系列简报教程 |
项目资源
| 用途 | 地址 |
|---|---|
| Search API 快速入门 | https://docs.perplexity.ai/docs/search/quickstart |
| Search API 参数参考 | https://docs.perplexity.ai/api-reference/search-post |
| 快速入门(创建 API 密钥) | https://docs.perplexity.ai/docs/getting-started/quickstart |
| 价格 | https://docs.perplexity.ai/docs/getting-started/pricing |
| 官方 SDK | https://docs.perplexity.ai/docs/sdk/overview |
建议先放一个关键词手动运行几次,看看结果质量和网站过滤效果。
调整满意后再加关键词、设置定时任务,每周导出一次 CSV 浏览。