简介:基于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 先列一遍。部分精简系统没装 unzip,Debian/Ubuntu 用 apt install unzip,CentOS 用 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两条命令都能正常输出版本号,说明解释器就绪。接下来是依赖。这份源码对外部库的依赖通常很克制,主要就是 requests,sqlite3、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。按 Ctrl+Shift+P 打开命令面板,搜 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": f"J_{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": f"https://item.jd.com/{sku}.html", } resp = requests.get(PRICE_API, params=params, headers=headers, timeout=10) resp.raise_for_status() return resp.json()[0]两个关键点:params 里的 skuIds 必须带 J_ 前缀,这是接口的历史约定;headers 里的 User-Agent 和 Referer 是基本伪装,UA 模拟浏览器是为了不被当成异常请求,Referer 填商品页地址则是因为页面内调用这个接口时本来就会带上该来源,缺了它接口可能返回空列表。timeout=10 意味着一轮请求超过 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_history,MIN(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。网络一抖动,连接既不成功也不失败,一直在那里等,整个线程被卡死;此时如果线程是被主循环串行调用的,后续所有抓取全部阻塞,再遇到未捕获异常就直接退出进程。
解决:所有网络请求统一加 timeout=10;重试函数捕获 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", minutes=30, jitter=300) sched.start()minutes=30 表示每 30 分钟执行一次,jitter=300 表示每次触发在 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, marker="o", markersize=2) plt.xticks(rotation=45) plt.tight_layout() plt.savefig("price_trend.png", dpi=150)matplotlib.use("Agg") 是很容易漏掉的关键配置,它让 matplotlib 在无图形界面的服务器上也能把图渲染保存,不写这一行在 Linux 上可能直接报错。图保存下来后,隔几天翻一眼就能看出价格区间和波动节奏,不用再每天手动刷新商品页。
我自己的运行习惯是:抓取间隔调到 90 分钟,提醒只保留“跌穿目标价”和“历史新低”两类,邮件标题直接带商品名和价格。监控系统的价值不是制造焦虑,而是在对的时间提醒你一次,仅此而已。希望帮到你。
本文还有配套的精品资源,点击获取