ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python京东价格监控系统实战:爬虫、SQLite与定时提醒

Python京东价格监控系统实战:爬虫、SQLite与定时提醒 简介基于Python的京东价格监控系统完整源码面向有商品比价需求的Python学习者和开发者解决手动查看价格信息滞后、无法及时决策的痛点。系统整合Requests与Selenium两种爬取方式支持通过JS接口或页面渲染获取价格内置Sqlite/MySQL存储、免费代理池以及邮件提醒模块可实现自定义商品监控和品类降价订阅七折提醒等实用功能。资源包共23个文件压缩后大小506KB其中10个.py脚本为主要功能实现7张PNG截图展示邮件通知、页面效果和数据库结构等运行界面另有README/Markdown说明文档、配置文件与LICENSE协议便于快速理解项目结构与二次开发。源码附带建库脚本、代理配置和邮箱检测示例从爬取、存储到邮件提醒形成完整闭环。目前已有65人学习适合作为Python爬虫与自动化办公的实践参考。1. 京东价格监控系统源码这个压缩包解决的是“人肉蹲价格”的疲劳很多人蹲京东价格蹲在“手动刷新——看到降价——犹豫一下——错过”的循环里。这个标题给的 zip 是一个用 Python 写好的京东价格监控系统定时抓取你指定商品的页面价格写进本地数据库跌穿目标价或者出现历史新低时自动通过邮件或 Server酱把价格变化推给你。它的核心价值不是爬虫本身而是把“盯盘”从人肉定时器变成一条能跑几个月的后台任务顺带把 requests、SQLite、定时调度、消息推送这一整条链路走通。适合两类人一类是真的在蹲某件大件商品的价格另一类是想看一个简单爬虫项目如何落到工程结构和异常处理上。下面从解压这份 zip 开始把整条链路拆开讲。2. 先把源码跑起来Python 环境、zip 解压与项目结构核对拿到手的是一个 zip 压缩包别急着写爬虫先把环境对齐。这一节就三板斧解压、装 Python 依赖、对照项目结构找到配置入口。很多新手翻车就翻在第一步——不看压缩包里有啥就开始装依赖结果版本对不上、目录找不到白白浪费时间。2.1 解压 zip 的三种姿势与伪加密识别Windows 下直接右键“解压到当前文件夹”即可我习惯用 7-Zip它会更明确地显示压缩包内部结构遇到有问题的包报错也更清楚。Linux 下用 unzip命令很简单unzip jd-price-monitor.zip -d jd-price-monitor cd jd-price-monitor-d 指定解压目标目录避免把一堆文件散落在当前目录里如果只想看压缩包内容而不实际解压用 unzip -l jd-price-monitor.zip 先列一遍。部分精简系统没装 unzipDebian/Ubuntu 用 apt install unzipCentOS 用 yum install unzip 补上。这是 linux 解压缩命令 zip 的日常用法不需要记更多参数解压和列目录这两个够用。需要注意网上流传的源码包偶尔会遇到 zip 伪加密7-Zip 打开提示需要密码但这份包其实根本没设密码。原因是打包工具把压缩文件头里的加密标志位置了 1文件数据本身没有被加密。遇到这种包直接输入空密码回车往往就能解开。想确认是不是伪加密可以用 Python 检查一下加密标志位import zipfile with zipfile.ZipFile(jd-price-monitor.zip) as zf: for info in zf.infolist(): # flag_bits 第 0 位为 1 表示加密标志被置位 print(info.filename, bool(info.flag_bits 0x1))如果打印出来的标志全是 True而你又确定这份源码不带密码那基本就是伪加密把内容解出来照常使用。这个检查脚本也可以顺便核对 zip 里到底有哪些文件防止解压完发现目录结构和预想的不一样。解压后第一件事不是读代码而是看有没有 README 和 requirements.txt。作者通常会把启动步骤写在 README 里先花 5 分钟读一遍能省下后面两个小时的排查。如果解压后发现没有 requirements.txt也没有 main.py可能下错了包或者压缩包被二次打包过这时优先对照压缩包内部的目录和文件名不要硬套别人的项目结构。2.2 Python 解释器与依赖安装从裸环境到能跑 main.py这类监控项目对 Python 版本要求不高3.8 以上就能跑建议直接用 3.10 或 3.11遇到 f-string、类型注解时更舒服。Windows 上按 python 安装教程装解释器的时候记得勾选 Add Python to PATH否则后续 python 和 pip 命令都找不到这是最容易被忽略的一步。装完验证一下环境命令行里输入python --version pip --version两条命令都能正常输出版本号说明解释器就绪。接下来是依赖。这份源码对外部库的依赖通常很克制主要就是 requestssqlite3、smtplib、datetime 这些全是 Python 标准库。这种设计我很喜欢部署到新机器时只需要装一个包。先建虚拟环境再装依赖避免污染全局环境python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/macOS 激活 source venv/bin/activate pip install -r requirements.txtrequirements.txt 里如果只有 requests 一行说明作者把第三方依赖控制得很好如果列了很多先分辨哪些是可视化、哪些是调度用的再决定要不要全装。pip 下载慢时可以在命令后加 -i 指定镜像源比如清华 PyPI 镜像速度会快很多但镜像地址要写对否则会报 404。用 VSCode 打开这个项目时注意把解释器指到 venv 里的 Python。按 CtrlShiftP 打开命令面板搜 Python: Select Interpreter选 venv 目录下那个解释器。VSCode python 环境配置这块选错解释器会导致 requests 明明装过却报 ModuleNotFoundError实际不是没装是没选对。2.3 项目结构核对先找到 config.py 再谈别的这类源码包解压后一般长这样jd-price-monitor/ ├── config.py # SKU 列表、目标价、抓取间隔 ├── fetcher.py # 请求价格接口、解析价格 ├── storage.py # SQLite 建表、写入、查询 ├── notifier.py # 邮件 / Server酱 / Webhook 提醒 ├── main.py # 主循环抓取 → 入库 → 判断 → 提醒 └── requirements.txt文件命名可能不完全一样但职责划分大同小异config 管配置fetcher 管网络storage 管数据notifier 管推送main 串联全局。拿到源码包后我的习惯是先打开 config.py把要监控的商品 SKU 和 target_price 改好再跑 main.py 看日志而不是先去改 fetcher 里的请求逻辑那是后面调优阶段的事。验证项目能不能被正确导入可以执行一条命令试探python -c from fetcher import fetch_price; print(fetch_price(100012043978))如果输出一个包含价格字段的 dict说明依赖、导入路径和网络链路都通了如果报 SSL 相关错误多半是机器上证书链不完整或用的是特殊网络环境。到这一步整个项目的基础运行条件就具备下面开始逐层讲核心逻辑。3. 抓京东价格数据从商品 URL 到一个干净价格值的完整链路京东价格监控的核心动作只有一个拿到商品 SKU请求价格接口解析出当前到手价。这一章把这条链路拆开每一步讲清楚为什么这么写、参数改哪里、失败时看什么新手可以照着敲熟手也能对照校验自己的写法。3.1 先搞清楚 SKU 和商品 URL 的关系京东商品详情页长这样https://item.jd.com/100012043978.html URL 末尾那串数字就是 SKU它是商品的唯一标识也是后续所有请求的核心参数。注意一个商品可能对应多个 SKU比如手机的不同颜色、不同存储版本各有独立 SKU 和独立价格监控之前先确认你盯的是哪一个。在 config.py 里把要监控的商品整理成列表我一般这样写WATCH_LIST [ { sku: 100012043978, name: 某品牌手机 256G, target_price: 2799.0, }, { sku: 100012043979, name: 某品牌手机 512G, target_price: 3299.0, }, ]name 字段看起来只是给日志看的实际在提醒文案里非常有用否则你收到一封邮件只写 SKU 100012043978 降价了还得去查是哪个商品。target_price 是阈值判断的依据后面第 4 章会用到。SKU 列表控制在 20 个以内是性价比最高的监控规模。3.2 用 requests 拉价格接口UA 与 Referer 缺一不可如果你直接 requests.get 商品页 HTML会发现页面里找不到价格。原因很直接详情页的价格是 JS 异步加载的HTML 骨架里根本没有。常见做法是请求专门的价格接口比如 p.3.cn/prices/mgets这个接口只要传 SKU 就能返回价格 JSON轻量又准确。import requests PRICE_API https://p.3.cn/prices/mgets def fetch_price(sku: str) - dict: params {skuIds: fJ_{sku}} headers { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ), Referer: fhttps://item.jd.com/{sku}.html, } resp requests.get(PRICE_API, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()[0]两个关键点params 里的 skuIds 必须带 J_ 前缀这是接口的历史约定headers 里的 User-Agent 和 Referer 是基本伪装UA 模拟浏览器是为了不被当成异常请求Referer 填商品页地址则是因为页面内调用这个接口时本来就会带上该来源缺了它接口可能返回空列表。timeout10 意味着一轮请求超过 10 秒直接失败这个参数能避免一次网络抖动把整个监控线程挂死具体原因放到第 5 章讲。3.3 从返回 JSON 里挑对字段p、op、m 的优先级接口返回的是一个列表列表里每个元素对应一个 SKU返回格式长这样[ { id: J_100012043978, p: 2799.00, op: 3299.00, m: 4499.00 } ]三个字段含义容易混这里用一张表说清楚字段含义监控中的用途p当前到手价叠加促销、满减后首选价格来源op划线原价p 为空时的兜底m市场参考价 / 吊牌价最后兜底一般不用于判断做降价监控真正要的是 p它是促销叠加后用户实际会付的钱。但 p 在部分场景下会返回空字符串比如促销信息未生成时所以解析时要按优先级做兜底不能只取一个字段def parse_price(item: dict): for key in (p, op, m): raw item.get(key) try: price float(raw) if price 0: return price except (TypeError, ValueError): continue return None逻辑是按 p、op、m 的顺序取第一个能被 float() 转换且大于 0 的值转换失败就跳到下一个字段。这样做只有一个目的宁可这一轮抓不到价格也绝不能把 0 写进数据库。0 一旦入库后面算历史最低价时整个统计就废了这是监控项目里代价最高的隐蔽错误。3.4 频率控制与失败重试把“礼貌”写进代码里监控不是请求一次就结束而是每天几十次、连续跑几个月。对京东这种体量的站点个人家庭带宽去请求价格接口把频率控制在合理范围基本不会遇到异常真正出问题的都是自己节奏没把握好。我的常规做法是每轮抓取完随机等 15 到 30 分钟再跑下一轮import random import time def wait_a_bit(): # 900~1800 秒也就是 15~30 分钟 delay random.uniform(900, 1800) time.sleep(delay)用 random 而不是固定 sleep 1800是为了避免每次请求都落在整点前后行为上更像人工浏览的节奏。网络请求一定要做失败重试但重试要有上限和退避间隔否则接口一抖动就开始无限重试反而给对方服务器增加压力def fetch_with_retry(sku: str, retries: int 3): for i in range(retries): try: return fetch_price(sku) except (requests.RequestException, IndexError, KeyError): # 退避间隔5 秒 / 10 秒 / 15 秒 time.sleep(5 * (i 1)) return None捕获的三类异常对应三种不同情况RequestException 是网络层问题包括超时、连接重置等IndexError 说明接口返回了空列表KeyError 表示返回结构中缺少预期字段。重试 3 次仍失败就返回 None由上层决定跳过这一轮整个脚本不会因为单次失败而崩溃。第 4 章会把抓取、存储、判断串成完整的主循环。4. 价格存进 SQLite 再谈降价表结构、历史最低与阈值判断抓到价格只是第一步价格监控的另一半在“历史”。没有历史数据你只知道现在多少钱不知道它是不是跌到近三个月的最低点。这一章讲存储选型、表结构设计和降价判断的三段式规则把这部分写完一个能产出判断结果的最小闭环就成型了。4.1 为什么是 SQLite而不是 CSV 或 Excel很多初学者习惯先写 CSV一行追加一条价格听起来省事。但 CSV 有两个硬伤第一多个进程或线程写入时会互相抢文件锁日志和数据混在一起很难排查第二查询历史趋势得把整个文件读回来自己算类型还得手工转。SQLite 是 Python 标准库自带的模块不需要安装额外的数据库服务一个 .db 文件搞定全部数据几万条记录对它来说毫无压力。选 SQLite 的工程理由很明确事务、索引、SQL 查询。等你哪天想知道“这个月最低价出现在哪一天”一条 SQL 就查出来了不用写循环里的 if 比较。而且个人监控这种单机小应用根本不需要上 MySQL省去安装、初始化、鉴权一整套运维成本这才是工具边界感。这里我坚持直接用 sqlite3 裸 SQL不引入 ORM。原因很简单项目就两张表几十行 SQL 就能覆盖全部需求引入 ORM 反而多一层学习和排错成本。python 基础语法里的列表、字典、函数用熟就能看懂这套代码。4.2 price_history 表的结构设计与建表语句表设计不追求复杂四个字段足够自增主键、SKU、价格、抓取时间。注意我给 sku 和 fetched_at 加了联合索引因为监控查询的模式基本就是“按 SKU 取一段完整历史”这个索引能让查询稳定走索引路径虽然几千条数据感知不到差距但习惯是好的。import sqlite3 def init_db(path: str prices.db) - sqlite3.Connection: conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS price_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku TEXT NOT NULL, price REAL NOT NULL, fetched_at TEXT NOT NULL ) ) conn.execute( CREATE INDEX IF NOT EXISTS idx_sku_time ON price_history(sku, fetched_at) ) return conn两个 execute 不能合并成一句sqlite3 的 execute 一次只执行一条语句这是新手容易踩的点。fetched_at 存的是“2024-05-20 08:00:00”这种 ISO 格式字符串它有一个好处字典序和真实时间顺序一致所以 ORDER BY fetched_at 可以直接用不需要先解析成时间对象。写入时用参数化查询from datetime import datetime def insert_price(conn: sqlite3.Connection, sku: str, price: float): ts datetime.now().strftime(%Y-%m-%d %H:%M:%S) conn.execute( INSERT INTO price_history (sku, price, fetched_at) VALUES (?, ?, ?), (sku, price, ts), ) conn.commit()参数占位符 ? 而不是拼 SQL 字符串一方面防止 SQL 注入风险另一方面让 sqlite3 帮你处理类型转换避免数字变成字符串存进去。每次写入后 commit 一次对个人监控这个量级没有性能压力还保证进程突然退出时数据不丢。4.3 历史最低查询与提醒判断三段式规则降价判断我按三段式来做把逻辑拆开避免纠缠在一起先查历史最低再比目标价最后比新低价。写成两个函数各管一段def get_history_min(conn: sqlite3.Connection, sku: str): row conn.execute( SELECT MIN(price) FROM price_history WHERE sku ? AND price 0, (sku,), ).fetchone() return row[0] if row and row[0] else None def check_alert(conn: sqlite3.Connection, item: dict, price: float): sku item[sku] target item[target_price] if price target: return target hist_min get_history_min(conn, sku) if hist_min is None or price hist_min: return new_low return Nonecheck_alert 返回三种结果target 表示跌穿你的心理价位new_low 表示创了历史新低None 表示不用打扰你。顺序上先判 target 再判 new_low因为跌穿目标价的同时往往也是新低只报一次能减少打扰。SQL 里的 price 0 条件必须保留不能只靠 Python 层过滤防止早期误入库的 0 值把 MIN 拉低导致误报。这个“0 价污染”是监控项目里最隐蔽的坑之一下一章专门列出排查记录。5. 京东价格监控的 5 个常见坑现象、原因与解决监控脚本和一次性爬虫最大的区别是它要连续跑几周甚至几个月可靠性藏在每一个异常处理细节里。这 5 个坑都是运行中真实存在的问题每条按现象、原因、解决三段写方便你对号入座。5.1 价格变成 0 还入了库把“历史最低”彻底污染现象某天日志里开始频繁出现“新低价 0.0”的提醒数据库里冒出一大片 0 价格之后所有历史最低判断全部失效。原因价格接口的 p 字段在部分促销场景下返回空字符串float() 抛 ValueError 被外层代码捕获后填了 0或者解析函数本身在异常分支里返回了 0而调用方没有拦下来。0 一旦进了 price_historyMIN(price) 永远是 0新低价判断从此失真。解决解析函数只返回大于 0 的值第 3.3 节已经这么做写入前再做一道防御校验if price is None or price 0: 直接跳过这一轮查询历史最低时 SQL 里补上 price 0 条件。三层防护都做因为每一层都可能被后续重构破坏只依赖一层迟早出事。5.2 提醒轰炸同一目标价提醒了十几遍现象价格在目标价附近来回波动每跑一轮就收到一封邮件一晚上攒了十几封最后不得不关掉脚本。原因判断逻辑写成了“价格小于等于目标价就发提醒”这个条件每次抓取都成立每次都会发。价格 2799 比目标价 2800 低 1 块原则上确实满足条件但人不需要被提醒十几遍只需要在被跌穿的那一刻知道一次就够了。解决把“提醒”这件事也落库建一张提醒记录表CREATE TABLE IF NOT EXISTS alert_log ( sku TEXT NOT NULL, alert_type TEXT NOT NULL, price REAL NOT NULL, created_at TEXT NOT NULL, UNIQUE(sku, alert_type) )发送提醒之前先 INSERT OR IGNORE插入成功说明这条提醒从没发过才真正执行通知插不进去说明已经提醒过直接跳过。UNIQUE(sku, alert_type) 是数据库层的去重约束让 SQLite 替你把关。如果想改成“每天最多提醒一次”把唯一键换成 (sku, alert_type, date(created_at)) 即可改动成本很低。5.3 跑一宿就崩没设超时导致请求一直挂着现象脚本放到服务器上跑第二天起来发现进程已经退出数据库里的数据停在昨天晚上某一点之后全是空白。原因最常见的原因是 requests.get 没设 timeout。网络一抖动连接既不成功也不失败一直在那里等整个线程被卡死此时如果线程是被主循环串行调用的后续所有抓取全部阻塞再遇到未捕获异常就直接退出进程。解决所有网络请求统一加 timeout10重试函数捕获 requests.RequestException主循环最外层再包一层 try/except异常时把 traceback 写进日志文件而不是裸退出。跑长任务的脚本日志就是后悔药没有日志的监控脚本等于没有黑匣子崩一次就彻底不知道该查哪里。写好日志文件是长跑项目投入产出比最高的一件事。5.4 SKU 一多就频繁掉线别急着上多线程现象开始只盯 2 个 SKU 时一切正常加到 20 个后发现每轮总有 2 到 3 个请求失败偶尔整轮全部失败重试也无济于事。原因很多人一看到请求慢就想着上线程池并发20 个线程同时打同一个接口被服务端判定为异常访问连接被批量断开。问题不是单请求太慢而是并发太猛触发了一层隐形的频率限制。解决保持串行请求每个请求之间 sleep 0.5 到 1 秒。20 个 SKU 一轮也就多花二十秒左右对 30 分钟的监控间隔来说完全无感。如果确实想加快用 requests.Session 复用底层连接同时把并发数压到 2 到 3而不是随手开一个无界线程池。个人监控场景下串行永远是最稳妥的选择。5.5 时间字段格式不统一画价格曲线时排序翻车现象第 6 章画出来的价格曲线 x 轴乱序走势像过山车完全看不出规律。原因fetched_at 字段在不同版本代码里存过“2024/5/20 8:00”和“2024-05-20 08:00:00”两种格式或者同一格式里月和日没补零字符串按字典序排序时两种风格混在一起就乱了。解决统一用 %Y-%m-%d %H:%M:%S 这一种格式写入查询时按 ORDER BY fetched_at 排序。用下面这条 SQL 验证格式是否统一SELECT sku, fetched_at FROM price_history ORDER BY sku, fetched_at LIMIT 10;输出的时间要么全部是斜杠要么全部是横杠如果混着出现说明历史数据格式不统一需要写个小脚本清洗修正而不是在画图代码里将就处理。排序错乱只是表象根因是写入层不统一修数据不如修源头。6. 让监控长期跑调度方式、提醒通道与一张价格曲线图主循环用 while True 加 sleep 也能跑但想改间隔要改代码想限制运行时段很别扭进程意外退出后也没人帮你拉起来。更省心的方式是把调度交给专门的库APScheduler 是 Python 生态最常见的定时任务方案。把抓取、入库、判断、提醒收成一个 run_once 函数然后挂给调度器from apscheduler.schedulers.blocking import BlockingScheduler def run_once(): # 抓取、入库、判断、提醒 pass sched BlockingScheduler() sched.add_job(run_once, interval, minutes30, jitter300) sched.start()minutes30 表示每 30 分钟执行一次jitter300 表示每次触发在 0 到 300 秒内随机偏移和 3.4 节随机延时的思路一样避免所有请求都落在整点。如果想只在白天跑换成 cron 触发方式配置 hour 参数的起止窗口即可。提醒通道上邮件最可靠用 smtplib 标准库就能发不依赖第三方平台Server酱和企业微信机器人则是向手机推送更直观的做法实现上都是往一个 webhook 地址 POST JSON会了 requests 就没有难度。提醒文案应带上商品名、当前价、目标价和抓取时间一眼能看懂发生了什么而不是只丢一串 SKU。历史价格的可视化值得最后做一步它能把零散数据变成一眼能读的趋势判断。matplotlib 直读 SQLite 就能输出曲线图import sqlite3 import matplotlib matplotlib.use(Agg) # 无桌面环境也能渲染图片 import matplotlib.pyplot as plt conn sqlite3.connect(prices.db) rows conn.execute( SELECT fetched_at, price FROM price_history WHERE sku ? ORDER BY fetched_at, (100012043978,), ).fetchall() times, prices zip(*rows) plt.plot(times, prices, markero, markersize2) plt.xticks(rotation45) plt.tight_layout() plt.savefig(price_trend.png, dpi150)matplotlib.use(Agg) 是很容易漏掉的关键配置它让 matplotlib 在无图形界面的服务器上也能把图渲染保存不写这一行在 Linux 上可能直接报错。图保存下来后隔几天翻一眼就能看出价格区间和波动节奏不用再每天手动刷新商品页。我自己的运行习惯是抓取间隔调到 90 分钟提醒只保留“跌穿目标价”和“历史新低”两类邮件标题直接带商品名和价格。监控系统的价值不是制造焦虑而是在对的时间提醒你一次仅此而已。希望帮到你。本文还有配套的精品资源点击获取
返回列表