ARTICLE DETAIL

资讯详情

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

京东商品比价系统:Django+requests爬虫实战与避坑指南

京东商品比价系统:Django+requests爬虫实战与避坑指南 简介基于Python与Django框架开发的京东商品比价系统利用requests库编写爬虫抓取商品数据附带完整源码与数据库文件面向毕业设计、课程设计及项目开发场景也适合系统学习电商类Web项目的开发者参考。系统实现注册登录、商品收藏、十五天内价格折线图展示、按品类推荐降幅最大商品及跳转购买等功能覆盖电商比价类项目的核心业务模块。资源压缩包共2001个文件大小约16.2MB除Python源码外还包含Django模板、HTML页面、前端JS与CSS样式、SQL数据库脚本以及JSON/XML配置和多语言mo/po翻译文件类型相对完整可直接导入运行并在此基础上扩展。源码经过严格测试稳定性有保障涵盖爬虫采集、数据存储、价格走势可视化与推荐逻辑等关键模块可作为课程设计、毕设演示或企业级比价系统的参考蓝本并快速延展使用。目前已有75人学习下载适合具备一定Python与Django基础、希望快速搭建完整比价项目的开发者使用。1. 京东商品比价系统为什么说它是 Django 爬虫项目最稳的练手模板“京东商品比价系统”这个名字看着像标准课程设计模板但它其实就是一条完整的 Python Django requests 爬虫生产链路requests 从京东搜索页抓商品标题和价格Django 把数据落到 MySQL再用模板把结果按价格排序展示出来。对要交毕业设计、课程设计的人来说它把网络爬虫、Django ORM、视图与模板、后台管理四块内容一次性串了起来比单独写爬虫脚本或者只做 CRUD 更有答辩素材。我拆这类资源包的习惯是先看它能不能跑通再看里面哪些代码值得改成自己的写法。这篇就按这个顺序写先讲清楚系统结构和数据表设计再带着你从环境配置跑到爬虫入库最后把反爬、编码、数据库这几个最容易翻车的点单独拎出来。拿到资源包后你完全可以照着这篇重新走一遍而不是停留在“能打开项目”的阶段。2. 系统架构与数据表设计先搞懂比价数据的来龙去脉2.1 MVT 分工与 requests 爬虫任务的边界Django 的 MVT 结构一旦拆开看这套比价系统的职责边界就非常清楚模板负责展示商品卡片和价格表格视图负责接收搜索关键词、查询数据库并返回页面模型负责定义商品表和价格记录表。剩下最敏感的爬虫逻辑往哪里放才是很多课程设计做得最乱的地方。我见过不少同学把 requests 的抓取代码直接写在 views.py 里视图每被访问一次就去抓一次京东。这种做法有两个直接后果一是页面响应时间会被拖到十几秒用户搜索一个关键词要等很久二是搜索请求稍微多点就会同时发出大量外部请求京东的反爬拦截概率成倍上升。所以我在拆这套源码时会特别看它的爬虫模块是不是独立存在的。常见做法是单独建一个 crawler.py 或者 utils 目录视图只负责读数据库爬虫只负责写数据库两者通过表来解耦。requests 和 Scrapy 的选型也是同理。如果这是生产级大规模采集Scrapy 的引擎、调度器、中间件值得用但这种比价系统的抓取量不大requests 写出来的代码是线性流程请求、响应、解析、入库每一步都摆在明面上调试时不容易出玄学问题。答辩时老师问“爬虫怎么实现”你只需要指着代码说清楚每行在做什么就行。这也是为什么这类毕业设计资源普遍带的是 requests 而不是 Scrapy。2.2 商品表和价格记录表字段、类型与索引这么定比价系统的核心是两张表一张存商品当前信息一张存价格历史记录。如果只存一张商品表那比价就退化成“静态商品列表”无法体现历史价格变化答辩时也少了一个可以展开讲的技术点。所以拿到源码包后先打开 models.py看看是不是有类似下面这种拆分。商品表至少要包含商品 ID、标题、链接、图片、店铺名、当前价格。价格记录表则用外键关联商品记录每次抓到的价格和时间点。为什么要拆两张表因为“当前价格”和“历史价格”的查询模式完全不同当前价格要高频读取历史价格要按时间范围查询并可能画趋势图。如果都塞进一张表每跑一次爬虫就插几万行新记录商品表会无限膨胀列表页查询也会越来越慢。字段类型上有个常见的坑价格不要用 FloatField要用 DecimalField。浮点数在 MySQL 里的存储精度会随计算飘移比价系统的核心逻辑就是比较价格一旦出现 49.100000001 这种脏数据排序结果就是错的。DecimalField 的 max_digits 给 10decimal_places 给 2足够覆盖大多数商品价格。下面是我习惯的模型写法也是这套资源里最应该先看的文件# price_app/models.py from django.db import models class Product(models.Model): sku_id models.CharField(max_length32, uniqueTrue) # 京东商品ID唯一约束 title models.CharField(max_length255, db_indexTrue) # 商品标题 url models.URLField() # 商品链接 image models.URLField(blankTrue) # 图片地址 shop models.CharField(max_length128, blankTrue) # 店铺名 price models.DecimalField(max_digits10, decimal_places2, default0) # 当前价格 updated_at models.DateTimeField(auto_nowTrue) # 最后更新时间 class Meta: ordering [price] # 列表页默认按价格升序 class PriceRecord(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameprice_records) price models.DecimalField(max_digits10, decimal_places2) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at]这段代码里有两个必须注意的细节。一个是 sku_id 设了 unique这既保证了商品不会重复入库也是后续 update_or_create 的查询条件。另一个是外键的 related_name 设成了 price_records这让你能从商品对象直接取历史价格写法是 product.price_records.all()。如果漏掉 related_nameDjango 默认会给一个 price_record_set 的名字读起来别扭命令行调试时也容易敲错。索引方面商品表的 title 建议加 db_index因为比价系统最常见的入口就是关键词搜索标题模糊查询必须走索引。PriceRecord 表的 created_at 也建议建索引因为后面按时间范围查价格趋势时没有索引的扫描会非常慢。外键字段 product_id 在 Django 里默认会自动建索引不用手动加这一点和原生 MySQL 的习惯略有差异。2.3 从搜索词到比价页面一条完整的数据流现在把整条链路穿起来用户在页面输入“无线鼠标”并提交URL 变成 /search/?q无线鼠标。视图函数拿到关键词后先去数据库检查有没有近期的商品缓存有就直接读库没有就调用爬虫模块抓取京东搜索页。爬虫解析出商品标题、链接、价格、店铺名后用 update_or_create 写入商品表同时往价格记录表补一条快照。最后视图把商品列表按价格升序渲染到模板用户看到的就是一个低到高排列的比价表。这个流程还说明了一个容易被忽视的点为什么非要数据库如果把抓取结果直接存在内存变量里进程重启数据就没了而且多个用户同时搜索时会相互污染。有了数据库爬虫和页面展示就完全解耦了你甚至可以白天用 Django 后台看结果晚上定时跑爬虫第二天早上再打开页面就是新的比价数据。比价系统里“价”是核心历史记录是灵魂这两样都得靠数据库持久化。3. Django 侧复现从创建 app 到数据库迁移跑通项目3.1 环境准备虚拟环境、依赖与项目解压位置拿到源码包后第一件事不是急着开 IDE而是先把项目目录结构看清楚。一个标准的 Django 课程设计包解压后根目录应该能看到 manage.py、项目配置目录、业务 app 目录以及一份数据库导出文件。manage.py 是 Django 项目所有命令的入口后面所有操作几乎都从它开始。我一般会先创建虚拟环境避免依赖跟系统全局 Python 冲突。Python 版本建议用 3.8 到 3.11太新的版本有时候会遇到个别依赖包还没兼容的情况。下面这组命令在 Windows 和 Linux 下都适用只是激活命令略有不同# 创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows PowerShell: venv\Scripts\activate # macOS / Linux: source venv/bin/activate # 安装依赖 pip install -r requirements.txtrequirements.txt 是源码包必须带的文件里面通常会声明 Django、requests、lxml、mysqlclient 等依赖。如果包里没带你可以根据实际用到的包手动安装requests 用来抓页面lxml 用来做 xpath 解析mysqlclient 或 pymysql 用来连 MySQL。安装完成后先执行 python manage.py check这一步能快速发现配置层面的错误不用等到运行服务器时再看到一堆 Traceback。3.2 settings 配置数据库、时区与主机名Django 项目能不能跑起来一半取决于 settings.py。这套比价系统的 settings 里有三个地方必须检查ALLOWED_HOSTS、DATABASES、TIME_ZONE。本地调试时 ALLOWED_HOSTS 可以直接放空或写 [*]但如果部署到服务器一定不要用星号否则会被扫到后更容易受到恶意请求。数据库配置是另一个关键点。如果源码包带的是 MySQL 导出文件你需要修改 DATABASES 里的连接信息。常见配置如下# settings.py 中的关键配置 ALLOWED_HOSTS [*] # 本地调试可以放开部署后按域名收紧 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: jd_price, # 数据库名 USER: root, # 数据库账号 PASSWORD: 你的密码, # 数据库密码 HOST: 127.0.0.1, # 本机连接 PORT: 3306, OPTIONS: {charset: utf8mb4}, } } TIME_ZONE Asia/Shanghai USE_TZ False # 本地比价系统中关闭 UTC 时间避免 datetime 混乱这里 USE_TZ 的设置直接影响爬虫入库时间。很多课程设计会在描述时间时出现“差 8 小时”的问题就是因为 Django 默认开启 UTC而爬虫写入的时间被当成 UTC 存储。比价系统要记录价格快照的历史时间点我建议直接设成 USE_TZ False让数据库存当地时间后续展示不用再做换算。如果包里直接使用的 SQLite就不需要配 MySQL。但要注意课程设计场景通常要求用 MySQL因为答辩时老师会问数据库设计。SQLite 只是本地验证阶段最方便的选择。3.3 用 migrate 和 createsuperuser 完成初始化这里有两个初始化路线只能选一种要么导入源码包自带的 SQL 文件要么执行 Django 迁移。最忌的是先导入 SQL又执行 migrate最后报“表已存在”的错误。我一般先看有没有 .sql 文件如果有就先把数据库建好再导入。MySQL 下建议先创建数据库并指定字符集然后再导入导出的 SQL 文件否则中文会乱码# 登录 MySQL 创建数据库 mysql -u root -p -e CREATE DATABASE jd_price DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 导入指定 SQL 文件 mysql -u root -p jd_price --default-character-setutf8mb4 jd_price.sql导入成功后你只需要执行创建超级用户这一步不需要再 migrate。如果源码包没有提供 SQL 文件而是迁移文件已经存在那就走标准流程python manage.py makemigrations python manage.py migrate python manage.py createsuperusercreatesuperuser 需要依次输入用户名、邮箱、密码。邮箱可以随便填但密码不能太简单否则 Django 会直接拒绝。完成后运行 python manage.py runserver访问 http://127.0.0.1:8000/admin 登录后台就能看到商品表和价格记录表了。到这一步项目基础已经通了接下来才是整个系统最有分量的部分爬虫怎么抓、怎么解析、怎么入库。4. requests 爬虫实战请求头、解析与入库的完整写法4.1 请求参数怎么设headers、timeout、重试与编码爬虫能不能稳定抓数据一半看请求头。京东对请求头十分敏感只用 requests.get 裸请求大概率拿到验证码页。常见的做法是把浏览器开发者工具里的请求头完整拷贝下来。至少要有 User-Agent、Accept、Accept-Language如果是搜索页Referer 也要带上。下面这个配置可以当作起点# crawler/request_helper.py import requests import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry 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, Accept: text/html,application/xhtmlxml,application/xml;q0.9, image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.jd.com/, } def get_session(): session requests.Session() # 自动加一个重试机制避免偶发网络抖动直接挂掉 retry Retry(total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504]) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) return session这里面有两个参数值得细说。timeout 是每次请求必须设置的建议 10 秒。不设 timeout 的话一旦京东那边连接不返回requests 会一直等下去爬虫任务会卡死。重试用 urllib3 的 Retry只重试 3 次退避时间 0.5 秒起步避免在服务端已经返回 500 的情况下不断轰炸。如果你用的是简单的 requests.get也可以在 except 里自己写 sleep 再循环效果类似。最后一个编码细节requests 有时解析 Content-Type 里的 charset 不准尤其是京东页面内容较大时。稳妥做法是拿到响应后手动指定 resp.encoding utf-8再取 resp.text。如果忽略这一步你可能会在后面的 xpath 解析里看到一堆乱码标题。4.2 商品信息解析用 xpath 取标题、价格和链接页面抓回来后下一步是解析。这套系统常见的是用 lxml 的 xpath 做解析。xpath 的优势是浏览器开发者工具里可以直接 copy而且 text() 函数的用法非常直观。但我要先给个心理预期京东搜索页的 DOM 结构经常改版源码包里写好的选择器不一定永远有效所以拿到包后第一件事是用一个简单脚本验证当前页面结构。# crawler/parser.py from lxml import etree def parse_search_page(html): selector etree.HTML(html) items [] # 商品列表项的选择器京东现在大体还是 li 元素带 gl-item 类 # 如果返回空先在浏览器里按 F12 查看当前页面结构再回来改这里 for node in selector.xpath(//li[contains(class,gl-item)]): title node.xpath(.//div[contains(class,p-name)]/a/title) url node.xpath(.//div[contains(class,p-name)]/a/href) if not title: continue title title[0].strip() href url[0].strip() if href.startswith(//): href https: href items.append({ title: title, url: href, }) return items这段代码里 xpath 的contains(class,gl-item)是经典写法因为 class 可能是gl-item或gl-item后面跟其他标识。/a/title是拿商品标题href是拿链接。这些结果都是字符串数组xpath 不会自动帮你取第一个所以要用[0]再 strip 一下。价格这一项我单独说。新版京东搜索页的价格往往不是静态 HTML直接在列表项里用 xpath 找.//div[contains(class,p-price)]可能拿不到值因为价格是懒加载后由 JavaScript 渲染出来的。常见做法有两种一种是从页面内嵌的全局参数里用正则提取 JSON另一种是拿到商品链接后去商品详情页再抓价格。很多课程设计为了节省成本直接在列表页用“快照数据”但那样会导致价格字段长期为空。下面这种正则提取 JSON 的方法是更稳的# crawler/parser.py 补充 import re import json def extract_page_json(html): # 页面里往往有一段 window.pageConfig {...} 这样的全局变量 pattern rwindow\.pageConfig\s*\s*({.*?}); match re.search(pattern, html, re.S) if not match: return None try: return json.loads(match.group(1)) except json.JSONDecodeError: return None这段代码的实用性在于不依赖具体 DOM 层级京东只要还在页面里输出 window.pageConfig这一类数据就能解析到。拿到 JSON 后再按字段名去找商品 ID、价格、名称比硬磕 xpath 稳定得多。调试时可以先 print 一下 JSON 的 keys看有没有你要的数据再决定字段路径。4.3 入库与去重Django ORM 的 update_or_create 用法解析出商品列表后不要直接批量 create 到数据库因为同一件商品会在多次抓取中反复出现。正确的写入姿势是 update_or_create按 sku_id 去查存在就更新标题、价格、店铺不存在就新建。同时每次抓取都往 PriceRecord 表插一条价格快照这样就保留了历史价格曲线。from price_app.models import Product, PriceRecord def save_product(item): product, created Product.objects.update_or_create( sku_iditem[sku_id], defaults{ title: item[title], url: item[url], image: item.get(image, ), shop: item.get(shop, ), price: item[price], } ) # 不管商品是新是旧价格快照都要留一条 PriceRecord.objects.create(productproduct, priceitem[price]) return product, createdupdate_or_create 的语义是“按关键字查询找不到就创建找得到就更新 defaults 里的字段”。这里关键字用的是 sku_id因为它在 models 里设了 unique这是保证去重的关键。如果你用普通字段做查询条件比如 title那标题稍微改半个字就会重复入库比价结果会越来越乱。给 price 用 DecimalField 之后写库时直接传字符串价格也能自动转例如调 PriceRecord.objects.create(price69.90)。但如果解析时拿到的是一个像“69.90”的字符串必须先过滤掉非数字字符否则 Decimal 类型会直接抛异常。具体做法可以这样re.sub(r[^\d.], , price_text)。这个坑在爬虫入 clip 的时候几乎必踩。5. 避坑指南京东反爬、编码和数据库同步的五个翻车现场5.1 请求头不全被京东直接拦截现象requests.get 返回 200但页面内容不是商品列表而是一段“访问过于频繁”的提示或者干脆返回 403 Forbidden。原因请求头只有默认的 Python-urllib京东的防爬规则会立刻识别出这不是浏览器访问。缺 User-Agent、Accept-Language、Referer 任何一个都可能被判为异常请求。解决把浏览器开发者工具里看到的完整请求头复制下来至少补全 User-Agent、Accept、Accept-Language、Referer。再用 requests.Session 发起请求Session 会自动保存 Cookie能维持一定的连续性。如果仍然被拦截检查是不是当前 IP 请求次数太密集在两次请求之间加 time.sleep(3) 到 time.sleep(5) 的随机延迟。5.2 价格解析出乱码或拿不到值现象标题和链接都解析正常但价格列表是空的或者取到的价格是一串“9?9.00”这样的乱码。原因一个是页面编码没指定requests 默认从响应头推断编码推断错误就会乱。另一个是价格由动态脚本渲染静态 HTML 里根本没有对应文本xpath 自然取不到。解决拿到响应后手动设置 resp.encoding utf-8。如果静态页里没有价格改用 4.2 节里的正则提取 window.pageConfig JSON。提取价格文本后一定要用 re.sub 过滤掉“”和空格再交给 DecimalField。乱码问题还可能是页面本身是 gzip 压缩响应requests 会自动解压但如果直接拿到的是二进制内容先检查响应头里的 Content-Encoding。5.3 Cookie 过期和请求太频繁导致 IP 被封现象爬虫跑了十几分钟后突然所有请求都返回 302 跳转或者跳到一个安全验证页面之后无论怎么改 headers 都拿不到数据。原因京东会记录同一 IP 的访问频率连续高频请求会触发临时风控封禁。另外搜索页会下发关键 CookieCookie 一旦过期商品接口就拒绝返回数据。解决控制抓取频率每次请求之间随机 sleep 3 到 8 秒避免固定间隔。Session 里的 Cookie 要保存下来如果一段时间后失效就重新发起一次首页请求刷新 Cookie。还有一个更容易做到的办法是把抓取量控制在前 1 到 2 页课程设计场景通常不需要全量数据够展示功能就行。如果已经被临时封禁停止抓取半小时左右一般会自动恢复。5.4 数据库迁移报错和导入导出乱码现象先导入了源码包的 SQL 文件又执行 python manage.py migrate结果报 “table already exists” 或者 “relation already exists”。另一个场景是从 MySQL 导出 SQL 后导入到自己的机器中文全变成问号。原因导入 SQL 和 migrate 是两条初始化路线混用就会表结构冲突。中文乱码则是字符集不匹配数据库、表、连接字符串三处字符集必须一致。解决选一条路线走到底。用了 SQL 文件就把迁移文件当成参考不要再执行 migrate用 migrate 就别导入 SQL。字符集方面统一使用 utf8mb4创建数据库时指定 DEFAULT CHARACTER SET utf8mb4导入时用 --default-character-setutf8mb4 参数。如果已经乱码把数据库表删掉重新导入用 data 修复基本不划算。5.5 项目在自己电脑正常部署到服务器后样式全丢现象本地 python manage.py runserver 一切正常复制到云服务器后Django 后台和前台页面都没有 CSS控制台一堆 404指向 /static/ 路径。原因DEBUGTrue 时 Django 会帮你托管静态文件一旦 DEBUGFalseDjango 就不会再处理静态文件请求必须由 nginx 或 whitenoise 来托管。解决如果只是想临时演示把 settings 里的 DEBUG 保持为 True如果想正式部署安装 whitenoise并在 MIDDLEWARE 里加上 whitenoise.middleware.WhiteNoiseMiddleware同时执行 python manage.py collectstatic 把所有静态文件收集到 STATIC_ROOT。比价系统本身没有很重的静态资源用 whitenoise 是最省事的方案。这一条对毕业设计答辩演示尤其重要否则你在老师电脑上打开没有样式的页面第一印象就打折了。6. 进阶技巧把比价结果从“能看”变成“能用”6.1 用 Django 管理命令做定时抓取页面能跑通、爬虫能入库这套系统只完成了一半。真正的比价系统需要定时更新价格否则跑一次就停数据很快就会过期。我建议把爬虫入口写成一个 Django 管理命令这样既可以用命令行手动触发也能挂到系统定时任务里。在业务 app 下新建 management/commands/fetch_prices.py内容大致如下from django.core.management.base import BaseCommand import time from crawler.parser import parse_search_page from crawler.request_helper import get_session from price_app.models import Product, PriceRecord class Command(BaseCommand): help 抓取指定关键词的京东商品价格 def add_arguments(self, parser): parser.add_argument(keyword, typestr) def handle(self, *args, **options): session get_session() html session.get( https://search.jd.com/Search, params{keyword: options[keyword]}, timeout10, ).text items parse_search_page(html) # 这里只是演示入口实际要循环处理每个 item time.sleep(3) self.stdout.write(f解析到 {len(items)} 条商品)这样你在项目根目录执行 python manage.py fetch_prices 无线鼠标就能把这一个关键词的最新价格抓进库。再配合 cron 或 Windows 任务计划每天跑一次比价数据就会持续更新。Be careful任务计划里一定要进入虚拟环境再执行路径写绝对路径否则定时任务会找不到 Django 环境。6.2 用缓存把列表页提速爬虫入库频率低但用户访问列表页的频率可能很高。每次访问都去数据库做模糊查询数据量大了以后会明显变慢。我一般会给搜索页加 Django 自带的缓存使用 cache_page 装饰器按查询参数缓存结果比如from django.views.decorators.cache import cache_page cache_page(60 * 30) # 缓存 30 分钟 def search_view(request): keyword request.GET.get(q, ) # 查询数据库并渲染模板这段代码的重点是过期时间设成 30 分钟正好和定时抓取的频率匹配用户看到的数据不会太旧也不会频繁触发查询。默认缓存后端是本地内存演示够用如果放服务器建议换成 Redis 或数据库缓存。最后说一个我的习惯拆这类课程设计包我从来不会第一时间埋头跑 runserver而是先打开 models.py 和爬虫解析函数确认表结构与自己预期一致再用一个最小关键词验证页面结构。有一次我跳过这一步直接跑包结果因为京东改版解析器拿不到价格我以为是源码问题折腾了一晚上才发现是选择器过期。从那以后每次拿到课程设计源码我都强制先做“表结构 选择器”两个体检再谈后续功能。这套 Django requests 的比价系统也不例外希望你拿到手后少走一点我走过的弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表