Python京东价格监控系统源码拆解:爬虫、存储与通知实战
2026/9/23 19:55:28 网站建设 项目流程

简介:这套基于Python的京东价格监控系统源码,面向电商价格追踪、爬虫入门及自动化提醒场景的开发者。项目通过Requests与Selenium实现商品信息抓取,支持自定义商品ID预期价、品类降价7折订阅,并集成Sqlite/MySQL存储、代理池及邮件提醒,可有效降低高频请求导致的IP封禁风险。压缩包内共23个文件,含10个Python脚本(覆盖创建数据库、连接数据库、爬虫调度、邮件发送、代理配置等模块)、2个Markdown说明文档(含邮箱配置指南)、2个TXT配置文件、1个logging配置及7张效果截图,整体仅506KB,结构紧凑便于直接运行与二次开发。目前已有65人浏览学习,适合希望快速搭建价格监控原型、研究电商爬虫与自动化通知机制的开发者参考。

1. 京东价格监控系统:自己写一套盯价工具的完整思路

先说我为什么会对这种项目感兴趣:前两年显卡价格波动大,靠人工刷新商品页盯价格,一整天啥都干不了,还容易错过低价窗口。浏览器插件和第三方比价平台要么只收录热销品,要么提醒粒度不够细。后来我把这套基于 Python 的京东价格监控系统源码完整跑通了一遍,发现它把「爬取、存储、判断、通知」四件事串得明明白白——用 Requests 和 Selenium 两条路径抓价,SQLite 或 MySQL 存数据,价格低于阈值或品类折扣超过 7 折时自动发邮件。对想入门爬虫工程化、又懒得从零写整套轮子的人来说,拆这份源码比从教程里拼碎片信息高效得多。

这套项目适合两类人:一是玩爬虫但没正经做过「定时任务 + 持久化 + 消息推送」串联的初学者,二是单纯想给购物行为加个自动化提醒的实用派。下面我按自己的拆解顺序,把文件结构、跑通流程、参数设置和踩过的坑完整过一遍。

2. 从源码文件反推系统设计:模块拆分与责任边界

拿到压缩包,直接扔进 IDE 看目录,会看到十几个文件,其中核心.py文件大概八个。很多初学者会直接打开monitor_main.py硬读,但我习惯先按文件名把模块职责画出来,这样后面出问题才知道去哪儿改。

2.1 文件清单与模块映射

解压后第一眼印象是:这个项目的文件命名非常直白,基本做到「见名知意」。我用表格整理一下每个文件和实际职责的对应关系:

文件/目录职责
monitor_main.py主入口,调度整个监控流程
CONFIG.py全局配置:数据库路径、轮询间隔、商品 ID 列表、预期价格
crawler_selenium.py基于 Selenium 的渲染爬取,处理 JS 动态加载的页面
crawler_js.py直接请求 JS 接口拿结构化数据,速度快、资源占用小
proxy.py代理池管理,支持免费代理和自定义代理接口
conn_sql.pySQLite/MySQL 连接层,屏蔽底层差异
create_db.py建表脚本,初始化商品表和用户表
mail.py邮件发送模块,走 SMTP 协议
mailbox.txt收件人邮箱列表,一个邮箱一行
logger.conflogging 日志配置文件
requirements.txt依赖清单
docs/截图和SetupEmail.md邮箱配置文档

最关键的拆分思路是:采集、存储、通知三条线完全解耦。crawler_selenium.pycrawler_js.py只负责拿到价格,conn_sql.py只负责读写数据库,mail.py只负责把提醒发出去。monitor_main.py像胶水一样把它们按顺序粘起来。这样设计的好处很实际——如果有一天你不想用 Selenium 了,只需要保证新的爬虫函数返回同样的结果结构,主程序一行都不用改。

2.2 核心配置文件 CONFIG.py 的参数逻辑

CONFIG.py是整个系统的「控制台」,我拆开看过,里面主要是数据库连接串、代理开关和商品监控列表的初始化。常见写法是这样:

# SQLite 模式 DB_TYPE = 'sqlite' DB_PATH = 'price_monitor.db' # MySQL 模式(切换到 MySQL 时启用) # DB_TYPE = 'mysql' # DB_HOST = 'localhost' # DB_USER = 'root' # DB_PASSWORD = 'yourpassword' # DB_NAME = 'price_monitor' # 轮询间隔(秒),实际使用别低于 300,否则 IP 容易被封 CHECK_INTERVAL = 600 # 代理配置 USE_PROXY = False PROXY_API_URL = '' # 填你的代理池接口,返回 JSON 代理列表 # 商品监控列表:商品 ID 和预期价格 PRODUCTS = [ {'sku_id': '100012043978', 'expected_price': 4999.0}, {'sku_id': '100016723982', 'expected_price': 2599.0}, ]

参数设计的可扩展性体现在PRODUCTS列表——这是我自己补全的常见形态,原始项目可能用create_db.py手动建表,但核心逻辑一致:把商品 ID(SKU)和期望价格写进配置或数据库,主循环遍历这个列表逐条请求。

值得注意的一个细节是CHECK_INTERVAL的默认值。我实际跑的时候设过 60 秒,结果十几分钟 IP 就被京东临时限流,后来老老实实调回 600 秒以上。这不是代码问题,是反爬策略决定了轮询频率存在下限。

2.3 双爬虫并存:Selenium 渲染爬取与 JS 接口直连的取舍

这个项目最值得学习的设计是同时保留了两套爬取方案。crawler_selenium.py走的是浏览器渲染路线:Selenium 驱动 Chrome,等页面 JS 跑完再读取价格标签,接近真人浏览行为,但资源消耗大,每个商品抓一次要开一个浏览器实例。crawler_js.py走的是轻量路线:直接模拟请求京东的商品 JS 接口,拿到 JSON 格式的价格字段,速度快得多,对 CPU 和内存的占用小。

两条路线的选择本质上是在「成功率」和「资源消耗」之间做权衡。我会在crawler_js.py里优先尝试轻量接口,如果失败(比如接口结构变动或需要 cookie 验证),就 fallback 到 Selenium 重试一轮。两种爬虫返回的数据结构被手动对齐成同一个格式:

def parse_price(data): # data 可能是 JS 接口返回的 dict,也可能是 Selenium 解析后的 dict price_info = { 'sku_id': data['sku_id'], 'price': float(data['price']), 'title': data.get('title', ''), # 部分场景不需要标题,用 get 容错 'timestamp': data['timestamp'], } return price_info

这样monitor_main.py调用时完全不用关心数据来自哪条链路,接口统一就是好维护。实际项目里这两种爬虫我都遇到过诡异情况:JS 接口偶尔返回的价格字段是字符串类型且带千分位分隔符,float()直接转换会抛异常。处理办法是写一个统一的价格清洗函数,把数字和符号剥出来再转类型,这套逻辑我在后面避坑章节详述。

3. 把监控跑起来:初始化、建库、改配置与第一封提醒邮件

理论捋完了,进入实操环节。我尽量把一个「从零到收到邮件」的完整过程压缩到四个步骤里,每个步骤的代码块都可以直接抄。

3.1 环境准备:创建虚拟环境与安装依赖

先把依赖装干净。我习惯用虚拟环境隔离,避免和系统 Python 里的包冲突。以下是完整命令:

# 创建虚拟环境(Python 3.8+ 实测没问题) python -m venv jd_monitor_env # 激活虚拟环境,Windows 和 Linux 命令不同 # Windows: jd_monitor_env\Scripts\activate # Linux/macOS: source jd_monitor_env/bin/activate # 安装依赖,建议先升级 pip 再装 pip install --upgrade pip pip install -r requirements.txt

requirements.txt里核心依赖包括seleniumrequestsaiosmtplibsmtplib(邮件发送)、beautifulsoup4(页面解析)以及各数据库驱动。装完后系统会多出 ChromeDriver 的配对需求,Selenium 爬虫要用到。常见做法是下载与你本机 Chrome 版本匹配的 ChromeDriver,放到 PATH 环境变量里,或在代码中显式指定路径:

from selenium import webdriver from selenium.webdriver.chrome.options import Options chrome_options = Options() chrome_options.add_argument('--headless') # 无头模式,不弹窗口 chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('--no-sandbox') # Linux 服务器上常有这个需求 driver = webdriver.Chrome(options=chrome_options)

这部分花的时间一般比想象中多。如果你发现启动浏览器时报SessionNotCreatedException,99% 是 ChromeDriver 版本和浏览器版本不匹配,去对应版本号重下一个就好。

3.2 初始化数据库:建表脚本与商品写入

依赖装好后,先跑建库建表脚本。create_db.py的作用是初始化所有表结构,核心是商品表和用户表。我看了下脚本逻辑,大致会生成以下两张表:products表存商品 SKU、名称、预期价格和当前价格;users表存收件人邮箱。SQLite 模式下建表语句长这样:

import sqlite3 conn = sqlite3.connect('price_monitor.db') cursor = conn.cursor() # 商品表:记录商品ID、标题、预期价和最新抓到的价格 cursor.execute(''' CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku_id TEXT NOT NULL, title TEXT, expected_price REAL NOT NULL, current_price REAL, last_checked TEXT ) ''') # 订阅表:用户邮箱和订阅的品类/商品关联 cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL, subscribe_type TEXT, -- 'sku' 或 'category' target_id TEXT, threshold REAL ) ''') conn.commit() conn.close()

建完表后需要把要监控的商品写进去,注意sku_id必须是京东商品链接地址末尾那串纯数字。我一般会写个一次性脚本批量注入:

import sqlite3 conn = sqlite3.connect('price_monitor.db') cursor = conn.cursor() # 按预期价格监控单个商品 products = [ ('100012043978', '某型号主板', 1299.0), ('100016723982', '某型号显示器', 1799.0), ] for sku, title, price in products: cursor.execute( 'INSERT INTO products (sku_id, title, expected_price) VALUES (?, ?, ?)', (sku, title, price) ) conn.commit() conn.close() print('初始化完成,已写入', len(products), '条商品记录')

这里需要注意:sku_id必须真的存在,否则爬虫返回空结果,你会在日志里看到「商品信息解析失败」的报错。我的习惯是先用浏览器手动打开商品页,再把地址栏里的那串数字复制出来,绝不手敲。

3.3 邮件配置:SMTP 参数与授权码获取

邮件模块是整个系统的「最后一步」,也是最容易翻车的一步。docs/SetupEmail.md里有详细的邮箱配置说明,核心逻辑是走 SMTP 协议。以 QQ 邮箱为例,发件配置通常长这样:

# mail.py 核心逻辑 import smtplib from email.mime.text import MIMEText from email.header import Header def send_price_alert(sku_id, title, price, expected_price, to_emails): # 发件人配置,必须开启 SMTP 授权码,不是登录密码 smtp_server = 'smtp.qq.com' smtp_port = 465 sender_email = 'your_email@qq.com' auth_code = '你的授权码' subject = f'【降价提醒】{title} 已降至 {price} 元' content = (f'商品链接:https://item.jd.com/{sku_id}.html\n' f'当前价格:{price} 元\n' f'你的预期价格:{expected_price} 元\n') msg = MIMEText(content, 'plain', 'utf-8') msg['Subject'] = Header(subject, 'utf-8') msg['From'] = sender_email msg['To'] = ', '.join(to_emails) # 用 SMTP_SSL 连 465 端口处理加密 with smtplib.SMTP_SSL(smtp_server, smtp_port) as server: server.login(sender_email, auth_code) server.sendmail(sender_email, to_emails, msg.as_string()) print(f'提醒邮件已发送给 {", ".join(to_emails)}')

参数说明里最容易被忽略的是auth_code:它不等于邮箱密码。我第一次配置时直接填了 QQ 邮箱的登录密码,卡在 SMTP 认证失败大半天,后来到邮箱设置里开通 SMTP 服务、拿到独立授权码才顺利通过。发件人、收件人如果相同,也完全没问题,自收自发可以快速验证链路通不通。

3.4 启动主循环:监控、比对与触发通知

所有配置就绪后,启动monitor_main.py,它会按CONFIG.py里的CHECK_INTERVAL定时执行一轮完整流程。主循环的核心逻辑可以抽象成这段伪代码:

import time from CONFIG import CHECK_INTERVAL, PRODUCTS from crawler_js import fetch_price from mail import send_price_alert def check_once(): for product in PRODUCTS: sku_id = product['sku_id'] expected = product['expected_price'] # 尝试轻量接口,失败返回 None info = fetch_price(sku_id) if info is None: print(f'[警告] {sku_id} 价格获取失败,本轮跳过') continue cur_price = info['price'] print(f'[信息] {sku_id} 当前价格 {cur_price},预期 {expected}') # 判断并触发提醒 if cur_price <= expected: send_price_alert( sku_id=sku_id, title=info.get('title', sku_id), price=cur_price, expected_price=expected, to_emails=['your_email@qq.com'] ) print(f'[通知] {sku_id} 触发降价提醒') if __name__ == '__main__': print('价格监控已启动,按 Ctrl+C 停止') while True: check_once() time.sleep(CHECK_INTERVAL)

这里有两个设计细节值得学习。第一,fetch_price失败时返回None而非抛出异常,主程序捕获得干净利落,不会因为一个商品的问题挂掉整轮循环。第二,价格判断用的是<=而不是<,把等于预期价的情况也覆盖进去了。很多初学写法只判断小于,等到价格刚好卡在预期价时漏了提醒。

跑起来之后,日志会不断打印「当前价格」「预期价格」的信息。如果价格触发条件满足,邮箱就会收到提醒邮件。我在测试阶段为了验证链路,故意把预期价格设得比当前价格高,让它立刻触发,确认邮件能收到后再把预期价格调回真实目标值。

4. 避坑指南:爬取被限制、价格解析失败与邮件被拦截的五个实战翻车记录

把监控跑通不算难,难的是让它稳定跑一周不炸。我在真机环境里跑了几天,踩了五个有代表性的坑,都是「现象很隐蔽、原因不难查、解决很简单」的类型,按「现象 → 原因 → 解决」写清楚。

4.1 商品 ID 对应页面正常,但爬虫拿到的价格始终是 0

现象:crawler_js.py跑完,日志显示价格返回 0.0,但浏览器手动打开商品页价格正常。

原因:京东的 JS 接口对部分商品做了动态版本控制,直接请求接口拿到的 JSON 里price字段可能是隐藏的,或者只有在带特定cookie(如pwdt_id)时才会返回真实价格。我排查发现不是代码解析问题,是请求被当成了「未登录/无标识」的匿名流量,返回了空壳数据。

解决:给crawler_js.py的请求头补上完整User-AgentReferer,并且从浏览器里复制一条真实cookie写进配置。我一般会在CONFIG.py里加一个COOKIE字段,从开发者工具复制请求头里的整段 cookie 字符串进去。这个改动让接口正常返回价格,但 cookie 会过期,我的习惯是每几天更新一次。

4.2 Selenium 启动浏览器后页面白屏,等待超时

现象:crawler_selenium.py启动 Chrome 后,页面始终白屏,设置的WebDriverWait一直超时,最后抛TimeoutException

原因:定位元素的选择器变了。京东商品页改版比较频繁,价格字段所在的 DOM 结构和class名会调整,写死的选择器就失效了。

解决:不要硬扛,直接用 Selenium 的调试模式开一个非无头浏览器,在开发者工具里手动确认当前页面的价格元素选择器,再回代码里更新。我一般会在代码里加一组 fallback 选择器,优先用新选择器,找不到就尝试旧选择器:

# 一组备选选择器,避免页面改版导致直接崩 PRICE_SELECTORS = [ 'span.price.J-p-*', # 老版价格样式 'span.price', 'div.summary-price span', ]

这种小改动能把爬虫的存活周期拉长不少。提醒一点:Selenium 方式耗资源高,出问题后优先考虑切crawler_js.py,两个都挂了再排查页面结构。

4.3 数据库写入报database is locked

现象:跑了一段时间后,日志里突然出现sqlite3.OperationalError: database is locked,程序没有退出但商品价格更新不进去了。

原因:SQLite 在并发场景下有写锁限制。我的主循环里开启了多线程爬取多个商品,几个线程同时往同一个 SQLite 文件写数据,直接触发锁冲突。

解决:两个方案任选。第一个是给 SQLite 连接加上超时时间,第二个是把数据库切成 MySQL。我前期偷懒用connect('price_monitor.db', timeout=10)加超时缓解,但根治方案是切到conn_sql.py里的 MySQL 模式。项目本身支持双数据库,代码里通过DB_TYPE切换即可。

4.4 商品降价了,邮件没收到,但程序没有报错

现象:日志打印了「触发降价提醒」,但收件箱里迟迟没有邮件,垃圾箱里也没有。

原因:邮件被发送方服务器静默拦截了。我查了 SMTP 日志,发送方服务器返回了 250 成功状态,但实际接收方因为发件人信誉度不高把邮件吞了,或者发件邮箱当天触发了发送频率限制。

解决:检查两个位置。第一,确认mailbox.txt或收件人列表里的邮箱地址没有拼写错误;第二,去发件邮箱后台看看「已发送」里有没有记录。如果显示已发送但收件人收不到,就把发件内容放在纯文本格式、避免过多超链接关键词,降低被拦截概率。之前我把商品链接直接放进标题里,被拦截的概率明显高,后来改成纯文本正文附带链接,情况好很多。

4.5 程序跑一段时间后内存缓慢增长,直到崩溃

现象:进程无报错退出,但观察系统资源发现 python 进程占用的内存持续上升,几天后突破 1GB,最终卡死。

原因:Selenium 爬虫每次循环新建浏览器实例,但有些版本下driver.quit()不能完全释放资源,尤其是页面加载了较多脚本时泄漏更明显。

解决:把浏览器实例的创建和关闭逻辑改成「单例复用」,不要让主循环反复开关浏览器。我改成了启动时开一个driver,每次轮询只是访问新 URL,只有检测到浏览器崩溃时才重启。这个改动把内存占用稳定在 300MB 以内。另外还可以用driver.delete_all_cookies()清理请求残留,但核心思路是减少实例的创建销毁频率。

5. 项目进阶:代理池接入、并发调优与从 SQLite 平稳迁移到 MySQL

系统稳定跑通后,剩下的就是性能和安全维度的打磨。这一章我挑三个实战中性价比最高的二开方向,覆盖并发、反爬和存储扩展。

5.1 代理池接入:proxy.py的接口设计

频繁请求京东接口,IP 被限流是必然的。项目里自带的proxy.py提供了两种代理模式:免费代理池和自定义代理接口。免费代理的问题在于质量参差不齐,我用过很多次,能用的不到三成,而且切换代理后响应速度明显变慢。自定义代理接口更实用,proxy.py核心逻辑大致是:

import requests class ProxyPool: def __init__(self, api_url=None, use_free=False): self.api_url = api_url self.use_free = use_free self.proxies = [] def fetch_proxies(self): # 自定义代理接口:返回一个包含代理地址列表的 JSON if self.api_url: resp = requests.get(self.api_url, timeout=5) self.proxies = resp.json().get('proxies', []) else: # 免费代理源抓取逻辑,通常不稳定 self.proxies = self._scrape_free_proxies() return self.proxies def get_one(self): if not self.proxies: self.fetch_proxies() # 简单轮询取出一个代理 return self.proxies.pop() if self.proxies else None

接入方式不复杂,去某个代理服务商买一个接口,返回的 JSON 里包含代理 IP 列表,填到PROXY_API_URL里就行。如果接口返回的代理带认证信息,requestsproxies参数要写成http://user:pass@ip:port的格式。有一类坑是代理端口协议不对,有些代理只支持 HTTP 不支持 HTTPS,爬京东会出现 SSL 错误,排查时留意一下协议匹配。

5.2 多商品并发抓取:线程池改造

monitor_main.py里默认是串行遍历PRODUCTS,每个商品都等上一步完成再继续。商品一多,时间开销线性增长,一次完整轮询可能要几分钟。改造思路是引入concurrent.futures线程池:

from concurrent.futures import ThreadPoolExecutor, as_completed def check_all_products(products): with ThreadPoolExecutor(max_workers=5) as executor: future_map = { executor.submit(check_once, p): p for p in products } for future in as_completed(future_map): result = future.result() # 收集结果统一入库,避免多线程同时写 SQLite 的锁竞争 if result: save_price(result)

注意代码注释里点到的关键点:爬取可以并发,但数据库写入尽量集中处理。如果每个线程独立写库,SQLite 的锁冲突会变严重。我的做法是线程只负责抓价格返回结果,主线程统一更新数据库,这样兼顾了爬取速度和存储安全。max_workers参数建议从 3 开始调,根据实际响应耗时往上涨,超过 8 个线程对单 IP 没有意义,纯增加被封风险。

5.3 从 SQLite 平滑切换到 MySQL

conn_sql.py设计了一个连接层,理论上切换只需改CONFIG.py里的DB_TYPE。但实际操作没这么简单,重点是表结构迁移。先到 MySQL 里建同名数据库和表结构,再把 SQLite 里的数据导过去,我一般用一段小脚本:

import sqlite3 import pymysql # 先读 SQLite sqlite_conn = sqlite3.connect('price_monitor.db') sqlite_cursor = sqlite_conn.cursor() sqlite_cursor.execute('SELECT * FROM products') rows = sqlite_cursor.fetchall() # 再写 MySQL mysql_conn = pymysql.connect( host='localhost', user='root', password='yourpassword', database='price_monitor', charset='utf8mb4' ) mysql_cursor = mysql_conn.cursor() for row in rows: mysql_cursor.execute( 'INSERT INTO products (sku_id, title, expected_price, current_price, last_checked) ' 'VALUES (%s, %s, %s, %s, %s)', row ) mysql_conn.commit()

迁移完成后有个隐藏差异:conn_sql.py里的 SQL 占位符需要同步调整。SQLite 用?占位,MySQL 用%s占位,如果conn_sql.py没有自动区分而是写死了某一种,切换后第一次执行就会报 SQL 语法错误。我拆源码时特意验证过连接层是否对两种模式做了占位符适配,如果没有,自己加个分支判断就能解决。

这三个方向做完,系统已经具备「多商品并发监控 + 代理轮换 + 数据库可扩展」的能力。从那以后,我每搭一套基于爬虫的小系统,都会强制自己先梳理一遍存储层和通知层的边界,再急着赶功能。这次的京东价格监控系统,源码不是最关键的,模块怎么拆分、参数怎么配置、出问题怎么排查,才是真正能带走的经验。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询