Django+requests京东比价系统实战:从爬虫到展示全链路拆解 简介这是一套基于Python与Django框架开发的京东商品比价系统完整项目源码面向计算机相关专业的毕业设计、课程设计及项目开发学习者帮助解决电商价格监控与比价分析类选题的实现难题。项目采用request爬虫抓取京东商品数据配合数据库存储已实现注册登录、商品收藏、十五天内价格折线图展示、按品类推荐降幅比最大商品以及跳转购买等核心功能逻辑完整、贴近真实业务场景。压缩包共2001个文件约16.2MB其中以1174个py源码文件为主体辅以368个pyc编译文件、128个html模板、82个js脚本及css样式、po/mo国际化语言包、sql建表脚本与json配置等前后端与数据层结构清晰。目前已有75人学习下载源码经过严格测试可直接运行参考并在此基础上进行功能扩展或二次开发适合作为毕业设计答辩与课程实践的可靠基础。1. 从零拆解一套京东商品比价系统Django requests 到底能跑多远电商比价这件事听起来像个小工具真动手才知道坑有多密。我最近把一套基于 Python Django 的京东商品比价系统完整跑了一遍从爬虫抓取、数据入库到前端展示链路不算长但每一环都有值得说道的地方。这套资源的核心价值在于它把爬取—清洗—存储—比价—展示这条完整链路用 Django 串了起来而不是丢给你一个孤零零的爬虫脚本。适合谁正在做毕业设计、课程设计或者想拿一个完整 Web 项目练手的同学。它不追求高并发工业级但胜在结构清晰、能跑通、方便二次改造。下面我按实际拆解顺序把选型理由、关键代码、参数设置和踩坑记录一条条摊开讲。2. 技术选型与项目骨架为什么是 Django 而不是 Flask2.1 比价系统的功能拆解与框架匹配先想清楚这个系统要干什么。比价系统的本质是定时或按需抓取目标商品的名称、价格、店铺、销量等字段存进数据库然后在前端按关键词搜索、按价格排序、展示历史价格走势。它天然需要 ORM、后台管理、模板渲染和路由这几样东西。Django 自带 admin 后台、ORM 和模板引擎等于开局就送了一套管理界面和数据层对毕业设计这种功能要全、时间要短的场景非常友好。Flask 更轻但你得自己拼 SQLAlchemy、自己写后台工作量反而更大。这套资源的技术栈是 Python Django requests MySQL常见做法也可换 SQLite。requests 负责发 HTTP 请求拿页面Django 负责业务逻辑和展示。选 requests 而不是 Scrapy是因为比价场景的抓取量不大、页面结构相对固定用 requests 解析库足够没必要上 Scrapy 那套重量级调度。2.2 目录结构与核心模块职责一个能跑的 Django 项目目录大致长这样jingdong_price/ ├── manage.py ├── jingdong_price/ # 项目配置 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── spider/ # 爬虫应用 │ ├── models.py # 商品数据模型 │ ├── views.py # 抓取触发与比价逻辑 │ └── utils.py # 请求封装、解析函数 ├── templates/ # 前端模板 └── static/ # 静态资源models.py定义商品表utils.py放请求头和解析逻辑views.py把抓取和展示串起来。这种分层的好处是爬虫规则变了只改utils.py数据字段变了只改models.py互不干扰。2.3 数据模型设计字段怎么定才够用比价系统的数据模型直接决定后面能做什么分析。核心表至少要有这些字段字段名类型说明goods_idCharField商品唯一标识用于去重titleCharField商品标题priceDecimalField当前价格用 Decimal 避免浮点误差shop_nameCharField店铺名称comment_countIntegerField评论数可作热度参考crawl_timeDateTimeField抓取时间用于历史价格对比urlURLField商品链接价格字段一定要用DecimalField而不是FloatField。我见过太多人用 float 存价格结果 19.9 存进去变成 19.899999比价时排序全乱。这是血泪经验别省这一步。# spider/models.py from django.db import models class Goods(models.Model): goods_id models.CharField(max_length32, uniqueTrue, verbose_name商品ID) title models.CharField(max_length255, verbose_name商品标题) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) shop_name models.CharField(max_length128, blankTrue, verbose_name店铺) comment_count models.IntegerField(default0, verbose_name评论数) crawl_time models.DateTimeField(auto_now_addTrue, verbose_name抓取时间) url models.URLField(max_length500, blankTrue, verbose_name商品链接) class Meta: ordering [-crawl_time] verbose_name 商品goods_id加uniqueTrue是为了配合后面的update_or_create做去重避免同一商品反复入库。auto_now_addTrue让抓取时间自动填充省得手动传。3. requests 抓取实战请求头、分页与解析的完整链路3.1 请求封装请求头是能不能拿到数据的第一道关京东的搜索页对请求头比较敏感裸 requests 直接请求大概率拿到空数据或验证页。常见做法是伪装一套完整的浏览器请求头尤其是User-Agent、Referer和Cookie。# spider/utils.py import requests import time import random 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: https://search.jd.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url, paramsNone, retry3): 带重试的页面抓取失败后随机延时再试 for i in range(retry): try: resp requests.get( url, headersHEADERS, paramsparams, timeout10 ) if resp.status_code 200: return resp.text except requests.RequestException as e: print(f第{i1}次请求失败: {e}) time.sleep(random.uniform(1, 3)) # 随机延时降低被封风险 return Nonetimeout10防止请求卡死retry3配合随机延时是应对偶发失败的标配。random.uniform(1, 3)让每次请求间隔不固定比固定 sleep 更不容易触发风控。注意这里的Cookie我没有硬编码因为 Cookie 会过期正确做法是从浏览器手动复制后放进配置或者用 session 维持。3.2 分页抓取page 参数与翻页逻辑京东搜索页的分页参数是page但它是奇数递增的1、3、5……每页大约 30 个商品。翻页逻辑要处理这个规律def crawl_search(keyword, max_page5): 按关键词抓取多页搜索结果 base_url https://search.jd.com/Search all_goods [] for p in range(1, max_page * 2, 2): # 1,3,5,7... params {keyword: keyword, enc: utf-8, page: p} html fetch_page(base_url, paramsparams) if not html: continue goods parse_goods(html) all_goods.extend(goods) time.sleep(random.uniform(2, 4)) # 页间延时拉长 return all_goodsrange(1, max_page * 2, 2)生成的就是 1、3、5 这样的奇数序列。页间延时比单次重试延时更长因为连续翻页更容易被识别。max_page控制抓取深度毕设演示抓 5 页足够抓太多既慢又容易触发限制。3.3 页面解析定位价格与标题的稳定写法解析是比价系统里最容易翻车的一环因为页面结构会变。常见做法是用 BeautifulSoup 配合 CSS 选择器把选择器集中管理方便页面改版时统一替换。from bs4 import BeautifulSoup def parse_goods(html): 从搜索页 HTML 中提取商品列表 soup BeautifulSoup(html, html.parser) results [] for item in soup.select(li.gl-item): try: goods_id item.get(data-sku, ) title item.select_one(div.p-name em).get_text(stripTrue) price item.select_one(div.p-price i).get_text(stripTrue) shop item.select_one(div.p-shop a) results.append({ goods_id: goods_id, title: title, price: price, shop_name: shop.get_text(stripTrue) if shop else , }) except AttributeError: continue # 单个商品解析失败不影响整体 return results>from decimal import Decimal, InvalidOperation from .models import Goods def save_goods(items): 批量入库按 goods_id 去重更新 for it in items: try: price Decimal(it[price].replace(¥, ).strip()) except (InvalidOperation, KeyError): continue # 价格解析失败就跳过 Goods.objects.update_or_create( goods_idit[goods_id], defaults{ title: it[title], price: price, shop_name: it.get(shop_name, ), }, )update_or_create以goods_id为键存在就更新defaults里的字段不存在就新建。这样同一商品的历史价格会被覆盖——如果你要做价格走势得另建一张历史价格表每次抓取都插一条而不是覆盖。这是设计上要提前想清楚的点。4. Django 比价逻辑与前端展示从查询到排序4.1 比价视图多条件查询与排序比价的核心视图要支持关键词搜索、价格区间过滤和排序。用 Django ORM 的filter和order_by组合即可# spider/views.py from django.shortcuts import render from .models import Goods def compare(request): keyword request.GET.get(kw, ).strip() min_price request.GET.get(min, ) max_price request.GET.get(max, ) order request.GET.get(order, price) qs Goods.objects.all() if keyword: qs qs.filter(title__icontainskeyword) if min_price: qs qs.filter(price__gtemin_price) if max_price: qs qs.filter(price__ltemax_price) if order in (price, -price, -comment_count): qs qs.order_by(order) return render(request, compare.html, {goods_list: qs[:50]})title__icontains做不区分大小写的模糊匹配price__gte/price__lte做区间过滤。order参数做了白名单校验只允许指定的排序字段防止用户传入非法字段导致报错。qs[:50]限制返回条数避免一次渲染太多拖慢页面。4.2 模板渲染与价格高亮前端模板把商品列表渲染成表格或卡片价格最低的做高亮这是比价系统最直观的价值点!-- templates/compare.html -- table thead trth商品/thth价格/thth店铺/thth评论数/th/tr /thead tbody {% for g in goods_list %} tr {% if forloop.first %}classcheapest{% endif %} tda href{{ g.url }}{{ g.title }}/a/td td¥{{ g.price }}/td td{{ g.shop_name }}/td td{{ g.comment_count }}/td /tr {% endfor %} /tbody /table因为视图里默认按价格升序forloop.first就是最便宜的那条给它加个cheapest类做高亮。这个思路简单但有效答辩时演示效果很直观。4.3 定时抓取的两种落地方式比价系统不能只靠手动点按钮抓取。常见做法有两种一是用 Django 的manage.py自定义命令配合系统定时任务二是用 Celery 做异步调度。毕设场景推荐第一种简单可控# spider/management/commands/crawl.py from django.core.management.base import BaseCommand from spider.utils import crawl_search, save_goods class Command(BaseCommand): help 按关键词抓取京东商品并入库 def add_arguments(self, parser): parser.add_argument(keyword, typestr) parser.add_argument(--pages, typeint, default3) def handle(self, *args, **options): items crawl_search(options[keyword], max_pageoptions[pages]) save_goods(items) self.stdout.write(f入库 {len(items)} 条)写好命令后python manage.py crawl 手机 --pages 5就能手动跑再挂到系统的定时任务里定时执行。add_arguments让关键词和页数可配置不用改代码。5. 避坑与排查这套系统最容易翻车的五个地方5.1 抓不到数据返回空列表或验证页现象fetch_page返回 200但parse_goods解析出来是空的。原因请求头不完整尤其是缺Cookie或Referer被识别为非浏览器请求。解决从浏览器开发者工具里复制完整的请求头重点补上Cookie和Referer并确保User-Agent是真实浏览器串。如果还是不行降低抓取频率加长页间延时。5.2 价格字段入库报错或精度丢失现象入库时抛InvalidOperation或者价格显示成 19.899999。原因一是价格字符串里带了¥或空格没清理干净二是模型用了FloatField。解决入库前用replace(¥, ).strip()清洗模型字段改成DecimalField转换时用Decimal()而不是float()。5.3 同一商品重复入库现象数据库里同一个商品出现多条记录。原因没用goods_id做唯一约束或者用了create而不是update_or_create。解决给goods_id加uniqueTrue入库统一走update_or_create。如果已经有一堆重复数据先写个脚本按goods_id分组去重。5.4 页面改版导致选择器失效现象之前能跑某天开始解析全空。原因目标页面结构调整CSS 选择器对不上了。解决把选择器集中写在utils.py顶部改版时只改一处解析时用try/except包住每个字段单个失败不影响整体定期手动跑一次验证。5.5 抓取频率过高触发限制现象跑着跑着请求全部失败或返回异常页面。原因请求间隔太短被判定为异常流量。解决页间延时拉到 2 到 4 秒单次重试延时 1 到 3 秒控制单次抓取页数别一次性抓几十页。毕设演示抓几页够用没必要贪多。6. 进阶技巧把比价数据用出花来跑通基础链路后这套系统还有不少可以深挖的地方。第一个是历史价格走势前面提到update_or_create会覆盖价格要做走势就得单独建一张PriceHistory表每次抓取插一条记录前端用折线图展示。这个改动不大但答辩时是个亮点。class PriceHistory(models.Model): goods models.ForeignKey(Goods, on_deletemodels.CASCADE) price models.DecimalField(max_digits10, decimal_places2) record_time models.DateTimeField(auto_now_addTrue)每次save_goods时顺手插一条历史记录查询时按goods分组按时间排序即可。第二个是抓取结果的校验。我一般会加一个简单的合理性检查价格大于 0 且小于某个上限比如 100000标题长度大于 5不符合的直接丢弃。这样能过滤掉解析错位产生的脏数据。校验项规则处理价格范围0 price 100000超出则丢弃标题长度len(title) 5过短则丢弃goods_id非空且为数字为空则丢弃第三个是给抓取加日志。别只用print用 Python 的logging模块把每次抓取的页数、成功条数、失败原因记下来出问题时翻日志比猜快得多。import logging logger logging.getLogger(spider) logger.info(抓取关键词%s 页数%s 入库%s, keyword, pages, len(items))从那以后我每次改完解析逻辑都会先手动跑一遍小批量抓取确认字段对得上再挂定时任务绝不直接上生产。这套系统本身不复杂难的是把每个环节的边界摸清楚。希望帮到你。本文还有配套的精品资源点击获取