简介这是一套面向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人学习下载可直接获取完整可运行的PlaywrightLLM多任务监控方案包含开箱即用的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_HOSThttps://npmmirror.com/mirrorsLinux/macOS或set PUPPETEER_DOWNLOAD_HOSThttps://npmmirror.com/mirrorsWindows CMD2.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-AgentAccept-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[classmsg-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内存占用飙升至 4GBCPU 持续 90%原因Puppeteer 默认每个browser实例开独立 Chromium 进程。5 个任务 5 个 Chromium 资源爆炸。解决所有任务复用同一个browser实例用browser.newPage()创建独立 pageconst browser await puppeteer.launch({ ... }); // 全局单例 // 每个任务用 const page await browser.newPage(); // 任务结束调用 await page.close(); // 不关 browser5.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 调nodemailersqlite3查询生成 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 tabletrth任务/thth时间/thth字段/thth级别/thth价格/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 tdb${r.level}/b/td td${r.price || -}/td /tr ).join()} /table ; // 发送邮件... });这套机制让我彻底告别“翻日志查昨天谁砍价”的手动操作。现在每天睁眼第一件事是看这封邮件——它不告诉我“发生了什么”而是告诉我“哪些变化值得我今天花10分钟处理”。希望帮到你。本文还有配套的精品资源点击获取
