简介:这套基于Python+Django的京东商品比价系统源码包,采用requests爬虫采集数据并附带数据库,专为毕业设计、课程设计与项目开发准备,实现了用户注册登录、商品收藏、15天价格折线图、每类商品降价幅度排行推荐以及一键跳转购买等功能。压缩包共2001个文件,大小16.2MB,其中1174个Python脚本(py/pyc)承载后端与爬虫逻辑,128个HTML页面搭配82个JS、19个CSS构成前端界面,88个mo/po为国际化语言文件,2个SQL文件用于数据库初始化,目录结构清晰,便于定位修改。项目代码经过严格测试,可放心参考并在此基础上扩展,已有75人学习下载。对于希望快速搭建电商比价场景、深入理解Django开发与爬虫实战的读者,这套源码连同数据文件、页面模板和翻译资源,提供了完整且可直接运行的全套素材,实践价值较高。
1. 京东商品比价系统:一个课设项目怎么把爬虫、Django 和价格历史串成闭环
京东商品比价系统这个题目,说白了就是“requests 抓京东商品数据,Django 做展示,数据库存价格历史”的一条完整链路。这里说的 request 就是 Python 的 requests 库,不是某个开源框架。它特别适合毕业设计或课程设计:评审老师要看到爬虫、要看到 Web 界面、还要看到数据库表,这套结构一个不落。这个项目最值得琢磨的不是爬虫写得有多猛,而是“比价”两个字——要能比价,就得有历史价格;要有历史价格,就得先把数据模型设计对。多数人在这里翻车,等到做了一半才发现价格没地方存历史,返工成本极高。
2. 数据库设计先行:商品表与价格快照表怎么建才能支撑比价
比价系统里最常见的错误,是把价格当成商品的一个普通字段,每次抓到新价格就 update 一次。这样数据库里永远只存在“最后一次价格”,前面抓到的全被覆盖了。比价这个需求,本质上要的是价格随时间的流水:今天 199,明天 179,后天 189,这些点连起来才是价格走势。所以我做这类项目,第一步不写爬虫,先定表结构。
2.1 为什么价格要单独建一张快照表,而不是直接改商品字段
价格快照表的设计思路和银行流水账一样:只插入,不修改。每次从京东抓到价格,就往快照表里新增一条记录,商品表本身只保存 sku、标题、店铺这类相对稳定的信息。这样做有三个直接好处:
- 随时能算“历史最低价”“平均价”“最新价”,三个查询全是聚合操作,不用依赖任何外部缓存。
- 可以按时间维度看波动。比如某商品大促前把价格抬高再折扣,这种小动作只有保留了历史记录才看得出来。
- 表结构更接近真实业务。电商平台的商品和报价本来就是一对多关系,按这个模型写出来的代码,答辩时也经得起追问。
反过来,如果图省事把价格字段放在 Product 表里,第一个星期还看不出问题,等你要画折线图、算涨跌幅的时候就会发现:只有当前值,没有历史值,后悔药都没得吃。快照表的代价是存储量会涨,但一个课设项目的量级完全不需要考虑这个问题,只要把抓取频率控制在合理范围就行。
2.2 Django 的 models.py 落地:核心字段与索引设计
明确了“商品表 + 价格快照表”的思路,就可以直接写 models.py 了。下面是我常用的简化版本:
from django.db import models class Product(models.Model): sku = models.CharField('京东SKU', max_length=32, unique=True) title = models.CharField('商品标题', max_length=255, db_index=True) shop = models.CharField('店铺', max_length=128, blank=True) url = models.URLField('商品链接') category = models.CharField('分类', max_length=64, blank=True) created_at = models.DateTimeField('首次入库', auto_now_add=True) class Meta: db_table = 'product' ordering = ['-created_at'] def __str__(self): return self.title class PriceRecord(models.Model): product = models.ForeignKey( Product, on_delete=models.CASCADE, related_name='price_records', verbose_name='商品' ) price = models.DecimalField('价格', max_digits=10, decimal_places=2) source = models.CharField('价格来源', max_length=16, default='jd') captured_at = models.DateTimeField('抓取时间', auto_now_add=True) class Meta: db_table = 'price_record' ordering = ['-captured_at'] indexes = [ models.Index(fields=['product', 'captured_at'], name='idx_prod_captured'), ] def __str__(self): return f'{self.product.sku}@{self.price}'有几个字段要专门说明。sku 是京东商品ID,链路里唯一能稳定标识商品的字段,所以必须加 unique=True,后续 get_or_create 都靠它去重。title 加了 db_index,因为按标题搜索商品是比价页面最频繁的操作,虽然课设数据量小感受不明显,但养成加索引的习惯没有坏处。
price 用 DecimalField 而不是 FloatField,这是做价格数据的一个原则。浮点数在计算机里是二进制的近似表示,0.1 + 0.2 都可能算出不精确的结果,涉及钱的数据一律用定点数。max_digits=10, decimal_places=2 足够覆盖京东绝大多数商品价格,如果你要抓大宗商品或批量采购价,可以适当放大 max_digits。
related_name 写成 price_records,方便反向查询,比如product.price_records.all()。on_delete=models.CASCADE 的意思是删掉商品时连同价格记录一起删,课程设计里够用;生产环境一般会改成 PROTECT,避免误删历史数据。
提示:如果你用的是 Django 4.1 之前的版本,Meta 里用 index_together = [('product', 'captured_at')] 也可以;新版推荐 indexes 写法,语义更清晰。
2.3 初始化数据库与后台管理:用 Django Admin 先看到数据
表定义好之后,先不要急着写爬虫,把数据库初始化和 Admin 后台配好,你后续调试会舒服很多。如果项目里还没有 app,先执行python manage.py startapp compare创建应用;已有的话直接改代码。然后在项目根目录依次执行:
python manage.py makemigrations compare python manage.py migrate python manage.py createsuperuser python manage.py runservermakemigrations 会根据 models.py 生成迁移文件,migrate 把表建到数据库里。createsuperuser 按提示输入用户名、邮箱和密码,然后再启动开发服务器,访问/admin就能登录后台。
为了让后台直接看到商品和价格记录,在 compare/admin.py 里写:
from django.contrib import admin from .models import Product, PriceRecord @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ('sku', 'title', 'shop', 'created_at') search_fields = ('title', 'sku') @admin.register(PriceRecord) class PriceRecordAdmin(admin.ModelAdmin): list_display = ('product', 'price', 'source', 'captured_at') list_filter = ('source', 'captured_at')开发阶段默认用 SQLite 最省事,零配置,一个文件就是整个库。如果课题要求用 MySQL,把 settings.py 里的 DATABASES 换成下面这样:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'price_compare', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }用 MySQL 时注意字符集要指定 utf8mb4,否则商品标题里的生僻字或特殊符号可能存不进去。平时排查数据,可以用 DB Browser for SQLite 或 dbx 这类数据库工具直接打开库文件看表,不用每次都进 Django shell。
如果项目文档里带了现成的数据库文件,优先直接导入后从 Admin 里翻一遍商品表和价格表,确认表结构和字段和你理解的一致,再开始写爬虫。这样后面所有解析代码都可以对照真实数据结构来写,不用瞎猜。
3. 京东爬虫的 requests 实现:请求构造、页面解析与价格补全
数据库结构定了,接下来做爬虫。这个环节的产出很简单:给定一个搜索关键词,返回一批商品字典,里面包含 sku、标题、价格、店铺、链接。爬虫本身不该关心数据库,它只负责把页面变成结构化数据,入库的事交给 Django 的 ORM 做。
先说清楚一个现实:京东这类电商站对爬虫并不友好,前端会做基础的 JS 动态渲染和风控来干扰静态抓取。requests 能稳定拿到的是首屏 HTML,价格有时藏在异步接口里,有时候干脆延迟加载。所以这个标题下的爬虫,重点不在“硬碰硬破解”,而是做好请求伪装、快速解析、以及价格缺失时的补全策略。
3.1 请求构造:User-Agent、Cookie 与搜索参数怎么设
我一般用 requests.Session 而不是裸的 requests.get,因为 Session 会自动管理 Cookie、复用 TCP 连接,连续抓取时的效率和请求成功率都会高一些。构造一次京东搜索请求,常见做法是这样的:
import requests import random import time HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' '(KHTML, like Gecko) Chrome/120.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://www.jd.com/', } COOKIES = { 'uuid': '你自己的cookie值', # 其他京东需要的 cookie 字段按需补充 } def build_session(): s = requests.Session() s.headers.update(HEADERS) s.cookies.update(COOKIES) return s def search_products(keyword, session=None): s = session or build_session() params = { 'keyword': keyword, 'enc': 'utf-8', 'wq': keyword, 'psort': '3', } resp = s.get('https://search.jd.com/Search', params=params, timeout=10) resp.raise_for_status() resp.encoding = 'utf-8' return resp.text这里的几个参数值得解释。User-Agent 至少要伪装成主流浏览器,不然很多站点第一步就返回 403。Cookie 不建议整个从浏览器复制粘贴,尤其是登录态很长的时候,复制过来反而容易触发风控,我会在浏览器里登录京东后,只挑必要字段带过来。params 里的 enc 和 wq 是搜索页自身需要的参数,psort 是排序参数,值 3 代表按销量排序,你按自己需求调。
提示:Cookie 有保质期,隔几天跑一次的系统,最好写一个检测逻辑:如果返回页面里出现“请登录”或验证码关键词,就提示手动更新 Cookie,而不是继续傻抓。
3.2 解析商品列表:用 BeautifulSoup 和正则抓 SKU 与标题
返回的 HTML 要用解析器处理。BeautifulSoup 适合拿结构化节点,正则适合从字符串里抠价格、ID 这类固定格式内容。解析商品列表我一般这样写:
import re from bs4 import BeautifulSoup def extract_price(text): if not text: return None cleaned = re.sub(r'[^\d.]', '', text) try: return float(cleaned) if cleaned else None except ValueError: return None def parse_product_list(html): soup = BeautifulSoup(html, 'html.parser') items = [] for node in soup.select('li.gl-item'): sku = node.get('data-sku') if not sku: continue title_tag = node.select_one('.p-name a') title = title_tag.get_text(strip=True) if title_tag else '' shop_tag = node.select_one('.p-shop a') shop = shop_tag.get_text(strip=True) if shop_tag else '' price_tag = node.select_one('.p-price') price = extract_price(price_tag.get_text(strip=True)) if price_tag else None items.append({ 'sku': sku, 'title': title, 'shop': shop, 'price': price, 'url': f'https://item.jd.com/{sku}.html', }) return items选择器 li.gl-item 对应搜索页里每个商品卡片,data-sku 是卡片的商品ID属性。这些 class 名是京东长期沿用下来的,但也可能随改版变化——写解析器时一定要先用 F12 看一下当前页面结构,不要照抄老教程死磕。
价格提取用正则把非数字和小数点的字符全部剔掉,¥1,299.00会变成1299.00。注意这里拿到的 price 是 float,最终入库时 ORM 会再转成 Decimal,不会有精度问题。如果页面结构变化导致一个商品都解析不出来,先别怀疑代码,把 resp.text 存成 html 文件,用浏览器打开看看是不是验证码页或者登录跳转页。
建议把 search_products、parse_product_list 这两个函数放到 compare/spider.py 里,后续定时任务、管理命令都可以直接 import 复用,不用到处复制粘贴。
3.3 价格补全:页面延迟加载时的处理手法与失败降级
京东搜索列表页的价格经常不是静态渲染的,requests 拿到的首屏 HTML 里可能没有价格节点。这种时候不要放弃整个商品,先保存 sku 和标题,价格走补全逻辑。常见做法是按 sku 去请求商品详情页,再从详情页里提取价格字段:
def fetch_price_by_sku(sku, session=None): s = session or build_session() url = f'https://item.jd.com/{sku}.html' try: resp = s.get(url, timeout=10) resp.encoding = 'utf-8' # 不同时期页面结构不同,优先找价格相关的 JSON 字段或 DOM 节点 m = re.search(r'["\']p["\']\s*:\s*["\']([\d.]+)["\']', resp.text) if m: return float(m.group(1)) soup = BeautifulSoup(resp.text, 'html.parser') price_tag = soup.select_one('.price, .p-price') if price_tag: return extract_price(price_tag.get_text(strip=True)) except requests.RequestException: pass return None这段代码的关键不是某一个正则,而是“逐级降级”的思路:先尝试页面内嵌的 JSON,再尝试价格节点,都拿不到就返回 None。补全失败的记录落到库里之后,可以靠后续抓取再补。我一般不会把 price=None 的记录写进价格表,因为 DecimalField 不允许空值;要么跳过这次写入,要么先记 0 再等后台任务修正。
补全逻辑单独抽一个函数,还有一个好处:你可以先跑一轮标题抓取快速入库,再跑一轮价格补全慢慢填数据。两件事解耦之后,哪一步挂了都容易排查。
3.4 限速、重试与失败退避:别让一台本机变成风控黑匣子
爬虫写完了,最忌讳的是立刻开 20 个线程并发抓。京东的风控对请求频率很敏感,短时间高频请求大概率换来滑块验证码。课程设计阶段,单线程加随机延时是最稳的方案:
import time import random def robust_get(session, url, retries=3, delay=(1, 3)): for attempt in range(retries): try: resp = session.get(url, timeout=10) if resp.status_code == 200: return resp except requests.RequestException as e: print(f'attempt {attempt + 1} failed: {e}') time.sleep(random.uniform(delay[0], delay[1]) * (attempt + 1)) return Noneretries 表示最多重试 3 次,delay 是每次失败后的基础等待时间,乘以“第几次失败”形成退避,比如第 2 次失败后等 2 到 6 秒。这样既不会在站点抖动时立刻放弃,也不会在失败后疯狂重试把风控阈值越撞越高。抓取循环里,每个商品之间建议至少间隔 1 到 2 秒。
4. 把抓回的数据变成比价结果:Django 视图、模板与价格计算
爬虫入库以后,数据是躺在数据库里的,比价系统要让人看得见结果。这个章节的任务是把“商品 + 价格记录”变成页面上的“最新价 / 历史最低价 / 平均价 / 涨跌幅”。页面不用做得多华丽,但三个计算维度必须准确,因为答辩时老师大概率会指着数字问:“这个值是怎么算出来的?”
4.1 视图层的比价计算:最新价、最低价、平均价与涨跌幅
比价计算放在视图层做,天然的思路是先取出商品,再逐条查询价格记录。数据量小的时候没问题,但更规范的做法是让数据库把聚合算好,视图只负责组织和展示。下面这段代码同时演示了两种方式:
from django.db.models import Min, Avg from django.shortcuts import render from .models import Product, PriceRecord def compare_view(request): keyword = request.GET.get('q', '').strip() products = Product.objects.all() if keyword: products = products.filter(title__icontains=keyword) result = [] for product in products[:50]: records = product.price_records.all() if not records.exists(): continue latest = records.order_by('-captured_at').first() today = latest.captured_at.date() # 同一天同一商品只保留最新一条,避免均价被高频时间段带偏 duplicated = records.filter(captured_at__date=today).exclude(pk=latest.pk) if duplicated.exists(): duplicated.delete() stat = records.aggregate(lowest=Min('price'), avg=Avg('price')) lowest_record = records.order_by('price', 'captured_at').first() if stat['lowest']: rate = (latest.price - stat['lowest']) / stat['lowest'] * 100 else: rate = None result.append({ 'product': product, 'latest_price': latest.price, 'latest_at': latest.captured_at, 'lowest_price': stat['lowest'], 'lowest_at': lowest_record.captured_at, 'avg_price': round(stat['avg'], 2) if stat['avg'] else None, 'rate': round(rate, 2) if rate is not None else None, }) result.sort(key=lambda x: (x['rate'] is None, x['rate'] or 0), reverse=True) return render(request, 'compare/result.html', { 'result': result, 'keyword': keyword, })这段代码里最关键的是聚合查询。records.aggregate(lowest=Min('price'), avg=Avg('price'))会在 SQL 层一次性算出最低价和平均价,而不是把所有记录拉到 Python 里再算,价格记录多了以后性能差距会非常明显。records.order_by('price', 'captured_at').first()取的是“价格最低且最早出现”的那一条,这样能拿到最低价出现的时间点。
关于删除重复记录:同一商品在一天内可能被定时任务抓多次,如果每次都写一条记录,均价会被高频时间段带偏。这里先按抓取时间倒序取当天最新一条,再把同一天的其他记录用 delete() 清掉。这是 Django 删除对象的标准姿势,查询集调用 delete() 即可。注意这段去重逻辑演示目的比较强,生产环境建议放到定时任务里,而不是放在页面访问链路中。
涨跌幅的计算口径是自己定义的。我这里用“(最新价 - 历史最低价)/ 历史最低价”,反映当前价格比最低点贵了多少。也有人用“最新价对比前一天价格”,你想换口径只改一行公式,但必须在页面或注释里写清楚,免得答辩时口径不一致被问倒。
4.2 模板渲染:商品卡片、价格对比与“去京东”外链
计算好结果之后,模板层只做展示。比率计算已经在视图完成,模板不要再去写复杂逻辑。一个商品卡片大概长这样:
<!-- templates/compare/result.html --> <div class="product-grid"> {% for item in result %} <div class="card"> <h3>{{ item.product.title }}</h3> <p class="price"> 最新价格:<strong>{{ item.latest_price }}</strong> <span class="muted">抓取于 {{ item.latest_at|date:"m-d H:i" }}</span> </p> <p>历史最低:{{ item.lowest_price }}({{ item.lowest_at|date:"Y-m-d" }})</p> <p>平均价格:{{ item.avg_price }}</p> {% if item.rate is not None %} <p class="trend">较历史最低 {{ item.rate }}%</p> {% else %} <p class="trend">价格数据不足,无法计算涨跌幅</p> {% endif %} <a href="{{ item.product.url }}" target="_blank" rel="nofollow noopener noreferrer">去京东查看</a> </div> {% empty %} <p>没有找到相关商品,换个关键词试试。</p> {% endfor %} </div>模板里的|date:"Y-m-d"是 Django 内置的日期格式化过滤器,{% empty %}是 for 循环没结果时的兜底。价格数据不足的判断必须在视图里先做好,模板只区分“有值”和“没有值”两种情况,这是模板层的使用原则:只展示,不计算。
商品外链加rel="nofollow noopener noreferrer"是基本的安全习惯,这样跳转时不会携带来源信息,也能防止目标页面反向操作你的页面。想加价格走势图的话,最简单的方案是给每个商品生成一个折线图接口,返回最近 30 天价格数组,前端用 ECharts 画;课程设计阶段如果时间紧,先用涨跌幅文字也完全够用。
5. 京东爬虫避坑清单:五个真实翻车点与对应解法
这一章写的都是我自己在类似项目里踩过的坑。爬虫这个方向有个特点:代码报错不可怕,可怕的是代码不报错、数据却是错的。你盯着数据库看半天也找不出问题,最后发现是页面结构变了或者 Cookie 早过期了。下面五条按“现象 → 原因 → 解决”的顺序写,遇到类似症状直接对照排查。
5.1 现象:抓回来的价格全是 0.00 元,数据库里一片零
这是 requests 抓京东最常遇到的情况。打开价格表一看,captured_at 时间是新的,price 全是 0.00,仿佛爬虫正常工作但什么都没爬到。原因几乎都是同一个:搜索页的价格是异步加载或延迟渲染的,requests 拿到的首屏 HTML 里根本不含价格节点,你从价格节点里取文本自然取不到任何数字。
解决方法是先加日志确认:把每个商品解析结果打印出来,看 title 是否正常、price 是否为 None。title 正常但 price 为 None,就说明是价格补全的问题,而不是整个解析器失效。然后按上一章的 fetch_price_by_sku 补全逻辑走,先入库再用详情页价格回填。别把“没抓到”和“真便宜到 0 元”混在一起,价格字段要么不写,要么写成 None 并留状态标记,宁可后续补,也不要拿 0 污染平均值。
5.2 现象:请求返回验证码页面或直接 403,连标题都拿不到
可能是一次性抓太多,也可能是没有带 Cookie 直接裸请求。403 比较好理解,服务端拒绝访问;更隐蔽的是返回 200,但页面内容是验证码或登录跳转页,解析结果为空,代码不报错。我一般会在爬虫入口处做一个页面校验函数:
def is_blocked(html): return ('验证码' in html or '请登录' in html or '访问过于频繁' in html)原因基本是访问频率过高触发风控,或者 Cookie 过期导致被踢回登录态。解决方法是降频、减少单批次数量、随机延时,以及检查 Cookie 是否还有效。如果已经触发滑块验证码,光改代码没用,等一两个小时让风控降温,或者换一个网络出口再跑。
5.3 现象:价格字符串混着逗号和 HTML 标签,Decimal 转换直接炸掉
从页面取出来的价格文本经常是¥1,299.00,中间还有不间断空格和换行。直接塞进 DecimalField,Django 大概率抛 ValueError 或 InvalidOperation。这就是没有做数据清洗就入库的结果。
解决方法是统一走 extract_price 函数,正则r'[^\d.]'把所有非数字和小数点的字符全部剔除。要注意商品价格有的是纯整数1299,有的是1,299.00,还有的是1999起这种促销文案,清洗后还要再做一次 float 转换校验,失败就返回 None,让上层决定跳过还是补全。
5.4 现象:中文全部乱码,商品标题变成“锘挎弧”这类内容
乱码的最常见原因是编码识别错误。京东搜索页的响应头通常声明了 utf-8,但如果你用 resp.text 时没有设置 encoding,requests 会先按响应头解析;一旦响应头缺失或者声明了 gbk,而实际内容是 utf-8,标题就会变成乱码。
解决方法是拿到响应后立刻按页面 meta 声明的字符集设置:
resp = session.get(url, timeout=10) resp.encoding = resp.apparent_encoding or resp.encodingapparent_encoding 是 requests 根据字节内容推断出的编码,比固定写死 utf-8 更稳。注意这种推断有少量性能开销,只对文本页面用就好。还有一种乱码来自数据库连接,MySQL 没配 utf8mb4,那种乱码是入库后才出现的,用第 2 章的 OPTIONS charset 配置可以避免。
5.5 现象:报错 request header is too large,请求被服务端直接拒绝
这个报错看起来像网络问题,实际是请求头超出服务端限制。很多人图省事,把浏览器 DevTools 里整个 Request Headers 原封不动复制到代码里,连 Cookie 串一起带上,几十个字段加起来可能超过 8KB。服务端为了避免恶意请求和前端防爬,通常会对请求头大小设上限,超过了直接拒绝,表现就是 400 或 413。
解决方法是精简请求头。爬虫只需要 User-Agent、Accept、Accept-Language、Referer 这四五个关键字段,Cookie 也只保留必要项。另外注意别把大段 JSON 或 base64 内容塞进 GET 参数里,GET 的 URL 长度本身也有限制,超长链接同样会被服务端以 413 request entity too large 拒绝。记住一个原则:请求越轻,越不容易被拦。
6. 一条命令验证整套系统:从搜索到比价结果的端到端巡检
比价系统的功能点分散在爬虫、数据库和视图三层,要验证“能不能跑通”,最快的方式是用 manage.py shell 把整条链路一次性走一遍。这一步会把搜索、入库、聚合计算串起来,任何一个环节断了都能立刻看到报错位置。
6.1 用 manage.py shell 把爬虫、入库、比价串起来跑一遍
在项目根目录执行下面这段脚本,注意把 compare 换成你的实际 app 名:
python manage.py shell <<'EOF' from compare.spider import search_products, parse_product_list from compare.models import Product, PriceRecord from django.db.models import Min, Avg keyword = '蓝牙耳机' html = search_products(keyword) # 第3章的搜索函数 items = parse_product_list(html) # 第3章的解析函数 for item in items[:20]: product, _ = Product.objects.get_or_create( sku=item['sku'], defaults={'title': item['title'], 'url': item['url'], 'shop': item.get('shop', '')} ) if item['price']: PriceRecord.objects.create(product=product, price=item['price']) qs = Product.objects.filter(title__icontains=keyword).annotate( lowest=Min('price_records__price'), avg=Avg('price_records__price'), ) for p in qs[:10]: latest = p.price_records.order_by('-captured_at').first() print(p.sku, p.title, latest.price, p.lowest, p.avg) EOF这个脚本用 get_or_create 保证同一 sku 只建一条商品记录,第二次跑的时候商品表不会重复插入;价格表每次新增快照,这样曲线数据才会越来越厚。print 输出里应该能看到 sku、标题、最新价、最低价和平均价五项,如果最后几行都是 None,回去查爬虫这层的价格补全。
链路通了之后,剩下的就是让它持续跑。常见做法是写一个 Django management command 包裹上面的逻辑,然后交给系统定时任务,每两小时抓一轮。跑上两三天,价格历史有了起伏,比价页面的涨跌幅和趋势才真正有意义。这套结构和京东绑定得很紧,但换个目标站点只需要改解析器和字段映射,骨架可以直接复用。我自己第一版做类似系统时,把价格直接放在商品表里,做到一半发现折线图画不出来,最后推倒重来。后来只要涉及价格走势,一律先建快照表,这已经是我的默认习惯。希望帮到你。
本文还有配套的精品资源,点击获取