简介:这是一套面向Python开发者与自动化运维爱好者的闲鱼智能监控分析系统,解决二手商品实时追踪与AI精准筛选难题,适用于电商比价、抢购监控、市场调研等场景。资源包共39个文件,含11个核心Python脚本(如web_server.py、scraper.py、ai_handler.py)、2个Docker配置文件(docker-compose.yaml、Dockerfile)、4个图文说明(含教程.docx、README.md及PNG/JPG示意图)、以及prompt配置、环境模板和Web前端资源(HTML/JS/CSS),整体12.31MB,结构清晰,模块职责分明。已有197人学习下载,可直接获取完整可运行的Playwright+LLM多任务监控方案,包含开箱即用的Web管理界面、自然语言创建任务能力、多模态AI分析逻辑、企业微信/Bark即时推送集成及反爬增强策略,是研究AI驱动爬虫工程化落地的优质实践样本。
1. 闲鱼智能监控机器人:不是爬虫,是任务驱动的轻量级状态感知系统
你有没有试过在闲鱼上挂了20个商品,每天手动刷新、截图比价、看谁私聊、记下谁砍价到什么程度?我做过三个月——最后发现,真正拖垮效率的不是上架动作,而是「信息滞后」:买家已问价,你两小时后才看到;竞品悄悄降价30元,你还在按原价标;甚至有人批量下单又秒退,你连预警都没收到。这不是运营问题,是状态盲区。闲鱼智能监控机器人,本质不是模拟人工点击的“自动化脚本”,而是一套围绕「任务目标」构建的轻量级状态感知系统:它不追求全站抓取,只盯住你定义的「关键对象」(某商品ID、某用户昵称、某关键词对话),在变化发生时触发结构化记录+分级通知。适合中小卖家、二手数码工作室、学生兼职代售群体——不需要Python高手,但得会配规则、看日志、调阈值。它解决的不是“能不能自动点”,而是“变化发生时,我能不能在5秒内知道它发生了什么、影响有多大”。
2. 用 Puppeteer + Node.js 搭建最小可行监控骨架:从登录到目标页加载
闲鱼页面动态渲染强、反爬逻辑嵌在 JS 中,传统 requests + BeautifulSoup 会卡在登录态维持和 DOM 渲染上。Puppeteer 是目前最稳的落地选择:它复用 Chromium 实例,能真实执行 JS、处理滑块验证(虽闲鱼当前未强制,但预留能力)、保持 Cookie 会话。我们不追求高并发,先跑通单任务闭环。
2.1 环境初始化与基础依赖安装
mkdir xianyu-monitor && cd xianyu-monitor npm init -y npm install puppeteer dotenv chalk提示:
puppeteer默认下载 Chromium,国内网络可能慢。若失败,可提前设置镜像源:export PUPPETEER_DOWNLOAD_HOST=https://npmmirror.com/mirrors(Linux/macOS)或set PUPPETEER_DOWNLOAD_HOST=https://npmmirror.com/mirrors(Windows CMD)
2.2 登录态持久化:用 userDataDir 避免重复扫码
闲鱼登录依赖手机扫码,但扫码只需一次。关键在复用浏览器上下文:
const puppeteer = require('puppeteer'); const path = require('path'); // 指向固定 userDataDir,保存登录态 const userDataDir = path.join(__dirname, 'chrome-user-data'); (async () => { const browser = await puppeteer.launch({ headless: false, // 初次调试务必设为 false,观察登录流程 userDataDir, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage' ] }); const page = await browser.newPage(); await page.goto('https://www.xianyu.com/'); // 等待「我的」按钮出现,证明已登录 try { await page.waitForSelector('a[href="/my"]', { timeout: 10000 }); console.log('✅ 已检测到登录态,跳过扫码'); } catch (e) { console.log('⚠️ 未检测到登录态,请手动扫码登录,完成后按回车继续...'); await page.waitForTimeout(5000); await page.waitForNavigation({ waitUntil: 'networkidle0' }); } // 保存当前页为首页快照(用于后续对比) await page.screenshot({ path: 'home-loaded.png' }); console.log('📸 首页已加载并截图'); await browser.close(); })();逻辑说明:
userDataDir是核心——它把整个浏览器配置(含 Cookie、LocalStorage、缓存)存到本地文件夹,下次启动直接复用,无需重复扫码。headless: false仅限首次运行,必须亲眼确认扫码成功、页面跳转正常;后续可改为true节省资源。waitForSelector('a[href="/my"]')是闲鱼登录成功的稳定标志(未登录时该链接不存在),比等某个文字更可靠。
2.3 目标页精准加载:绕过首页重定向,直击商品/聊天页
闲鱼 URL 有重定向链(如xianyu.com/item/xxx→xianyu.com/detail/xxx),直接goto可能超时。正确做法是:先访问通用入口,再用page.evaluate注入 JS 触发跳转:
// 加载指定商品页(ID: 123456789) const itemId = '123456789'; await page.goto('https://www.xianyu.com/', { waitUntil: 'networkidle0' }); // 执行 JS 跳转,避免重定向等待 await page.evaluate((id) => { window.location.href = `https://www.xianyu.com/detail/${id}`; }, itemId); // 等待商品标题出现(防白屏) await page.waitForSelector('.detail-title', { timeout: 15000 }); console.log('✅ 商品页加载完成');参数说明:
timeout: 15000必须设——闲鱼商品页首屏渲染慢,尤其带视频时,10秒常不够。.detail-title是商品页主标题的稳定 class(经 2024 年 6 月实测),比h1或#title更抗改。- 不要用
page.goto('https://www.xianyu.com/detail/xxx')直接跳,易因 Referer 或 UA 被拦截返回首页。
3. 定义监控任务:用 JSON 规则引擎驱动差异化采集逻辑
监控不是“所有东西都抓”,而是“按需感知”。我们设计一个轻量 JSON 规则引擎,支持三类任务:商品价格/库存变动、私信关键词触发、用户行为轨迹(如某人连续访问你3个商品)。规则文件tasks.json结构如下:
[ { "id": "item_price_watch", "type": "item", "target": "123456789", "fields": ["price", "sold_count", "fav_count"], "notify": ["dingtalk", "email"], "threshold": {"price_change_percent": 5} }, { "id": "chat_keyword_alert", "type": "chat", "target": "张三", "keywords": ["砍价", "包邮", "急要"], "notify": ["weixin"] } ]3.1 解析规则并生成对应采集器
const fs = require('fs'); const tasks = JSON.parse(fs.readFileSync('tasks.json', 'utf8')); // 根据 type 分发采集逻辑 tasks.forEach(task => { switch (task.type) { case 'item': setupItemWatcher(task); break; case 'chat': setupChatWatcher(task); break; default: console.warn(`❌ 未知任务类型: ${task.type}`); } }); function setupItemWatcher(task) { console.log(`🔍 启动商品监控: ${task.target}`); // 后续将在此函数中注入商品页DOM提取逻辑 } function setupChatWatcher(task) { console.log(`💬 启动聊天监控: ${task.target} 关键词 ${task.keywords.join(', ')}`); // 后续将在此函数中注入聊天页消息轮询逻辑 }为什么用 JSON 而非代码写死?
- 运营人员可直接改
tasks.json增删任务,无需动 JS; - 支持 Git 版本管理,每次修改留痕;
- 天然兼容配置中心(如 Consul),未来可远程下发规则。
3.2 商品页结构化解析:避开动态 class,锚定语义节点
闲鱼商品页 class 名频繁变更(如price-xxx→price-yyy),但 DOM 层级和文本语义稳定。我们用 XPath + 文本匹配双保险:
async function extractItemData(page, taskId) { const data = { timestamp: new Date().toISOString(), task_id: taskId }; // 价格:找包含“¥”且父级有“价格”文本的 span const priceEl = await page.$x('//span[contains(text(), "¥") and ancestor::div[contains(text(), "价格")]]'); if (priceEl[0]) { data.price = await (await priceEl[0].getProperty('textContent')).jsonValue(); } // 已售:找“已售”后紧跟数字的 span(防“已售12件”和“已售出”混淆) const soldEl = await page.$x('//span[contains(text(), "已售")]/following-sibling::span[1]'); if (soldEl[0]) { const text = await (await soldEl[0].getProperty('textContent')).jsonValue(); data.sold_count = parseInt(text.match(/\d+/)?.[0]) || 0; } // 收藏数:找“收藏”后数字(闲鱼收藏按钮旁显示“xx人收藏”) const favEl = await page.$x('//span[contains(text(), "人收藏")]'); if (favEl[0]) { const text = await (await favEl[0].getProperty('textContent')).jsonValue(); data.fav_count = parseInt(text.match(/\d+/)?.[0]) || 0; } return data; }关键技巧:
$x()用 XPath 比$$()用 CSS 更抗 class 变更;contains(text(), "...")比text()="..."更容错(防空格、换行);following-sibling::span[1]精准定位相邻元素,避免误抓其他“已售”文案。
4. 状态比对与变更判定:用差分算法识别真实业务变动
监控的价值不在“抓到数据”,而在“判断是否值得通知”。比如价格从 ¥299 → ¥298.5,对二手交易几乎无意义;但“库存从 1 → 0”就是紧急信号。我们设计三层判定逻辑:
4.1 基础层:字段级数值差分
function diffFields(prev, curr, thresholds = {}) { const changes = []; const allKeys = [...new Set([...Object.keys(prev), ...Object.keys(curr)])]; allKeys.forEach(key => { if (key === 'timestamp' || key === 'task_id') return; const prevVal = prev[key]; const currVal = curr[key]; let isChanged = false; let reason = ''; if (typeof prevVal === 'number' && typeof currVal === 'number') { // 数值型:支持绝对差值和百分比阈值 const absDiff = Math.abs(currVal - prevVal); const percentDiff = prevVal ? (absDiff / prevVal * 100) : 0; if (thresholds[`${key}_change_abs`] && absDiff >= thresholds[`${key}_change_abs`]) { isChanged = true; reason = `绝对变化 ${absDiff} ≥ 阈值 ${thresholds[`${key}_change_abs`]}`; } else if (thresholds[`${key}_change_percent`] && percentDiff >= thresholds[`${key}_change_percent`]) { isChanged = true; reason = `百分比变化 ${percentDiff.toFixed(1)}% ≥ 阈值 ${thresholds[`${key}_change_percent`]}%`; } } else if (typeof prevVal === 'string' && typeof currVal === 'string') { // 字符串型:仅当完全不同时标记(如标题改写、描述新增) if (prevVal !== currVal) { isChanged = true; reason = `文本变更:"${prevVal}" → "${currVal}"`; } } if (isChanged) { changes.push({ field: key, prev: prevVal, curr: currVal, reason }); } }); return changes; }参数说明:
thresholds来自tasks.json的threshold字段,支持 per-field 配置;percentDiff计算时加prevVal ? ... : 0防除零错误;- 字符串比较不做模糊匹配(如 Levenshtein),因闲鱼标题/描述改动通常具业务意义。
4.2 业务层:组合规则过滤噪音
单纯数值变未必是业务事件。例如:
- 价格微调 + 库存减1 → 可能是买家拍下未付款,不通知;
- 价格降10% + 收藏增50 → 可能是主动促销,高优先级通知。
我们用规则表驱动:
| 触发条件 | 通知级别 | 动作 |
|---|---|---|
price_change_percent ≥ 5 AND sold_count > 0 | 紧急 | DingTalk + 邮件 |
sold_count === 0 AND fav_count > 100 | 高 | 微信模板消息 |
price_change_abs ≤ 1 AND sold_count === 0 | 低 | 仅记录日志 |
function applyBusinessRules(changes, task) { const rules = [ { condition: (c) => c.some(x => x.field === 'price' && x.curr < x.prev * 0.95 && changes.find(y => y.field === 'sold_count')?.curr > 0), level: 'urgent', action: ['dingtalk', 'email'] }, { condition: (c) => c.some(x => x.field === 'sold_count' && x.curr === 0) && changes.some(x => x.field === 'fav_count' && x.curr > 100), level: 'high', action: ['weixin'] } ]; for (const rule of rules) { if (rule.condition(changes)) { return { level: rule.level, actions: rule.action }; } } return { level: 'low', actions: [] }; }5. 避坑指南:闲鱼监控中 5 个血泪经验换来的真问题
闲鱼页面结构、反爬策略、网络环境高度不稳定。以下问题均来自真实部署(2023 Q4–2024 Q2,覆盖 12 个不同账号、3 类网络环境):
5.1 现象:首次扫码登录后,第二天再次运行报“登录态失效”,但手动打开 Chrome 用同一 userDataDir 却正常
原因:闲鱼服务端校验User-Agent+Accept-Language组合指纹。Puppeteer 默认 UA 是Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...,而真实手机 Chrome 是Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15...。服务端认为“同一设备突然换了 UA”,强制登出。
解决:在puppeteer.launch()中固定 UA 和语言:
await page.setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.47(0x18002f33) NetType/WIFI Language/zh_CN'); await page.evaluate(() => { Object.defineProperty(navigator, 'language', { value: 'zh-CN' }); });5.2 现象:商品页waitForSelector('.detail-title')超时,但页面明明已渲染
原因:闲鱼启用「懒加载」,.detail-title在首屏 DOM 中存在,但被display: none或visibility: hidden隐藏,直到滚动触发。waitForSelector默认不检测隐藏元素。
解决:改用page.waitForFunction检测元素存在且可见:
await page.waitForFunction(() => { const el = document.querySelector('.detail-title'); return el && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility === 'visible'; }, { timeout: 15000 });5.3 现象:聊天页监控时,新消息 DOM 插入后,page.$x('//div[@class="msg-item"]')总是抓到旧消息,新消息需等 2 秒才出现在结果里
原因:闲鱼聊天消息使用MutationObserver动态插入,但 Puppeteer 的$$方法是快照式查询,不监听后续变化。
解决:用page.waitForSelector等待新消息容器出现,再提取:
// 等待新消息块(含时间戳)出现 await page.waitForSelector('div.msg-item[data-timestamp]', { timeout: 5000 }); // 再查全部消息 const messages = await page.$$eval('div.msg-item[data-timestamp]', els => els.map(el => ({ time: el.getAttribute('data-timestamp'), text: el.querySelector('.msg-content')?.innerText || '' })) );5.4 现象:同一台机器跑多个监控实例(不同 userDataDir),内存占用飙升至 4GB+,CPU 持续 90%
原因:Puppeteer 默认每个browser实例开独立 Chromium 进程。5 个任务 = 5 个 Chromium = 资源爆炸。
解决:所有任务复用同一个browser实例,用browser.newPage()创建独立 page:
const browser = await puppeteer.launch({ ... }); // 全局单例 // 每个任务用: const page = await browser.newPage(); // 任务结束调用: await page.close(); // 不关 browser!5.5 现象:DingTalk 通知发送成功,但手机端收不到,PC 端却正常
原因:闲鱼监控脚本运行在服务器(如阿里云 ECS),其公网 IP 被钉钉服务端标记为“高风险来源”,默认屏蔽移动端推送。
解决:改用钉钉「自定义机器人」的 Webhook 发送,并在安全设置中添加服务器 IP 白名单;或改用企业微信应用(对企业微信,IP 白名单策略更宽松)。
6. 进阶技巧:用 SQLite 做本地状态中枢,实现跨天趋势分析与自动归档
纯内存比对只能看“这次 vs 上次”,但运营需要“过去7天价格走势”“某用户访问频次热力图”。我们引入 SQLite 作为轻量状态中枢,不依赖外部数据库,单文件搞定。
6.1 设计状态表结构:兼顾查询效率与扩展性
-- 任务元数据表(记录任务配置快照) CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, config TEXT NOT NULL, -- JSON string created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 状态快照表(每次采集存一行) CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, timestamp TEXT NOT NULL, -- ISO8601 data TEXT NOT NULL, -- JSON string of extracted fields FOREIGN KEY (task_id) REFERENCES tasks(id) ); -- 变更记录表(只存有业务意义的变更) CREATE TABLE IF NOT EXISTS changes ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_id INTEGER NOT NULL, field TEXT NOT NULL, prev_value TEXT, curr_value TEXT, level TEXT CHECK(level IN ('low','medium','high','urgent')), notified BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) );6.2 自动归档策略:防止 SQLite 文件无限膨胀
闲鱼商品页每10分钟采一次,一天144条,30天即4320行。我们设定自动归档规则:
snapshots表保留最近 7 天;changes表保留最近 30 天;- 归档数据压缩为 ZIP 存
archive/202406/目录,文件名含日期哈希。
const sqlite3 = require('sqlite3').verbose(); const db = new sqlite3.Database('./monitor.db'); function autoArchive() { const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; // 归档 snapshots db.run(`DELETE FROM snapshots WHERE timestamp < ?`, [sevenDaysAgo]); // 归档 changes db.run(`DELETE FROM changes WHERE created_at < ?`, [thirtyDaysAgo]); }6.3 用 SQL 实现趋势分析:不用导出 Excel,命令行直接出结论
比如查“商品ID 123456789 过去24小时价格波动区间”:
SELECT MIN(JSON_EXTRACT(data, '$.price')) as min_price, MAX(JSON_EXTRACT(data, '$.price')) as max_price, COUNT(*) as total_samples, ROUND(AVG(JSON_EXTRACT(data, '$.price')), 2) as avg_price FROM snapshots WHERE task_id = 'item_price_watch' AND timestamp >= datetime('now', '-24 hours');输出:
min_price = "¥289.00" max_price = "¥299.00" total_samples = 144 avg_price = 294.33注意:SQLite 3.38+ 支持
JSON_EXTRACT,若版本低需升级或改用json1扩展。
6.4 我的习惯:每天早10点自动邮件发《昨日监控简报》
用 Node.js 调nodemailer+sqlite3查询,生成 Markdown 邮件正文:
// daily-report.js const nodemailer = require('nodemailer'); const db = new sqlite3.Database('./monitor.db'); db.all(` SELECT t.id as task_id, s.timestamp, JSON_EXTRACT(s.data, '$.price') as price, c.level, c.field FROM changes c JOIN snapshots s ON c.snapshot_id = s.id JOIN tasks t ON s.task_id = t.id WHERE s.timestamp >= datetime('now', '-1 days') ORDER BY s.timestamp DESC LIMIT 20 `, (err, rows) => { if (err) throw err; const html = ` <h2>📊 闲鱼监控日报(${new Date().toLocaleDateString()})</h2> <table><tr><th>任务</th><th>时间</th><th>字段</th><th>级别</th><th>价格</th></tr> ${rows.map(r => ` <tr> <td>${r.task_id}</td> <td>${r.timestamp.split('T')[1].slice(0,5)}</td> <td>${r.field}</td> <td><b>${r.level}</b></td> <td>${r.price || '-'}</td> </tr> `).join('')} </table> `; // 发送邮件... });这套机制让我彻底告别“翻日志查昨天谁砍价”的手动操作。现在每天睁眼第一件事,是看这封邮件——它不告诉我“发生了什么”,而是告诉我“哪些变化值得我今天花10分钟处理”。
希望帮到你。
本文还有配套的精品资源,点击获取