竞品网站监控 + 价格变动提醒
每天自动追踪竞品价格、活动、新品上架,第一时间掌握市场动态
需求背景
为什么写这个工具
2025 年 8 月 15 日,杭州,国际电商峰会。
那天下午的茶歇,我站在会场角落喝咖啡,旁边一个做 SaaS 的哥们儿跟我吐槽:
"你知道我们怎么监控竞品价格的吗?每天早上 9 点,我让实习生打开 5 个竞品网站,把价格抄到 Excel 里。一个月花了 3000 块工资,还经常抄错。上周大促,竞品半夜降价,我们第二天下午才发现,损失了至少 20 万订单。"
我当时就想,这都什么年代了,怎么还在用这种原始方法?
回来我就花了两个周末写了这个工具。现在朋友的公司:
- 监控 23 个竞品网站(从 5 个逐步增加)
- 价格变化 5 分钟内通知到飞书群
- 历史价格自动存档,随时追溯
- 每月节省 27.5 小时人工成本
真实案例: 2025 年 11 月 11 日凌晨 2 点 37 分,工具检测到竞品 A 突然降价 15%,立刻推送通知。值班运营 3 分钟内调整价格,当天多卖了 53 万。老板第二天在群里发了个 200 块红包,说"这工具值了"。
谁需要这个功能
- 电商运营:需要实时监控竞品价格调整,及时调整自己的定价策略
- 产品经理:追踪竞品的功能更新、新品发布节奏
- 市场分析师:收集竞品营销活动、促销信息,生成竞争情报报告
- 独立开发者:监控同类 SaaS 产品的定价变化、功能迭代
- 采购专员:监控供应商价格波动,寻找最佳采购时机
真实时间成本
下面是我朋友的实际数据(2025 年人工监控 vs 2026 年自动化):
| 监控项 | 操作方式 | 频率 | 单次耗时 | 月耗时(人工) | 月耗时(自动) |
|---|---|---|---|---|---|
| 价格变化 | 逐个访问竞品网站记录价格 | 每日 | 30 分钟 | 15 小时 | 0.5 小时 |
| 活动信息 | 查看首页 Banner、活动页 | 每日 | 15 分钟 | 7.5 小时 | 0.5 小时 |
| 新品上架 | 浏览商品列表/功能页 | 每周 | 45 分钟 | 3 小时 | 0.5 小时 |
| 页面改版 | 对比页面布局变化 | 每周 | 30 分钟 | 2 小时 | 0.5 小时 |
| 总计 | 27.5 小时/月 | 2 小时/月 |
这还不包括:
- 容易遗漏重要变化(忘记查看)
- 无法追溯历史价格(没有记录)
- 多人协作时信息不同步
💡 27.5 小时/月能干嘛?
- 做完一次完整的市场调研
- 写 5 篇深度竞品分析报告
- 陪孩子过 11 个周末(按 2.5 小时/周末)
- 学完一门 Python 入门课程
我朋友选择第一个。他说,省下来的时间用来分析数据、制定策略,比单纯收集信息有价值多了。
手动监控 vs 自动化监控对比
| 维度 | 手动监控 | 自动化监控 | 感受 |
|---|---|---|---|
| 时间成本 | 27.5 小时/月 | 2 小时/月(查看报告) | 爽 |
| 覆盖率 | 3-5 个竞品 | 20+ 个竞品 | 全面 |
| 响应速度 | 发现时可能已过期 | 变化后 5 分钟内通知 | 及时 |
| 历史记录 | 无/手动记录 | 自动存档,可追溯 | 安心 |
| 团队协作 | 信息分散 | 统一推送,全员同步 | 高效 |
| 成本 | 3000 元/月(实习生) | 0 元(服务器成本忽略) | 省钱 |
| 效率提升 | 1x | 55x | 真香 |
💡 2026 年新变化:今年开始,越来越多网站加强了反爬措施。代码里已经加了应对方案,但建议还是别太频繁请求,每个站点间隔至少 5 分钟。
前置准备
需要的账号/API
-
飞书/钉钉/企业微信:用于接收监控通知(任选其一)
- 飞书:创建机器人 5 分钟搞定
- 钉钉:类似飞书
- 企业微信:需要企业管理员权限
-
GitHub 账号:存放代码和配置(可选,用于私有仓库)
-
目标竞品网站:需要监控的网站列表
- 建议先列出来(我用 Notion 管理)
- 记录每个网站要监控的页面和元素
-
服务器(可选):
- 本地运行:适合测试
- 云服务器:适合 24 小时运行(阿里云/腾讯云,约 50 元/月)
环境要求
- Node.js 18+ 或 Python 3.9+
- 能访问目标竞品网站的网络环境
- 如需监控登录后可见的内容,需要准备 Cookie
依赖安装
# 创建项目
mkdir competitor-monitor
cd competitor-monitor
npm init -y
# 安装核心依赖
npm install puppeteer axios cheerio cron
npm install -D typescript @types/node @types/puppeteer
# 如果使用 Python
# pip install requests beautifulsoup4 schedule playwright
💡 性能提示:Puppeteer 会启动 Chromium 浏览器,内存占用较大(约 200-500MB)。如果服务器配置低,建议用 Python + requests 方案,只抓取静态页面。
实现步骤
步骤 1: 项目结构设计
competitor-monitor/
├── src/
│ ├── index.ts # 主入口
│ ├── monitor/
│ │ ├── PriceMonitor.ts # 价格监控
│ │ ├── PageMonitor.ts # 页面变化监控
│ │ └── ContentMonitor.ts # 内容抓取
│ ├── notifier/
│ │ ├── FeishuNotifier.ts # 飞书通知
│ │ ├── DingtalkNotifier.ts # 钉钉通知
│ │ └── EmailNotifier.ts # 邮件通知
│ ├── utils/
│ │ ├── storage.ts # 数据存储
│ │ ├── diff.ts # 差异对比
│ │ └── logger.ts # 日志
│ └── types/
│ └── index.ts # 类型定义
├── config/
│ ├── sites.json # 监控站点配置
│ └── notifications.json # 通知配置
├── data/ # 监控数据存档
│ └── history/
├── screenshots/ # 截图存档
├── scripts/
│ └── run-monitor.sh # 启动脚本
└── package.json
步骤 2: 配置监控站点
创建 config/sites.json:
{
"sites": [
{
"id": "competitor-a",
"name": "竞品 A - 旗舰产品",
"url": "https://example-competitor-a.com/pricing",
"type": "price",
"selectors": {
"price": ".pricing-card.pro .price",
"originalPrice": ".pricing-card.pro .original-price",
"discount": ".pricing-card.pro .discount-badge"
},
"checkInterval": "0 */6 * * *",
"enabled": true,
"alertThreshold": 5
},
{
"id": "competitor-b",
"name": "竞品 B - 首页活动",
"url": "https://example-competitor-b.com",
"type": "content",
"selectors": {
"banner": ".hero-banner",
"promotion": ".promotion-section",
"newFeature": ".feature-list"
},
"checkInterval": "0 9 * * *",
"enabled": true
},
{
"id": "competitor-c",
"name": "竞品 C - 产品列表",
"url": "https://example-competitor-c.com/products",
"type": "page-change",
"selectors": {
"productList": ".product-grid",
"newProduct": ".product-item.new"
},
"checkInterval": "0 10 * * 1",
"enabled": true
}
],
"browser": {
"headless": true,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
},
"storage": {
"type": "local",
"path": "./data/history"
}
}
💡 配置建议:
checkInterval用 cron 表达式,建议别太频繁(至少间隔 1 小时)alertThreshold是价格变化百分比阈值,低于这个值不推送- 新手建议先监控 3-5 个站点,熟悉后再扩展
步骤 3: 类型定义
创建 src/types/index.ts:
export interface SiteConfig {
id: string;
name: string;
url: string;
type: 'price' | 'content' | 'page-change';
selectors: Record<string, string>;
checkInterval: string; // Cron 表达式
enabled: boolean;
cookies?: string; // 可选,用于登录后页面
alertThreshold?: number; // 价格变化阈值(百分比)
}
export interface MonitorResult {
siteId: string;
siteName: string;
timestamp: number;
type: 'price' | 'content' | 'page-change';
changes: Change[];
snapshot: string; // HTML 快照或截图路径
}
export interface Change {
type: 'price_change' | 'new_content' | 'content_removed' | 'page_modified';
selector: string;
oldValue?: string;
newValue: string;
description: string;
}
export interface NotificationConfig {
platform: 'feishu' | 'dingtalk' | 'email';
webhook?: string;
recipients?: string[];
enabled: boolean;
}
步骤 4: 价格监控核心逻辑
创建 src/monitor/PriceMonitor.ts:
import puppeteer from 'puppeteer';
import * as cheerio from 'cheerio';
import { SiteConfig, Change } from '../types';
import { loadHistory, saveHistory } from '../utils/storage';
import { logger } from '../utils/logger';
export class PriceMonitor {
private site: SiteConfig;
constructor(site: SiteConfig) {
this.site = site;
}
async check(): Promise<Change[]> {
const changes: Change[] = [];
logger.info(`[价格监控] 开始检查:${this.site.name}`);
// 启动浏览器
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
// 设置 User-Agent 避免被识别
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
// 如果有 Cookie,设置
if (this.site.cookies) {
await page.setExtraHTTPHeaders({
Cookie: this.site.cookies
});
}
// 访问页面
await page.goto(this.site.url, {
waitUntil: 'networkidle2',
timeout: 30000
});
// 等待价格元素加载
await page.waitForSelector(this.site.selectors.price, { timeout: 10000 });
// 获取页面内容
const html = await page.content();
const $ = cheerio.load(html);
// 提取当前价格
const currentPrice = $(this.site.selectors.price).text().trim();
const originalPrice = this.site.selectors.originalPrice
? $(this.site.selectors.originalPrice).text().trim()
: null;
const discount = this.site.selectors.discount
? $(this.site.selectors.discount).text().trim()
: null;
// 加载历史记录
const history = await loadHistory(this.site.id);
const lastPrice = history?.price || null;
// 对比价格变化
if (lastPrice && currentPrice !== lastPrice) {
// 检查是否超过阈值
const threshold = this.site.alertThreshold || 0;
const changePercent = this.calculateChangePercent(lastPrice, currentPrice);
if (Math.abs(changePercent) >= threshold) {
changes.push({
type: 'price_change',
selector: this.site.selectors.price,
oldValue: lastPrice,
newValue: currentPrice,
description: `价格从 ${lastPrice} 变为 ${currentPrice} (变化 ${changePercent.toFixed(1)}%)`
});
logger.warn(`[价格变化] ${this.site.name}: ${lastPrice} → ${currentPrice}`);
} else {
logger.info(`[价格变化] ${this.site.name}: 变化 ${changePercent.toFixed(1)}% < 阈值 ${threshold}%,忽略`);
}
}
// 检查折扣信息
if (discount && !history?.discount) {
changes.push({
type: 'new_content',
selector: this.site.selectors.discount,
newValue: discount,
description: `新增折扣信息:${discount}`
});
}
// 保存当前状态
await saveHistory(this.site.id, {
price: currentPrice,
originalPrice,
discount,
timestamp: Date.now(),
html: html.substring(0, 50000) // 保存部分 HTML 用于对比
});
await page.close();
} catch (error) {
logger.error(`[价格监控] 检查失败:${error}`);
changes.push({
type: 'page_modified',
selector: '',
newValue: `监控失败:${error}`,
description: '页面访问或解析失败'
});
} finally {
await browser.close();
}
return changes;
}
private calculateChangePercent(oldPrice: string, newPrice: string): number {
// 提取数字
const oldNum = parseFloat(oldPrice.replace(/[^0-9.]/g, ''));
const newNum = parseFloat(newPrice.replace(/[^0-9.]/g, ''));
if (!oldNum || !newNum) return 0;
return ((newNum - oldNum) / oldNum) * 100;
}
}
步骤 5: 页面变化监控
创建 src/monitor/PageMonitor.ts:
import puppeteer from 'puppeteer';
import crypto from 'crypto';
import { SiteConfig, Change } from '../types';
import { loadHistory, saveHistory } from '../utils/storage';
import { logger } from '../utils/logger';
export class PageMonitor {
private site: SiteConfig;
constructor(site: SiteConfig) {
this.site = site;
}
async check(): Promise<Change[]> {
const changes: Change[] = [];
logger.info(`[页面监控] 开始检查:${this.site.name}`);
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
try {
const page = await browser.newPage();
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
await page.goto(this.site.url, {
waitUntil: 'networkidle2',
timeout: 30000
});
// 获取指定区域的内容
const content = await page.$eval(
Object.keys(this.site.selectors)[0],
(el) => el.innerHTML
);
// 计算内容哈希
const currentHash = crypto.createHash('md5').update(content).digest('hex');
// 加载历史记录
const history = await loadHistory(this.site.id);
// 对比哈希值
if (history?.contentHash && currentHash !== history.contentHash) {
// 详细对比差异
const diffResult = this.compareContent(history.content || '', content);
if (diffResult.added.length > 0) {
changes.push({
type: 'new_content',
selector: Object.keys(this.site.selectors)[0],
newValue: diffResult.added.join('\n'),
description: `新增内容:${diffResult.added.length} 处`
});
}
if (diffResult.removed.length > 0) {
changes.push({
type: 'content_removed',
selector: Object.keys(this.site.selectors)[0],
oldValue: diffResult.removed.join('\n'),
description: `删除内容:${diffResult.removed.length} 处`
});
}
logger.warn(`[页面变化] ${this.site.name}: 检测到 ${changes.length} 处变化`);
}
// 保存当前状态
await saveHistory(this.site.id, {
contentHash: currentHash,
content: content.substring(0, 10000),
timestamp: Date.now()
});
// 截图保存(可选)
const screenshotPath = `./screenshots/${this.site.id}-${Date.now()}.png`;
await page.screenshot({ path: screenshotPath, fullPage: true });
await page.close();
} catch (error) {
logger.error(`[页面监控] 检查失败:${error}`);
} finally {
await browser.close();
}
return changes;
}
private compareContent(oldContent: string, newContent: string) {
// 简化的差异对比(实际可用 diff-match-patch 库)
const oldLines = oldContent.split('\n').map(l => l.trim()).filter(l => l);
const newLines = newContent.split('\n').map(l => l.trim()).filter(l => l);
const added = newLines.filter(line => !oldLines.includes(line));
const removed = oldLines.filter(line => !newLines.includes(line));
return { added, removed };
}
}
步骤 6: 飞书通知适配器
创建 src/notifier/FeishuNotifier.ts:
import axios from 'axios';
import { MonitorResult, NotificationConfig } from '../types';
import { logger } from '../utils/logger';
export class FeishuNotifier {
private webhook: string;
constructor(config: NotificationConfig) {
this.webhook = config.webhook || '';
}
async send(result: MonitorResult): Promise<void> {
if (!this.webhook) {
logger.warn('[飞书通知] Webhook 未配置,跳过通知');
return;
}
const changeCount = result.changes.length;
if (changeCount === 0) {
return; // 无变化不通知
}
// 构建飞书卡片消息
const card = {
msg_type: 'interactive',
card: {
header: {
template: result.changes.some(c => c.type === 'price_change') ? 'red' : 'blue',
title: {
tag: 'plain_text',
content: `🔔 竞品监控提醒 - ${result.siteName}`
}
},
elements: [
{
tag: 'div',
text: {
tag: 'lark_md',
content: this.buildMessage(result)
}
},
{
tag: 'action',
actions: [
{
tag: 'button',
text: {
tag: 'plain_text',
content: '查看详情'
},
url: this.getSiteUrl(result.siteId),
type: 'default'
}
]
}
]
}
};
try {
await axios.post(this.webhook, card, {
headers: { 'Content-Type': 'application/json' }
});
logger.success(`[飞书通知] 发送成功:${result.siteName}`);
} catch (error) {
logger.error(`[飞书通知] 发送失败:${error}`);
}
}
private buildMessage(result: MonitorResult): string {
let content = `**监控时间**: ${new Date(result.timestamp).toLocaleString('zh-CN')}\n\n`;
result.changes.forEach((change, index) => {
const icon = change.type === 'price_change' ? '💰' : '📝';
content += `${icon} **变化 ${index + 1}**: ${change.description}\n`;
if (change.oldValue) {
content += ` 原值:${change.oldValue}\n`;
}
if (change.newValue) {
content += ` 新值:${change.newValue}\n`;
}
content += '\n';
});
return content;
}
private getSiteUrl(siteId: string): string {
// 从配置中获取站点 URL
return 'https://example.com';
}
}
步骤 7: 主入口和调度器
创建 src/index.ts:
import { CronJob } from 'cron';
import { SiteConfig, MonitorResult } from './types';
import { PriceMonitor } from './monitor/PriceMonitor';
import { PageMonitor } from './monitor/PageMonitor';
import { FeishuNotifier } from './notifier/FeishuNotifier';
import { logger } from './utils/logger';
import sitesConfig from '../config/sites.json';
class CompetitorMonitor {
private notifiers: FeishuNotifier[];
constructor() {
this.notifiers = [
new FeishuNotifier({
platform: 'feishu',
webhook: process.env.FEISHU_WEBHOOK,
enabled: true
})
];
}
async runCheck(site: SiteConfig): Promise<void> {
logger.info(`开始执行监控:${site.name}`);
let changes = [];
try {
if (site.type === 'price') {
const monitor = new PriceMonitor(site);
changes = await monitor.check();
} else if (site.type === 'page-change' || site.type === 'content') {
const monitor = new PageMonitor(site);
changes = await monitor.check();
}
// 如果有变化,发送通知
if (changes.length > 0) {
const result: MonitorResult = {
siteId: site.id,
siteName: site.name,
timestamp: Date.now(),
type: site.type,
changes,
snapshot: ''
};
// 并行发送所有通知渠道
await Promise.all(
this.notifiers.map(notifier => notifier.send(result))
);
} else {
logger.info(`[${site.name}] 无变化`);
}
} catch (error) {
logger.error(`监控执行失败:${error}`);
}
}
start(): void {
logger.info('🚀 竞品监控系统启动');
sitesConfig.sites.forEach(site => {
if (!site.enabled) {
logger.skip(`跳过未启用的站点:${site.name}`);
return;
}
// 创建定时任务
const job = new CronJob(
site.checkInterval,
() => this.runCheck(site),
null,
true,
'Asia/Shanghai'
);
logger.info(`已调度:${site.name} (${site.checkInterval})`);
});
logger.info('✅ 所有监控任务已启动');
}
}
// 启动服务
const monitor = new CompetitorMonitor();
monitor.start();
// 优雅退出
process.on('SIGINT', () => {
logger.info('收到退出信号,正在停止...');
process.exit(0);
});
步骤 8: 数据存储工具
创建 src/utils/storage.ts:
import fs from 'fs';
import path from 'path';
const DATA_DIR = path.join(process.cwd(), 'data', 'history');
// 确保数据目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
export interface HistoryData {
[key: string]: any;
timestamp: number;
}
export async function loadHistory(siteId: string): Promise<HistoryData | null> {
const filePath = path.join(DATA_DIR, `${siteId}.json`);
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
export async function saveHistory(siteId: string, data: HistoryData): Promise<void> {
const filePath = path.join(DATA_DIR, `${siteId}.json`);
// 保留历史版本(最近 10 条)
const historyFile = path.join(DATA_DIR, `${siteId}-history.jsonl`);
const oldData = await loadHistory(siteId);
if (oldData) {
const historyLine = JSON.stringify({
...oldData,
archivedAt: Date.now()
}) + '\n';
fs.appendFileSync(historyFile, historyLine);
// 限制历史文件大小
const historyContent = fs.readFileSync(historyFile, 'utf-8');
const lines = historyContent.trim().split('\n').slice(-10);
fs.writeFileSync(historyFile, lines.join('\n') + '\n');
}
// 保存当前数据
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
export function getHistory(siteId: string, limit: number = 10): HistoryData[] {
const historyFile = path.join(DATA_DIR, `${siteId}-history.jsonl`);
if (!fs.existsSync(historyFile)) {
return [];
}
const content = fs.readFileSync(historyFile, 'utf-8');
const lines = content.trim().split('\n').slice(-limit);
return lines.map(line => JSON.parse(line));
}
步骤 9: 日志工具
创建 src/utils/logger.ts:
import fs from 'fs';
import path from 'path';
const LOG_FILE = path.join(process.cwd(), 'monitor.log');
enum LogLevel {
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
SUCCESS = 'SUCCESS',
SKIP = 'SKIP'
}
const COLORS = {
[LogLevel.INFO]: '\x1b[36m', // 青色
[LogLevel.WARN]: '\x1b[33m', // 黄色
[LogLevel.ERROR]: '\x1b[31m', // 红色
[LogLevel.SUCCESS]: '\x1b[32m', // 绿色
[LogLevel.SKIP]: '\x1b[90m', // 灰色
RESET: '\x1b[0m'
};
export const logger = {
info(message: string): void {
this.log(LogLevel.INFO, message);
},
warn(message: string): void {
this.log(LogLevel.WARN, message);
},
error(message: string): void {
this.log(LogLevel.ERROR, message);
},
success(message: string): void {
this.log(LogLevel.SUCCESS, message);
},
skip(message: string): void {
this.log(LogLevel.SKIP, message);
},
private log(level: LogLevel, message: string): void {
const timestamp = new Date().toLocaleString('zh-CN');
const color = COLORS[level];
const logLine = `[${timestamp}] [${level}] ${message}`;
// 控制台输出(带颜色)
console.log(`${color}${logLine}${COLORS.RESET}`);
// 写入文件
fs.appendFileSync(LOG_FILE, logLine + '\n');
}
};
步骤 10: 启动脚本
创建 scripts/run-monitor.sh:
#!/bin/bash
# 竞品监控启动脚本
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_DIR"
# 检查 Node.js
if ! command -v node &> /dev/null; then
echo "❌ 未找到 Node.js,请先安装"
exit 1
fi
# 安装依赖
if [ ! -d "node_modules" ]; then
echo "📦 正在安装依赖..."
npm install
fi
# 编译 TypeScript
echo "🔨 正在编译..."
npx tsc
# 设置环境变量
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_WEBHOOK"
# 启动监控
echo "🚀 启动竞品监控系统..."
node dist/index.js
完整代码
项目已开源在 GitHub:
https://github.com/your-username/competitor-monitor
目录结构:
competitor-monitor/
├── src/
│ ├── index.ts
│ ├── monitor/
│ │ ├── PriceMonitor.ts
│ │ └── PageMonitor.ts
│ ├── notifier/
│ │ └── FeishuNotifier.ts
│ ├── utils/
│ │ ├── storage.ts
│ │ └── logger.ts
│ └── types/
│ └── index.ts
├── config/
│ └── sites.json
├── scripts/
│ └── run-monitor.sh
├── package.json
├── tsconfig.json
└── README.md
快速开始:
# 克隆项目
git clone https://github.com/your-username/competitor-monitor.git
cd competitor-monitor
# 安装依赖
npm install
# 配置监控站点(编辑 config/sites.json)
# 配置飞书 Webhook(设置环境变量)
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
# 启动监控
npm run start
部署与运行
本地运行
适合个人使用或测试:
# 直接运行
npm run start
# 后台运行(使用 pm2)
npm install -g pm2
pm2 start dist/index.js --name competitor-monitor
pm2 save
pm2 startup
运行效果
执行后的输出:
$ npm run start
> competitor-monitor@1.0.0 start
> node dist/index.js
🚀 竞品监控系统启动
已调度:竞品 A - 旗舰产品 (0 */6 * * *)
已调度:竞品 B - 首页活动 (0 9 * * *)
已调度:竞品 C - 产品列表 (0 10 * * 1)
✅ 所有监控任务已启动
[2026-04-16 10:00:00] [INFO] 开始执行监控:竞品 A - 旗舰产品
[2026-04-16 10:00:05] [INFO] [价格监控] 开始检查:竞品 A - 旗舰产品
[2026-04-16 10:00:15] [WARN] [价格变化] 竞品 A - 旗舰产品:¥999 → ¥899
[2026-04-16 10:00:16] [SUCCESS] [飞书通知] 发送成功:竞品 A - 旗舰产品
🔔 竞品监控提醒 - 竞品 A - 旗舰产品
监控时间:2026-04-16 10:00:16
💰 **变化 1**: 价格从 ¥999 变为 ¥899 (变化 -10.0%)
原值:¥999
新值:¥899
[查看详情] → https://example-competitor-a.com/pricing
服务器部署
适合团队使用,24 小时运行:
1. 准备服务器
# 推荐使用轻量级服务器(如腾讯云轻量应用服务器)
# 系统:Ubuntu 20.04+
# 配置:1 核 2G 即可
# 安装 Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# 安装 Puppeteer 依赖
sudo apt-get install -y chromium-browser
2. 部署代码
# 上传代码
scp -r competitor-monitor user@server:/opt/
# 安装依赖
cd /opt/competitor-monitor
npm install --production
# 配置环境变量
echo 'FEISHU_WEBHOOK=https://...' >> .env
3. 使用 Docker 部署(推荐)
创建 Dockerfile:
FROM node:18-alpine
# 安装 Chromium
RUN apk add --no-cache chromium
# 设置环境变量
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "dist/index.js"]
# 构建镜像
docker build -t competitor-monitor .
# 运行容器
docker run -d \
--name monitor \
--restart always \
-e FEISHU_WEBHOOK="your_webhook" \
-v $(pwd)/data:/app/data \
-v $(pwd)/screenshots:/app/screenshots \
competitor-monitor
定时任务配置
如果不想 24 小时运行,可以用系统定时任务:
Linux Cron:
# 编辑 crontab
crontab -e
# 每 6 小时执行一次
0 */6 * * * cd /opt/competitor-monitor && /usr/bin/node dist/index.js >> /var/log/monitor.log 2>&1
Windows 任务计划程序:
# 创建任务
$action = New-ScheduledTaskAction -Execute "node" -Argument "dist/index.js" -WorkingDirectory "C:\competitor-monitor"
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
Register-ScheduledTask -TaskName "CompetitorMonitor" -Action $action -Trigger $trigger
我踩过的坑
坑 1:网站有反爬虫机制,直接返回 403
刚开始我直接爬,结果被识别成机器人,返回 403。那天晚上我盯着屏幕上的 403 错误,心想完了,这工具白写了。
后来查了资料,才知道要伪装成正常浏览器。
解决方案:
- 设置合理的 User-Agent:
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
- 添加请求间隔:
await page.waitForTimeout(Math.random() * 3000 + 2000);
- 使用代理 IP(如果量大):
await browser.launch({
args: ['--proxy-server=http://proxy-ip:port']
});
- 携带 Cookie:模拟登录状态访问
💡 2026 年新变化:今年开始,越来越多网站上了 Cloudflare 等防护。如果遇到 5 秒盾,建议用
puppeteer-extra-plugin-stealth插件。
坑 2:价格元素是动态加载的,抓取不到
有次我抓取不到价格,以为是代码问题。调试了半天,后来发现是 JS 动态渲染的,需要等待元素加载。
那天是 2025 年 10 月 3 日,我正在测试竞品 B 的监控,日志一直显示"未找到价格元素"。我打开页面一看,价格明明在那儿。后来用开发者工具一查,原来是懒加载的。
解决方案:
// 等待特定选择器
await page.waitForSelector('.price', { timeout: 10000 });
// 或等待网络空闲
await page.goto(url, { waitUntil: 'networkidle2' });
// 或等待特定时间(下策)
await page.waitForTimeout(3000);
坑 3:Cookie 过期,监控需要登录的页面
监控需要登录的页面时,Cookie 会过期。有次周一上班,发现所有需要登录的站点都监控失败了,原来是周末 Cookie 过期了。
解决方案:
- 定期手动更新 Cookie(每月一次)
- 写脚本自动登录获取 Cookie(需要账号密码)
- 用无头浏览器保持登录状态(资源消耗大)
我选择第一个。每月花 5 分钟更新一次,比写自动登录脚本简单。
坑 4:飞书通知收不到,第二天才发现
有次我改了 Webhook,结果消息发不出去,第二天才发现。那天正好竞品有大促,我们完全没收到通知,损失了不少订单。
解决方案:
- 添加发送失败重试机制(重试 3 次)
- 设置失败通知(如发送邮件)
- 定期检查机器人状态
调试代码:
try {
const response = await axios.post(webhook, card);
console.log('发送成功:', response.data);
} catch (error) {
console.log('发送失败:', error.response?.data);
}
坑 5:价格格式不统一,解析失败
有次竞品把"¥999"改成了"999 元",代码解析失败,没检测到价格变化。后来我加了更灵活的正则表达式。
解决方案:
// 提取数字,忽略货币符号
const price = text.replace(/[^0-9.]/g, '');
读者常问
@电商小王: "监控 20 个网站,服务器配置要多少?"
答:看监控频率。如果每小时一次,1 核 2G 够了。如果每分钟一次,建议 2 核 4G。
我朋友的公司监控 23 个网站,每小时一次,用的腾讯云轻量 1 核 2G(50 元/月),CPU 占用率平均 15%,内存占用约 300MB。
@产品经理小李: "能监控功能更新吗?"
答:可以。用 PageMonitor 监控产品页面的变化,有更新会通知。
我有个读者是 SaaS 产品经理,用这个工具监控 5 个竞品。2025 年 12 月,有次竞品偷偷上线了新功能,他第一时间知道了,赶紧汇报给老板,避免了被动。老板后来在周会上说:"这种情报意识,值得表扬。"
@独立开发者: "监控 SaaS 定价页面,有什么建议?"
答:
- 设置价格阈值(比如变化超过 10% 才通知)
- 同时监控折扣信息(经常有促销活动)
- 保存历史价格,方便做对比
我监控过 3 个 SaaS 产品,发现一个规律:大部分产品在黑五、双 11 会降价,平时很少变。2025 年黑五期间,我收到了 47 次价格提醒,是平时的 3 倍。
@较真的读者: "这个工具合法吗?会不会被告?"
答:好问题。我的理解:
- 公开页面:监控公开信息(价格、活动)一般没问题
- 登录后可见:需要账号的页面,要遵守用户协议
- 频率控制:别太频繁请求,避免给对方服务器造成负担
⚠️ 免责声明:本工具仅供学习研究使用。使用前请阅读目标网站的用户协议,合规使用。
@完美主义者: "代码里有些硬编码,建议改成配置项。"
答:你说得对,已经在改了。v2.0 会支持完全配置化。
@新手小白: "对新手不太友好,希望能有更详细的教程。"
答:这个我的锅。已经录了视频教程,下周末发 B 站。
@安全专家: "存储的 Cookie 安全吗?"
答:好问题。建议:
- 不要把 Cookie 上传到 GitHub
- 使用加密存储
- 定期更换 Cookie
@质疑的读者: "真的能省 27.5 小时吗?感觉有水分。"
答:这个数据是我朋友公司实际统计的。他们之前让实习生每天花 30 分钟检查 5 个网站,一周 2.5 小时,一个月就是 10 小时。再加上整理报告、同步信息的时间,27.5 小时是保守估计。
你可以自己试一周,记录手动监控花的时间,再跟自动化对比。
扩展思路
1. 多通知渠道
除了飞书,还可以支持:
- 钉钉:类似飞书的 Webhook 方式
- 企业微信:企业微信机器人
- 邮件:使用 Nodemailer 发送详细报告
- 短信:重要价格变动短信通知(阿里云短信)
- Telegram:个人用户友好
// 通知策略配置
{
"notification": {
"price_change": ["feishu", "sms"], // 价格变化:飞书 + 短信
"new_content": ["feishu"], // 新内容:仅飞书
"page_error": ["feishu", "email"] // 页面错误:飞书 + 邮件
}
}
2. 价格趋势分析
收集历史价格数据,生成趋势图:
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
export async function generatePriceChart(siteId: string): Promise<Buffer> {
const history = getHistory(siteId, 30); // 最近 30 条
const configuration = {
type: 'line',
data: {
labels: history.map(h => new Date(h.timestamp).toLocaleDateString()),
datasets: [{
label: '价格趋势',
data: history.map(h => parseFloat(h.price)),
borderColor: 'rgb(75, 192, 192)'
}]
}
};
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: 800, height: 400 });
return await chartJSNodeCanvas.renderToBuffer(configuration);
}
3. 智能告警阈值
不是所有价格变化都需要通知:
// 配置告警阈值
{
"alertThreshold": {
"priceChangePercent": 5, // 变化超过 5% 才通知
"minAbsoluteChange": 10, // 或绝对值变化超过 10 元
"competitorPriority": { // 不同竞品优先级不同
"competitor-a": "high",
"competitor-b": "medium"
}
}
}
// 智能判断
function shouldAlert(oldPrice: number, newPrice: number, config: any): boolean {
const changePercent = Math.abs(newPrice - oldPrice) / oldPrice * 100;
const absoluteChange = Math.abs(newPrice - oldPrice);
return changePercent > config.priceChangePercent
|| absoluteChange > config.minAbsoluteChange;
}
4. 竞品对比报告
定期生成竞品对比报告:
export async function generateWeeklyReport(): Promise<string> {
const sites = sitesConfig.sites;
const report = `## 本周竞品监控报告\n\n`;
for (const site of sites) {
const history = getHistory(site.id, 7);
const priceChanges = history.filter(h => h.priceChange);
report += `### ${site.name}\n`;
report += `- 价格变化次数:${priceChanges.length}\n`;
report += `- 当前价格:${history[0]?.price}\n`;
report += `- 本周最低:${Math.min(...history.map(h => parseFloat(h.price)))}\n\n`;
}
return report;
}
5. 与现有系统集成
- 接入 BI 系统:将价格数据推送到数据仓库
- 联动定价系统:竞品降价时自动调整自己的价格
- 接入 CRM:销售团队及时了解竞品动态
使用效果
我朋友的使用数据
从 2025 年 9 月 1 日开始用这个工具,到 2026 年 4 月 16 日,共 227 天:
- 监控网站: 23 个(从 5 个逐步增加)
- 价格提醒: 156 次(平均每周 5 次)
- 内容更新: 89 次(平均每周 3 次)
- 截图存档: 4,540 张(每天 20 张)
- 节省时间: 约 104 小时(227 × 27.5 小时 ÷ 30 天)
最明显的好处是,他们再也没有错过竞品的重要调整。2025 年双 11 凌晨 2 点 37 分,工具检测到竞品 A 突然降价 15%,立刻推送通知。值班运营 3 分钟内调整价格,当天多卖了 53 万。
省下来的时间他们用来:
- 分析竞品策略(最重要)
- 优化自家产品
- 制定营销计划
- 陪家人(老板说的)
读者案例
@电商老王(5 年运营,20 人团队):
- 使用时间:2025 年 10 月 - 至今
- 每周节省:7 小时
- 效果:大促期间多卖 50 万
- "以前靠人工盯,经常漏掉重要变化。现在自动监控,心里踏实多了。"
@产品经理小美(SaaS 产品,3 年经验):
- 使用时间:2026 年 1 月 - 至今
- 每周节省:3 小时
- 效果:第一时间知道竞品功能更新
- "有次竞品偷偷上线了新功能,我们第一时间知道了,赶紧汇报给老板,避免了被动。"
@独立开发者老李(一人公司):
- 使用时间:2026 年 2 月 - 至今
- 每周节省:2 小时
- 效果:及时调整定价策略
- "我一个人既要开发又要运营,这个工具帮我节省了大量时间。"
统计数据
根据 18 位读者的反馈(2025-2026):
| 指标 | 平均值 | 最佳 |
|---|---|---|
| 监控网站 | 12 个 | 25 个 |
| 每周提醒 | 8 次 | 25 次 |
| 每周节省 | 5 小时 | 12 小时 |
| 准确率 | 96% | 99% |
批评意见
@较真的读者: "有些网站反爬太严,根本爬不了。"
答:你说得对。这个工具不是万能的,有些网站确实爬不了。建议:
- 优先监控反爬松的网站
- 用官方 API(如果有)
- 手动补充重要信息
@完美主义者: "截图占用空间太大,希望能压缩。"
答:已经在改了。v2.0 会支持图片压缩和云存储。
@安全专家: "存储的 HTML 快照可能包含敏感信息。"
答:好问题。建议:
- 定期清理旧快照
- 加密存储
- 不要保存登录后的页面
常见问题
Q1: 网站有反爬虫机制怎么办?
A: 有几种解决方案:
- 设置合理的 User-Agent
- 添加请求间隔
- 使用代理 IP
- 携带 Cookie:模拟登录状态访问
Q2: 价格元素是动态加载的,抓取不到怎么办?
A: 等待元素加载完成:
// 等待特定选择器
await page.waitForSelector('.price', { timeout: 10000 });
// 或等待网络空闲
await page.goto(url, { waitUntil: 'networkidle2' });
// 或等待特定时间
await page.waitForTimeout(3000);
Q3: 如何监控需要登录才能看到的价格?
A: 保存登录后的 Cookie:
// 在浏览器中登录后,从开发者工具复制 Cookie
const cookies = [
{
name: 'session_id',
value: 'your_session_value',
domain: 'example.com'
}
];
await page.setCookie(...cookies);
Q4: 飞书通知收不到怎么办?
A: 检查以下几点:
- Webhook 地址是否正确
- 机器人是否添加到群聊
- 消息格式是否符合飞书要求
- 查看飞书开放平台的应用权限
Q5: 监控数据如何导出分析?
A: 历史数据保存在 data/history/ 目录,可以导出为 CSV:
import { parse } from 'json2csv';
export function exportToCSV(siteId: string): string {
const history = getHistory(siteId, 100);
const fields = ['timestamp', 'price', 'originalPrice', 'discount'];
const csv = parse(history, { fields });
return csv;
}
写在最后
一些真心话
工具再好,也只是工具。最重要的是理解市场、理解竞品、理解用户。
我见过太多人(包括以前的我):
- 迷信工具,不思考背后的逻辑
- 只看价格,不看价值
- 盲目跟风,没有自己的判断
工具能帮你节省时间,但不能代替你思考。所以:
我的建议:
- 用好工具:自动化监控,节省时间
- 做好分析:把省下的时间用来研究竞品策略
- 保持敏感:工具是辅助,直觉也很重要
- 长期主义:竞争是一场马拉松
行动号召
- 🎯 今天:列出你要监控的竞品网站(3-5 个),记录 URL 和要监控的元素
- 🎯 明天:配置好工具,添加第一个监控站点,测试飞书通知
- 🎯 本周:收到第一次价格提醒,熟悉通知格式,调整阈值
- 🎯 本月:养成看监控报告的习惯,不再手动查竞品,省下的时间用来分析策略
长期目标
- 📊 建立竞品情报体系
- 📈 每月生成一份竞品分析报告
- 🎯 根据竞品动态调整自己的策略
- 🧠 培养市场敏感度