
简介基于Python与Django实现的电商商品比价推荐系统面向电商数据分析、爬虫开发及推荐算法学习者解决多平台商品数据采集、价格对比与智能推荐落地问题。资源包共39个文件以31张项目运行截图为主附crawler.py爬虫脚本、ReduceByKeySortRddDemo.scala、JedisUtil.java等代码以及说明文档、README、附赠论文便于对照界面效果理解系统架构与数据流。包体仅3.48MB轻量易获取目前已有98人学习下载。内容涵盖京东、苏宁、淘宝、国美等平台爬虫采集、价格走势预测与基于深度学习算法的个性化推荐并展示Django后台管理、前端交互与数据存储实现适合毕业设计参考或作为电商推荐系统实战入门。价值点在于完整源码与运行截图、数据采集脚本、Scala/Java辅助工具及文档资料可支撑二次开发与论文撰写。1. 电商比价与推荐系统的真实工作量分布爬虫只占三成把一个多平台比价与推荐系统拆开来看最花时间的往往不是爬虫本身而是数据归一化、历史价格的时间序列建模和召回排序。爬虫部分用 requests BeautifulSoup 就能解决难点在于京东、淘宝、苏宁、国美各自页面结构差异很大价格字段出现的入口、格式和反爬级别都不一样真正决定系统能不能用的是 Django 里那几张数据表怎么设计、SPU 怎么归并、价格快照怎么存、推荐特征怎么构造。这篇文章按数据层、采集层、分析层、预测层四条线把一个可落地的方案讲清楚。适合打算用这个题目做毕业设计或自建比价服务的开发者对有爬虫经验但没搭过完整系统的工程师也有值得看的数据建模和排序参数。2. Django 数据模型与多平台爬虫采集层设计2.1 先定位比价系统的核心数据模型SPU、SKU 与价格快照比价系统的第一张表不是商品表而是一张把「同一个商品在不同平台的销售单元」关联起来的映射表。京东的商品叫 sku_id淘宝对应的是 item_id苏宁和国美的叫法又不一样直接用 URL 去重不可靠必须抽象出 SPUStandard Product Unit作为跨平台商品的统一标识。# products/models.py from django.db import models class Platform(models.Model): code models.SlugField(uniqueTrue) # jd/taobao/suning/guomei name models.CharField(max_length32) class Product(models.Model): spu_code models.CharField(max_length64, uniqueTrue) title models.CharField(max_length255) brand models.CharField(max_length64, db_indexTrue) category models.ForeignKey(Category, on_deletemodels.PROTECT) created_at models.DateTimeField(auto_now_addTrue) class ProductPlatform(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE) platform models.ForeignKey(Platform, on_deletemodels.CASCADE) sku_id models.CharField(max_length64) # 平台侧的原始商品ID url models.URLField() is_active models.BooleanField(defaultTrue) class Meta: unique_together (product, platform, sku_id) class PriceSnapshot(models.Model): pp models.ForeignKey(ProductPlatform, on_deletemodels.CASCADE) price models.DecimalField(max_digits12, decimal_places2) promotion_price models.DecimalField(max_digits12, decimal_places2, nullTrue, blankTrue) crawled_at models.DateTimeField(auto_now_addTrue, db_indexTrue) extra models.JSONField(defaultdict) # 优惠券、满减、库存状态 class Meta: indexes [models.Index(fields[pp, crawled_at])]PriceSnapshot 是一张只追加不更新的时间序列表每次抓到的价格都插入新行这样后续做价格趋势和预测才有原始序列。extra字段用 JSONField 把满减、优惠券、是否缺货这类非结构化信息存下来比把所有情况都设计成独立列省事得多。unique_together保证同一个平台不会重复插入同一个 SKU 的映射记录。SPU 归并是整个项目数据质量的根。常见做法是把「品牌 型号 关键规格」作为归并键型号文本需要先做小写化、全半角统一、去空格和括号内容再拼起来做 MD5 取前 16 位作为 spu_code。如果只拿商品标题做归并会出现「Apple iPhone 15 Pro 256GB」和「iPhone15Pro 256G」匹配不上的情况。这里值得单独维护一张别名表把「版」、「版本」、「容量」这类同义表达在入库前统一替换。2.2 requests BeautifulSoup 的多平台解析Parser 注册表与抓取参数多平台爬虫最常见的错误是每个平台写一套独立抓取逻辑复制粘贴几轮之后代码就散了。更可控的方式是定义一个 BaseParser 基类把请求逻辑统一放进去不同平台只重写 parse 方法再用一个字典注册表把平台代码映射到解析类。# crawlers/base.py import random import time import requests class BaseParser: platform unknown def __init__(self, timeout10, retries3, sleep(1, 3)): self.timeout timeout self.retries retries self.sleep sleep self.session requests.Session() self.session.headers.update({ User-Agent: self._random_ua(), Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) def _random_ua(self): ua_list [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/119.0 Safari/537.36, ] return random.choice(ua_list) def fetch(self, url): for attempt in range(self.retries): try: resp self.session.get(url, timeoutself.timeout) if resp.status_code 200: return resp.text except requests.RequestException: time.sleep(2 ** attempt) # 指数退避 time.sleep(random.uniform(*self.sleep)) return None def parse(self, html: str) - dict: raise NotImplementedError京东商品页的价格不在 HTML 源码里而是通过页面内嵌的 productInfo 拿到 sku_id 后再请求价格接口才能取到真实价格。这是一个「两段式」流程先解析基本信息再按价格接口返回 JSON。把 fetch 和 parse 拆开就是为了应对这种价格接口独立于商品页的情况。timeout 设 10 秒retries 设 3 次sleep 区间 1 到 3 秒这些参数按自己的网络状况调整调整的基准是「单台机器、单线程、每请求间隔不低于 1 秒」低于这个频率很容易被限流。平台数据入口解析要点京东商品页内嵌 JSON 异步价格接口商品信息与价格分离需要至少两次请求淘宝/天猫详情页初始化数据 JSON或 MTOP 接口请求头校验严格部分类目需要登录态苏宁商品页参数对象字段名在不同类目下不一致取值前要做类型判断国美HTML 静态结构页面结构相对稳定注意价格单位与促销价位置2.3 Django 管理命令与 Cron 串起定时采集采集任务用 Django management command 实现最直接不引入 Celery 也能把整个链路跑通。写成 Command 的好处是能直接复用项目的 ORM 配置和日志体系cron 调用的就是完整的 Django 环境。# crawlers/management/commands/crawl_products.py from django.core.management.base import BaseCommand from products.models import ProductPlatform from crawlers.registry import PARSER_REGISTRY class Command(BaseCommand): help 抓取指定商品链接的价格快照 def add_arguments(self, parser): parser.add_argument(--platform, typestr, defaultNone) parser.add_argument(--limit, typeint, default200) def handle(self, *args, **options): targets ProductPlatform.objects.filter(is_activeTrue) if options[platform]: targets targets.filter(platform__codeoptions[platform]) targets targets[: options[limit]] for pp in targets: parser_cls PARSER_REGISTRY.get(pp.platform.code) if not parser_cls: continue data parser_cls().run(pp.url) # 内部完成 fetch parse # 入库逻辑略注意用 update_or_create 保证价格字段追加写入 PriceSnapshot--platform参数用于单独跑某个平台方便排查京东解析挂了但其他平台正常的情况--limit用于小批量试跑。cron 里每 20 分钟执行一次日志追加到固定文件方便排查*/20 * * * * cd /opt/price_compare /usr/bin/python3 manage.py crawl_products --limit 200 /tmp/crawl.log 21采集入库时价格不能 update_or_create 直接覆盖那样会把历史序列丢掉。正确做法是查一下该 ProductPlatform 最近一条 PriceSnapshot 的价格和本次抓到的价格不一致再插入新记录一致则跳过。这样既保留了价格变化的完整轨迹也减少了很多重复行。3. 价格对比分析与机器学习/深度学习的商品推荐实现3.1 基于同一个 SPU 的多平台价格对比指标价格对比不是把当前价格列出来就结束至少要算出「当前最低价、平台均价、近 7 天最低价、历史最低价」这四档用户才看得出哪个平台真的便宜。用 Django ORM 做一次分组聚合就能拿到from django.db.models import Avg, Min, Max stats ( PriceSnapshot.objects .filter(pp__product_idspu_id, crawled_at__gteseven_days_ago, price__gt0) .values(pp__platform__code) .annotate( avg_priceAvg(price), min_priceMin(price), last_crawledMax(crawled_at), ) .order_by(min_price) )price__gt0是因为有些平台页面上促销价可能被抓成 0.01 甚至 0 元这类脏数据必须过滤。last_crawled用来判断某个平台的数据是不是已经过期比如三天没抓到新价格前端就应该标注「价格可能不是最新」而不是让用户误认为它当前就是这个价。比价页里常见的「降价幅度」可以这么算(promotion_price - avg_price) / avg_price负值表示低于平台平均价。这里要注意 promotion_price 可能比 price 高因为有些商品的活动价要求领券后才生效两个字段含义不同前端要标注清楚是「到手价」还是「券后价」。所有展示层接口都建议返回价格流水而不是只返回最新值让前端自己画趋势线。3.2 内容召回中文标题的 TF-IDF 相似度计算推荐系统的第一步是做候选集召回。没有用户行为数据的冷启动阶段最稳定的召回通道就是基于商品标题的内容相似度。中文标题分词错误对后续相似度计算影响很大直接整段分词效果不稳定改用字符级别的 TF-IDF 更可靠。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity titles list(Product.objects.values_list(title, flatTrue)) vec TfidfVectorizer( analyzerchar_wb, ngram_range(2, 3), max_features50000, ) matrix vec.fit_transform(titles) # 取第 idx 个商品的相似商品 TopN score cosine_similarity(matrix[idx:idx 1], matrix).flatten() top_n score.argsort()[-10:][::-1]analyzerchar_wb按字符窗口切分而不是按词切分中文里品牌型号的片段会被保留为二元组和三元组特征ngram_range(2, 3)覆盖了「iPh」「Pho」「one」这类能让相似商品对齐的片段也比 unigram 更有区分度。max_features50000限制特征维度避免把低频噪声字纳入计算。内容召回的用途不是替代行为推荐而是作为「用户刚搜完一个商品侧边栏展示同类商品」的主要数据源。相似度计算可以离线跑好结果存一张 ProductSimilarity 表线上查询直接按商品 ID 取 TopN不需要在请求里实时算矩阵。3.3 行为召回implicit ALS 协同过滤的用户向量有用户行为数据之后协同过滤比内容召回的个性化效果好得多。行为数据来自用户对商品的操作包括浏览、收藏、加入购物车三类分别按 1:3:5 的权重构造用户-商品矩阵。implicit 库的 ALS 实现适合处理这种隐式反馈矩阵。import implicit from scipy.sparse import coo_matrix row user_ids # 用户ID列表 col item_ids # 商品ID列表 val weights # 浏览1、收藏3、加购5 data coo_matrix((val, (row, col)), shape(n_users, n_items)).tocsr() model implicit.als.AlternatingLeastSquares( factors64, iterations15, regularization0.1, ) model.fit(data) # 对编号为 100 的用户做候选召回 recommended model.recommend(100, data[100], N50, filter_already_liked_itemsTrue)factors 设 64 是隐式反馈场景下的常用取值低于 32 时召回精度下降明显高于 128 对 50 万级商品矩阵的训练耗时增长明显iterations 15 轮已经能收敛再多对精度帮助有限regularization 0.1 控制向量范数调太高会把向量压得太小。filter_already_liked_itemsTrue保证推荐结果里不出现用户已经加购或收藏的商品。行为数据表建议独立记录不要在爬虫日志里临时拼。最早期的方案可以在 Django admin 里手动给测试用户打几个收藏让 ALS 能跑起来之后再接入真实埋点。隐式反馈矩阵的「负样本」天然缺失这一点 ALS 通过 confidence 权重机制处理所以不要让用户行为表里出现显式评分 0 的记录直接不写入矩阵即可。3.4 排序层双塔模型的简化实现与负样本构造召回阶段拿到 50 到 100 个候选商品后要用排序模型对候选集打分。双塔模型是深度学习排序里最容易落地的一种用户塔和商品塔分别输出向量点积就是排序分数。import torch import torch.nn as nn class TwoTower(nn.Module): def __init__(self, n_users, n_items, dim64): super().__init__() self.user_emb nn.Embedding(n_users, dim) self.item_emb nn.Embedding(n_items, dim) def forward(self, user_ids, item_ids, labelNone): u self.user_emb(user_ids) i self.item_emb(item_ids) score (u * i).sum(dim1) if label is None: return torch.sigmoid(score) loss nn.BCEWithLogitsLoss()(score, label.float()) return loss训练时正样本是用户点击、收藏、加购过的商品负样本从「召回出来但用户没有交互」的候选商品里采样。随机负样本虽然训练起来容易但会让模型学到「和用户向量完全不像的商品」就是负例和真实场景不一致从召回候选里采负样本能让模型学到的是「同样相似但用户没选」的差异排序效果明显更好。商品塔的 Embedding 可以用 ALS 训练出来的 item factors 做初始化这是离线协同过滤和深度学习排序衔接最快的方式。训练收敛速度快不少线上效果也比纯随机初始化稳定。两路向量的维度保持一致推荐里用到用户向量时可以直接复用双塔的 user embedding不需要再维护另一套维度体系。3.5 多路召回与排序特征表召回和排序的分工可以简单总结为召回要「全」排序要「准」。每个通道独立取 TopN融合后去重最后进排序模型的是这些候选的商品特征。召回通道输入数据TopN适用对象ALS 协同过滤用户行为矩阵50有历史行为的用户内容相似商品标题 TF-IDF50新用户、冷启动同价格带热销类目 价格区间30任何用户排序特征可以从三个角度凑商品侧与用户最近浏览商品的价格差、同品牌标记、平台均价比率、用户侧用户历史点击率最高的类目、价格带偏好、交互侧两个商品是否同一价格带、促销价折扣率。价格差特征在比价场景里尤其重要因为用户来比价的根本动机就是「同样东西别买贵」排序时把价格更低的商品往前推比单纯算相似度更贴合业务。4. 价格预测时序特征、Prophet 与促销场景的参数调整4.1 从价格快照构造日粒度训练集价格预测的训练数据不需要把每一条 PriceSnapshot 都喂给模型。价格在一天内往往只变几次直接按日粒度取当天最低价或者最后一次有效价格即可既减少了噪声也符合「预测明天什么价」的业务口径。from django.db.models import Min daily ( PriceSnapshot.objects .filter(pp_idpp_id, price__gt0) .values(crawled_at__date) .annotate(lowMin(price)) .order_by(crawled_at__date) )Min(price)取当日最低价作为训练目标比取收盘价稳定因为用户比价时关注的就是「这个平台今天最低能到多少」。如果某些天没有抓到数据会导致时间序列出现空洞要用 pandas 的 resample 填充缺失日期策略用 ffill 或者直接删除连续缺失超过 3 天的商品。训练数据不少于 60 天才有预测意义数据量少于这个规模时预测结果只能作为区间参考不要直接展示成精确价格。4.2 Prophet 预测未来 14 天价格区间价格序列通常同时包含长期趋势、促销周期和节假日效应。Prophet 对这种「趋势 季节 节假日」结构的序列处理得最省心不需要预先做差分或平稳性检验。价格预测的核心不是给出一个单一值而是给出 yhat_lower 和 yhat_upper 区间。import pandas as pd from prophet import Prophet df pd.DataFrame({ds: daily[date], y: daily[low]}) model Prophet( changepoint_prior_scale0.05, seasonality_prior_scale10, weekly_seasonalityTrue, daily_seasonalityFalse, ) model.add_country_holidays(country_nameCN) model.fit(df) future model.make_future_dataframe(periods14, freqD) forecast model.predict(future)changepoint_prior_scale 控制趋势对价格突变的敏感度0.05 是默认值促销频繁的商品建议调到 0.08 到 0.1但调太高会把随机波动当成趋势拐点预测结果抖动明显。seasonality_prior_scale 控制季节项强度设为 10 是为了让模型能学到 618、双十一这种明显的周期性波动。add_country_holidays会把法定节假日作为额外回归项加进去促销节点前后价格波动在不同年份并不严格对齐节假日项能帮模型把「放假前后价格往往异动」这一点学出来。预测结果的一个实用场景是「降价提醒」如果当前价格低于 forecast 的下沿 yhat_lower说明价格已经进入近 14 天大概率区间之外的低位此时向收藏用户推送「价格达近两周低位」的通知用户接受度比随意降价提醒高得多。4.3 深度学习做价格预测什么情况下才值得换 LSTM标题里写了深度学习但价格预测不一定要全用深度学习。深度学习的优势是能从更长的序列里学到复杂非线性模式劣势是对数据量和调参要求高。只有当单商品的历史价格超过 180 天、并且希望把平台差异、促销日距离、价格带等外部特征一并建模时LSTM 才有性价比。import numpy as np def make_windows(series, window14): X, y [], [] for i in range(len(series) - window): X.append(series[i:i window]) y.append(series[i window]) return np.array(X), np.array(y)这个窗口函数把连续 14 天的价格序列映射为第 15 天的价格LSTM 的输入就是这种形状的窗口数据。实际做下来会发现影响价格预测误差最大的因素不是选 Prophet 还是 LSTM而是有没有把「距 618 天数」「距双十一天数」这类促销特征放进去。促销事件导致的突变仅靠价格序列本身是学不出来的因为这些事件的日期不在价格数据里。所以哪怕用 LSTM也建议在输入里拼接事件距离特征而不是只喂价格窗口。指标定义参考范围MAE预测值与实际值的平均绝对差低于商品均价 5%MAPE平均绝对百分比误差3% 到 8%方向准确率涨跌方向预测正确的比例60% 以上评估时用 MAE 不够价格高和价格低的商品同样差 50 元相对意义完全不同。MAPE 更适合评估不同价格带的商品方向准确率则是给「明天是不是该买」这个决策用的。三分之二以上的商品的 MAPE 超过 10% 时不要急着调模型先检查价格快照里是不是混入了促销前改成高价、促销时再降价的「先涨后降」数据这类数据对预测干扰极大过滤掉虚假原价会让整体指标立刻改善。5. 验证接口与工程细节管理命令、查询优化与国产化环境部署5.1 用 DRF 暴露价格对比、比价详情与推荐结果接口价格对比和推荐最终都要通过接口给前端调用。用 Django REST Framework 的 ReadOnlyModelViewSet 就能覆盖查询场景不需要自己手写各种 GET 视图。# price_api/views.py from rest_framework.viewsets import ReadOnlyModelViewSet class ProductPriceViewSet(ReadOnlyModelViewSet): queryset ( ProductPlatform.objects .select_related(product, platform) .filter(is_activeTrue) ) serializer_class ProductPriceSerializer filterset_fields [product__spu_code, platform__code]注意这里用了select_related否则序列化输出平台名称时会对每个商品发一次额外查询一个商品四个平台就要多查四次数据库。接口设计上价格对比按 spu_code 返回全平台最新价、7 日均价和价格趋势数组推荐接口则接收 user_id读取多路召回融合结果再返回商品列表和推荐理由标签。验证系统是否跑通可以按这个顺序检查先确认 admin 后台能看到至少两个平台对同一个 SPU 的商品记录再通过接口参数?product__spu_codexxx返回 4 个商品的对比结果最后用管理命令predict_prices对其中一款商品生成未来 14 天的价格曲线。5.2 部署和验证这套系统时的五个关键工程细节数据量上来之后查询瓶颈一般不在 Django 而在数据库。PriceSnapshot 表会膨胀得最快务必保证(pp_id, crawled_at)上有复合索引否则按商品查历史价格会变成全表扫描。写入侧用 bulk_create 批量插入价格快照避免逐条 save管理命令里已经按平台过滤单平台批量插入对数据库压力小很多。部署在国产化环境时比如麒麟这类默认不带完整编译链的服务器pip install -r requirements.txt之前先确认 gcc、python3-dev 已安装否则 numpy、lxml 会在现场编译失败。建议在项目里固定 Python 3.10 以上版本和 Django 的 LTS 版本避免跨版本 API 差异。启动前用python manage.py check --deploy检查安全配置把 DEBUG 关掉ALLOWED_HOSTS 配成具体域名。线上跑一段时间后抓取稳定性是这套系统最需要盯的。每个平台写一个状态统计命令输出最近 24 小时成功率、平均响应耗时、最近一次成功抓取时间低于阈值时报警。不要追求所有平台的抓取频率一致京东价格变化频繁可以 10 分钟一次苏宁国美这类流量低一些的平台 30 分钟一次也够把请求频率先定在 5 秒以上等数据量和接口都稳定了再做并发调优。本文还有配套的精品资源点击获取