ARTICLE DETAIL

资讯详情

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

Python京东价格监控系统源码拆解:爬虫、存储与通知实战

Python京东价格监控系统源码拆解:爬虫、存储与通知实战 简介这套基于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.py和crawler_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手动建表但核心逻辑一致把商品 IDSKU和期望价格写进配置或数据库主循环遍历这个列表逐条请求。值得注意的一个细节是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.txtrequirements.txt里核心依赖包括selenium、requests、aiosmtplib或smtplib邮件发送、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(optionschrome_options)这部分花的时间一般比想象中多。如果你发现启动浏览器时报SessionNotCreatedException99% 是 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_emailqq.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_idsku_id, titleinfo.get(title, sku_id), pricecur_price, expected_priceexpected, to_emails[your_emailqq.com] ) print(f[通知] {sku_id} 触发降价提醒) if __name__ __main__: print(价格监控已启动按 CtrlC 停止) 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-Agent和Referer并且从浏览器里复制一条真实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, timeout10)加超时缓解但根治方案是切到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_urlNone, use_freeFalse): 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, timeout5) 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里就行。如果接口返回的代理带认证信息requests的proxies参数要写成http://user:passip: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_workers5) 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 平滑切换到 MySQLconn_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( hostlocalhost, userroot, passwordyourpassword, databaseprice_monitor, charsetutf8mb4 ) 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 语法错误。我拆源码时特意验证过连接层是否对两种模式做了占位符适配如果没有自己加个分支判断就能解决。这三个方向做完系统已经具备「多商品并发监控 代理轮换 数据库可扩展」的能力。从那以后我每搭一套基于爬虫的小系统都会强制自己先梳理一遍存储层和通知层的边界再急着赶功能。这次的京东价格监控系统源码不是最关键的模块怎么拆分、参数怎么配置、出问题怎么排查才是真正能带走的经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表