Python爬虫+Django实现京东商品比价系统:从数据采集到Web展示

发布时间:2026/10/28 1:05:18
Python爬虫+Django实现京东商品比价系统:从数据采集到Web展示 简介这是一套基于Python与Django开发的京东商品比价系统完整项目适用于毕业设计、课程设计或个人项目二次开发技术栈涵盖request爬虫、数据库设计与Web端交互。系统已通过严格测试具备注册登录、商品收藏、十五天价格折线图、分类降幅推荐及跳转购买等核心功能能够完整体现电商比价场景的爬虫、存储与可视化全流程。资源包共2001个文件以Python源码py与pyc为主另含HTML模板、CSS/JS前端样式、Django国际化翻译文件po/mo以及SQL脚本等整体约16.2MB目录结构清晰适合快速导入运行并学习关键实现。目前已有75人学习浏览。相比直接复制代码这套资料提供的是带工程化组织的完整数据库与前后端代码可辅助理解爬虫调度、价格曲线绘制和推荐逻辑也便于在现有基础上扩展新功能。1. 京东商品比价系统一个爬虫加 Web 展示的课设项目能解决什么实际问题同一个商品在京东自营、第三方店铺和不同促销节点下价格差个几十上百块是常态。所谓京东商品比价系统就是通过 requests 爬虫定时抓取京东商品页的价格与标题把结果存进数据库再通过 Django 把价格历史、最低价、价格走势展示成网页。对做毕业设计或课程设计的同学来说这套系统的好处在于技术栈干净——Python、Django、requests、MySQL 全是高频考点每一层都能单独写进报告里而且爬虫和 Web 是两条清晰的主线工作量容易拆分演示时也直观。我下面按自己实际搭过一遍的顺序从爬虫解析讲到 Django 模型再讲到数据库落地最后把容易翻车的地方集中列出来。2. 用 requests 把京东价格抓下来从商品 ID 到解析落库的完整链路2.1 为什么选 requests 而不是 Scrapy课设场景的选型理由很多教程一上来就推 Scrapy但对课设项目来说Scrapy 的学习曲线和调试成本经常超过收益。Scrapy 有 Item Pipeline、Middleware、Selector 一整套框架数据量小的时候这些全是负担。requests 的写法贴近普通 Python 代码逻辑是顺序的——请求一个页面、解析、存库、再请求下一个出了问题用 print 或者日志就能定位不用等整个爬虫跑完才发现中间某个 item 丢了。requests 做京东爬虫的核心流程只有三步构造请求头、发起 GET 请求、用 XPath 提取目标字段。下面这段是我在项目里最常用的最小实现import requests from lxml import html HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.jd.com/, } def fetch_item(sku_id): url fhttps://item.jd.com/{sku_id}.html resp requests.get(url, headersHEADERS, timeout5) resp.encoding utf-8 tree html.fromstring(resp.text) title tree.xpath(//div[classsku-name]/text()) return {sku_id: sku_id, title: title[0].strip() if title else } if __name__ __main__: print(fetch_item(100012043978))这段代码的逻辑很直接把商品 ID 拼成链接带上伪造的请求头访问然后用 XPath 取出标题。参数里关键的是 HEADERS 中的 User-Agent 和 Referer——UA 用来模拟浏览器环境Referer 填京东首页是为了让服务端认为请求是从站内跳转过去的。timeout 设 5 秒避免某个商品页面卡住导致整个爬虫线程阻塞。2.2 商品详情页解析XPath 的 text 函数与价格字段的三种形态京东的标题节点相对固定但价格字段不是每次都出现在同一个位置这是新手最容易懵的地方。常见的情况有三种价格在span classprice里、价格通过p-price容器的i标签展示、价格根本不在 HTML 里而是由页面加载后的 JavaScript 异步填充。前两种用 XPath 的 text 函数能直接取到第三种必须绕开静态页面去找接口。def parse_price(tree): # 情况一直接在价格节点内 price tree.xpath(//span[classprice]/text()) if price: return price[0].strip() # 情况二在 p-price 的 i 标签里 price tree.xpath(//div[contains(class, p-price)]//i/text()) if price: return price[0].strip() # 情况三都没有说明价格是异步加载的 return None这里用到了 xpath 的 text 函数来提取标签内部的文本内容。注意 xpath 的text()是和路径表达式连用的不是独立函数//span[classprice]/text()返回的是一个列表哪怕只有一个匹配结果也是列表所以取[0]之前要注意检查空列表。contains 那个写法是京东页面里常见的形式class 名经常是p-price加随机后缀直接用精确等号匹配不到必须用 contains 做模糊匹配。如果回显的 price 为 None说明这个商品走的是异步加载需要到京东的价格接口去拿。常见的做法是请求https://p.3.cn/prices/mgets?skuIdsJ_商品ID这个接口返回 JSON里面直接有p字段表示价格。这是我在项目里最常用的一条路代码不复杂但能避开很多页面解析上的坑。def fetch_price_from_api(sku_id): url fhttps://p.3.cn/prices/mgets params {skuIds: fJ_{sku_id}} resp requests.get(url, paramsparams, headersHEADERS, timeout5) data resp.json() if data and p in data[0]: return data[0][p] return None这里用 params 而不是直接把参数拼进 URL是因为 requests 会自动处理 URL 编码京东接口对 J_ 前缀的传参要求比较严格拼字符串时一旦漏了前缀或者多了空格就直接返回空数组。另外注意这个接口返回的价格是字符串类型存数据库之前要做一步 Decimal 转换否则后面查价格区间比较时会出类型错误。2.3 请求头伪装与频率控制让爬虫活过前十分钟的三条基本配置爬虫写完了能跑不代表能稳定跑。京东的反爬主要在风控层面不是简单封 IP而是检测请求特征的一致性。我见过最典型的翻车场景是前几个商品抓得正常第十个开始返回验证码页面HTML 里全是空节点。原因就是请求频率太高或者 User-Agent 太单一被识别成脚本。第一条配置是随机 User-Agent。不能只用一个 UA要在每次请求时从列表里随机选一个。哪怕只有三四个 UA 轮着用也比固定一个 UA 的效果好很多。第二条是请求间隔。不要用固定 sleep固定间隔会被统计规律识别出来随机 2 到 5 秒的浮动间隔更接近真实用户行为。第三条是异常后自动降速。当某次请求返回的状态码是 302 跳到登录页或者页面里检测不到预期节点时把当前线程的 sleep 时间翻倍。import random import time UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15, Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15, ] def safe_fetch(url): headers {User-Agent: random.choice(UA_POOL), Referer: https://www.jd.com/} try: resp requests.get(url, headersheaders, timeout5) if resp.status_code in (302, 403): time.sleep(random.uniform(5, 10)) else: time.sleep(random.uniform(2, 5)) return resp except requests.RequestException: time.sleep(10) return None这段代码把频率控制直接写进了请求函数。注意异常处理用的是 requests.RequestException这是 requests 所有网络异常的父类能一次性兜住连接超时、读超时和 DNS 解析失败。如果只捕获 ConnectionError碰到读超时还是会崩。sleep 放在请求之后而不是请求之前是为了让每次请求之间的实际间隔包含网络耗时本身延迟更自然也不容易在日志统计里暴露出整齐的节奏。3. Django 端把比价做厚模型设计、查询优化与定时更新3.1 三张核心表商品、价格记录、用户关注以及它们之间的关联爬虫只是数据采集的入口真正让系统像个比价系统的是 Django 的模型层。我的建议是不要把所有字段塞进一张表至少拆成三张商品表存静态信息价格记录表存历史价格用户关注表存用户和商品的关系。商品和价格记录是一对多关系用户和商品是多对多关系这个关系理顺了后面的查询和报表都好写。from django.db import models class Product(models.Model): sku_id models.CharField(max_length32, uniqueTrue, verbose_name京东SKU) title models.CharField(max_length255, verbose_name商品标题) shop_name models.CharField(max_length128, blankTrue, verbose_name店铺) url models.URLField(verbose_name商品链接) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: ordering [created_at] class PriceRecord(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameprices) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) is_promotion models.BooleanField(defaultFalse, verbose_name是否促销价) checked_at models.DateTimeField(auto_now_addTrue, verbose_name抓取时间) class UserFollow(models.Model): user models.ForeignKey(auth.User, on_deletemodels.CASCADE) product models.ForeignKey(Product, on_deletemodels.CASCADE) target_price models.DecimalField(max_digits10, decimal_places2, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue)模型设计的几个细节sku_id 加 unique 约束防止爬虫重复入库商品表重复抓取时用get_or_create更新标题而不是新建记录PriceRecord 的外键设置了 related_nameprices查商品时可以直接product.prices.all()拿到全部价格记录不用再写 filterDecimalField 而不是 FloatField 存价格原因是浮点比较在 SQL 查询中会触发精度问题比价系统要查最低价和价格区间精度不对是致命的。UserFollow 里的 target_price 是给降价提醒功能预留的字段课设里做一个简单的列表页展示历史最低价就够了但加上这个字段后报告里可以多写一个用户可设置期望价格系统在价格降到目标以下时高亮提示的功能点答辩时能多一个亮点。3.2 django 执行查询的写法按商品查最新价格、按时间范围查价格波动模型写完之后关键是把查询写好。这里要提一个 Django 的经典坑查最新价格时不能按 id 倒序然后 first()这个写法在数据量大时性能极差。正确做法是用价格记录表按 product 分组每组取最新一条这用窗口函数或者子查询才可以做到干净利落。from django.db.models import Max, F, Subquery, OuterRef def get_latest_price(product): latest_record PriceRecord.objects.filter( productproduct ).order_by(-checked_at).first() return latest_record.price if latest_record else None def get_price_trend(product, days30): from django.utils import timezone from datetime import timedelta start_time timezone.now() - timedelta(daysdays) records PriceRecord.objects.filter( productproduct, checked_at__gtestart_time ).order_by(checked_at) return [(r.checked_at, r.price) for r in records]第一个查询把所有价格记录按时间倒序取第一条就是最新价格。第二个查询用checked_at__gte做时间范围过滤是 Django ORM 的标准写法。这里需要注意的是排序字段 checked_at 和 created_at 的区别——一条价格记录可能在同一秒内插入多条created_at 精确到秒会出现并列排序结果不稳定。checked_at 如果也在同一秒内要再加一个 id 倒序做次级排序才能保证取到的确实是最后插入的那条。django 执行查询还有一档要注意的删除对象时要清理关联数据。PriceRecord 的外键设置的是 CASCADE 删除所以删商品时价格记录会自动跟着删但如果你删的是 UserFollow 记录而商品还在这个操作会很安全——外键的另一端没有被误删。如果你没有设置 on_delete 参数Django 会强制你在迁移时报错这会在后面避坑章节详细说。3.3 用 Celery 或 CRON 把爬虫挂成定时任务增量更新价格抓一次价格放入库不算完整的比价系统比价的意义在于持续记录价格变化。最简单的方案是用 Linux 的 cron 定时跑一个 Django management command。这个方案的优点是不用额外引入消息队列和 worker 进程课设演示时稳定性更高——Celery 虽然看起来高级但 Windows 环境下 Celery 4 之后的版本对子进程支持有兼容性问题答辩现场 celery worker 起不来的例子我见过太多次了。# management/commands/fetch_prices.py from django.core.management.base import BaseCommand from products.models import Product, PriceRecord class Command(BaseCommand): help 抓取所有已关注商品的最新价格 def handle(self, *args, **options): products Product.objects.all() for product in products: price fetch_price_from_api(product.sku_id) if price: PriceRecord.objects.create(productproduct, priceprice) self.stdout.write(f[OK] {product.sku_id}: {price}) else: self.stdout.write(f[FAIL] {product.sku_id})这个 command 可以放到 crontab 里每天定时执行也可以手动在 Django 项目根目录跑python manage.py fetch_prices。注意我在 handle 里用 self.stdout.write 而不是 print这是 Django 官方推荐的做法因为 management command 的输出要被测试框架捕获时print 是会绕过输出重定向的。command 里的 Product.objects.all() 如果商品数量上百个顺序抓会太慢常见做法是加一个--limit参数控制单次抓取数量把全量更新拆成多次增量执行这样即使某一次网络波动导致任务失败损失也只是局部商品没更新。4. 从零跑通这套系统数据库迁移与初始化的顺序4.1 创建 Django app 与注册模型比价系统的常见项目结构是一个主项目放 settings 和 urls一个 products app 放模型和视图爬虫代码放在 products 下的 management 命令或者独立 python 包里。这样分层的好处是爬虫和 Web 可以分别独立测试交报告的时候目录结构也清晰不用解释一堆散落的文件。django-admin startproject jd_compare cd jd_compare python manage.py startapp products创建好 app 之后必须在 settings.py 的 INSTALLED_APPS 里手动登记。新手最常犯的错误是忘记这一步然后模型建好了、迁移命令也跑了结果 migrate 提示没有任何迁移可以应用。检查方法很简单在 INSTALLED_APPS 里找到products.apps.ProductsConfig或者简单的products两者都可以后者是简写形式Django 会自动找到 app 的配置类。4.2 数据库迁移makemigrations 与 migrate 的常见翻车点Django 的模型变更和数据库结构同步是通过迁移文件来完成的。改过 models.py 之后先makemigrations生成迁移文件再migrate应用到数据库这是标准两步。但实际操作中有一个高频坑修改了模型字段后makemigrations 提示需要为已有数据提供默认值。python manage.py makemigrations products python manage.py migrate出现这个提示的原因是新加的字段在已有数据行里没有值。常见做法有两种如果字段本身允许为空加nullTrue, blankTrue让数据库不填值如果不允许为空就在迁移交互提示时给一个一次性默认值比如默认填 0。不要把临时默认值一填就完事要记住后期数据清洗时把默认值修正掉否则数据库里会出现一批假价格记录影响比价统计结果。数据库本身的资源配置也是坑。用了 MySQL 的同学要注意Django 默认用的是 MySQLdb 驱动Python 3 环境要装的是 mysqlclient用 PyMySQL 的话需要在项目的__init__.py里加一段pymysql.install_as_MySQLdb()不然后续迁移会报 module 找不到。4.3 把爬虫数据导入数据库三种方式对比与选择爬虫抓下来的数据最终要进数据库。方式有几种各有利弊。第一种是直接在爬虫脚本里调用 Django ORM——把 Django 的环境初始化加在爬虫开头然后像写普通 Django 代码一样操作模型这种方式简单直接推荐课设使用。第二种是抓完导出 CSV 再用 loaddata 或手工导入适合需要做数据清洗的场景。第三种是爬虫只负责产出 JSON 数据文件Django 视图里解析文件入库适合把爬虫和 Web 彻底解耦。# 方式一示例独立脚本里初始化 Django 环境 import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, jd_compare.settings) django.setup() from products.models import Product, PriceRecord def save_product(item_data): product, created Product.objects.get_or_create( sku_iditem_data[sku_id], defaults{title: item_data[title], url: item_data[url]} ) if not created and product.title ! item_data[title]: product.title item_data[title] product.save() PriceRecord.objects.create(productproduct, priceitem_data[price])这段里要注意 get_or_create 的语义它先查询 sku_id 是否已存在存在则直接返回已有对象并让 created 为 False不存在才创建。已存在但标题变化的情况要单独处理——这里做了同步更新。这个存在即更新的逻辑是比价系统增量更新的关键不能每次抓取都重新创建商品记录否则同一商品会有多条重复记录比价就变成垃圾数据比对了。5. 避坑手册京东反爬、数据库锁与 Django 部署的 5 个真实踩坑记录5.1 现象爬着爬着突然返回登录页HTML 里找不到任何商品节点原因单 IP 请求频率超过阈值被风控临时拦截。这个拦截不是封 IP而是让请求返回一个验证跳转页面UA 像是正常浏览器也照样被拦频率才是关键。解决把请求间隔从固定 2 秒改成 4 到 8 秒的随机值同时增加 UA 随机池。如果是比赛或者毕设演示需要短时间内抓取大量商品可以改用京东的价格 mgets 接口替代页面解析接口的请求成本远低于页面风控阈值也相对宽松。5.2 现象migrate 报错说表已存在或者迁移记录和数据库结构对不上原因手动在数据库里建过表然后又开始跑 Django 迁移两张表的 schema 不一致或者把别人项目里的迁移文件直接复制过来但数据库里的 django_migrations 表没有对应记录。解决开发阶段最干净的处理方式是删库重建——把数据库里的业务表全部 drop 掉连 django_migrations 表也删掉重新执行python manage.py migrate。正式数据不要这么干要先备份再处理课设阶段数据库里没有不可再生的数据删库重建是最节省时间的后悔药。5.3 现象页面显示的价格和爬虫抓下来的价格对不上原因京东商品页的价格不是一次性由 HTML 返回的而是页面加载后通过异步请求填充。按静态节点去解析拿到的是初始占位价格或者空。解决优先使用价本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询