ARTICLE DETAIL

资讯详情

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

基于Python的电商比价系统实战:爬虫、Django与协同过滤推荐

基于Python的电商比价系统实战:爬虫、Django与协同过滤推荐 1. 一个比价系统到底要解决什么问题先说结论这个项目表面上是“比价”本质上是把一个电商领域最常见的业务场景——用户要买一件商品却不知道哪个平台便宜、什么时候买便宜——用技术手段自动化、数据化。我做过好几个类似的信息采集类系统踩了不少坑。这次基于Python实现商品比价系统核心就干三件事第一把多平台的商品信息和价格抓下来第二把同一件商品在不同平台的价格对齐做一个有说服力的对比第三把比价数据变成能看、能用的东西包括价格走势、降幅监控、用户偏好推荐。技术栈选了Django框架、爬虫、协同过滤推荐算法、数据分析可视化这套组合不是随手拍的而是从实际需求倒推出来的。适合谁参考想系统学Python爬虫加Web开发的人准备做毕业设计或课程设计的同学以及想在电商选品、比价、价格监控方向做点小产品的开发者。我尽量少讲教科书上的空话多讲实际怎么落地哪些地方容易翻车。这个项目最核心的价值不在“比价”这个动作本身而在“数据”。价格历史、商品属性的变化、用户点击和收藏的偏好这些数据堆起来之后可以做价格预测、可以做推荐、可以做消费洞察。所以整体设计上从爬虫到存储到算法到可视化每一层都在为后续的数据应用铺路。1.1 为什么选择爬虫Django协同过滤这套组合先聊聊技术选型的逻辑这部分我在动手之前反复权衡过。爬虫负责数据来源这是整个系统活下去的前提。电商平台的商品页、搜索页、详情页结构各不相同有的接口返回JSON有的直接渲染在HTML里需要针对不同站点写不同的解析器。Python在爬虫领域生态最成熟requests、Scrapy、BeautifulSoup、lxml这些库随手就能用遇到登录态、验证码也有人分享过大量解法。只要不碰违规手段只是一般性浏览公开页面和个人学习用途Python爬虫是最省力的选择。Django负责业务层。比价系统需要有商品列表、商品详情、价格历史、用户注册登录、收藏、评论这些常规功能还要有数据分析和推荐相关的后台接口。Django自带ORM、Admin后台、模板引擎和中间件机制一套下来比Flask加各种插件的组合更省心尤其适合快速把原型做成可用系统。项目里用Django的rest_framework提供API接口前端不管是网页还是小程序都能对接。协同过滤负责“千人千面”的推荐。用户访问比价系统时最理想的体验是“不用搜首页就推荐我刚好想买的东西”。协同过滤不依赖商品本身的文本属性只需要用户的行为数据——点击、收藏、比价、购买——就能算出相似用户或相似商品这在电商场景里非常成熟。大数据在这里不是指Hadoop那套而是指数据量级到了单机数据库撑不住、需要有策略地存储和查询的程度。比如每天爬取几万条价格数据累计几个月就是几百万条价格历史表要按用户查询频率做聚合、要清理过期无效数据。这部分我用MySQL存储结构化数据配合Redis做缓存和热门商品排行实测几百万条数据依然能秒级响应。1.2 系统功能模块划分解读把整个系统切成几个功能模块每个模块都有明确的输入输出数据采集模块定时抓取指定商品的标题、价格、店铺、销量、评价数、详情链接存到原始数据表。数据清洗与匹配模块把同一商品在不同平台的不同标题、不同规格对齐形成统一商品ID。商品查询与比价模块用户输入商品关键词展示各平台的在售价格、当前最低价、历史最低价按价格或综合评分排序。价格趋势分析模块展示商品30天、90天价格曲线标记当前价格所处位置。用户行为采集模块记录用户点击、收藏、比价行为供推荐算法使用。协同过滤推荐模块基于用户行为计算相似度生成个性化商品推荐。可视化看板模块展示爬虫数据量、商品价格分布、平台占比、热门商品榜等统计图表。每个模块之间通过数据库耦合模块内部独立开发这个架构对单人开发很友好——先跑通爬虫再做匹配再做展示最后上推荐和可视化。下面我按这个顺序把每个模块的关键细节展开讲。2. 爬虫采集层数据从哪来怎么拿得稳爬虫是比价系统的“上游水渠”水渠不稳整个系统就都没水用。写爬虫容易写一个能长期稳定运行的爬虫很难。这里分享我在商品数据采集上的策略和踩坑过程。先定采集目标。比价系统的数据不需要覆盖全网所有商品那不现实也没必要。我选择垂直品类切入比如数码产品、家电、图书这些品类标准化程度高同一件商品在不同平台有明确的型号规格方便做匹配。选好品类后每个平台采集这几个关键字段商品标题、商品ID、当前价格、原价、店铺名称、销量、评价数、规格参数如颜色、版本、商品详情URL、采集时间。2.1 采集策略与频率规划采集策略分三层搜索页采集、详情页补全、定时任务增量更新。搜索页采集解决“从无到有”。用户搜索某个关键词爬虫按关键词去各平台搜索页抓取商品列表拿到第一批商品数据。搜索词怎么来手动维护一批种子关键词再从用户搜索记录里持续补充。详情页补全解决“从粗到细”。搜索页拿到的价格有时候是促销占位价详情页才是真实价格。所以对每个商品还需要进入详情页抓取最终价格、优惠信息、库存状态。这一步可以用协程并发或线程池10个并发请求批量处理效率提升非常明显。定时任务解决“从旧到新”。价格是会变的今天1299明天1399很常见。我用了APScheduler在Django里跑定时任务每个商品每小时抓一次最新价格写入价格历史表。这个频率要控制好抓得太密会大幅增加反爬风险和数据量抓得太疏又看不出价格拐点。数码家电类每小时一次足够。2.2 反爬应对与数据清洗细节反爬是绕不开的话题。平台限制请求频率的主要手段是IP限制、Header校验、行为检测。常规做法是控制请求频率、设置合理的User-Agent和Referer、使用代理IP池轮换、模拟人的操作间隙。这些是爬虫的基本功但一定不要碰加密解析、绕过登录验证这类灰产手段做个人学习项目守住合法合规的底线就行。我在采集时还做了一个“君子协定”单个平台并发数不超过5两次请求间隔随机化在1到3秒之间高峰期主动降频。这样采集效率虽然下降一点但账户和IP的安全系数高很多实测连续跑了两周没出问题。数据清洗这一步很多人偷懒结果后面匹配和展示全乱套。清洗要干的事包括去掉HTML标签和无意义的换行空格统一价格的精度价格转成浮点数原价和现价分开存销量和评价数统一成整数去掉“万”这类后缀换算规格参数拆成JSON字段比如版本、颜色、内存大小把平台自身的商品ID和系统生成的内部商品ID分开存。# 爬虫数据清洗的部分代码核心是字段统一 import re import json def clean_price(raw_price: str) - float: # 处理 ¥1,299.00、1299元、1.5万 这类格式 if not raw_price: return 0.0 s raw_price.replace(¥, ).replace(元, ).replace(,, ).strip() if 万 in s: return float(s.replace(万, )) * 10000 return float(s) def clean_title(raw_title: str) - str: # 去除广告词和无关符号 ad_words [【超值特惠】, [新品上市], 官方旗舰店] for w in ad_words: raw_title raw_title.replace(w, ) return re.sub(r\s, , raw_title).strip()价格清洗我特别提醒一句不同平台对价格的展示格式差别很大有的隐藏原价、有的把优惠券算在价格里这些情况必须在清洗阶段就明确“我们存的是最终实付价还是划线价”然后整个系统统一口径。3. 数据存储与模型设计比价的核心是数据模型数据模型设计决定了比价系统能做多深。我见过有人把商品和价格放在一张表里价格一更新就覆盖旧数据结果价格历史曲线完全没法画。正确的做法是把“商品实体”和“价格观测”分开。3.1 三张核心表的设计思路第一张是商品表Product字段包括内部商品ID、各平台对应商品ID、商品标题、品牌、品类、规格参数JSON、主图URL、创建时间。这张表代表“一件商品的标准信息”。第二张是平台商品表PlatformProduct字段包括内部商品ID关联、平台名、平台商品ID、平台价格、店铺名、销量、评价数、商品链接。一个内部商品对应多个平台商品因为同一件商品在不同平台可能是不同店铺在卖。第三张是价格历史表PriceHistory字段包括平台商品ID关联、价格、原价、采集时间。这张表只负责记录“某时刻某商品的价格是多少”一张表吃下所有价格快照后面画趋势图、算最低价都从这张表查。# Django models 的核心设计 from django.db import models class Product(models.Model): title models.CharField(max_length255) brand models.CharField(max_length100, blankTrue) category models.CharField(max_length100, blankTrue) specs_json models.JSONField(defaultdict) created_at models.DateTimeField(auto_now_addTrue) class PlatformProduct(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameplatform_products) platform models.CharField(max_length50) # jd/tmall/suning... platform_product_id models.CharField(max_length100) price models.DecimalField(max_digits10, decimal_places2) origin_price models.DecimalField(max_digits10, decimal_places2, nullTrue, blankTrue) shop_name models.CharField(max_length150, blankTrue) sales models.IntegerField(default0) url models.URLField() class PriceHistory(models.Model): platform_product models.ForeignKey(PlatformProduct, on_deletemodels.CASCADE, related_nameprice_history) price models.DecimalField(max_digits10, decimal_places2) origin_price models.DecimalField(max_digits10, decimal_places2, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue)价格历史表要建created_at的索引因为查询趋势图时基本都按时间范围过滤没索引几百万条数据直接慢查询。商品表里留specs_json存JSON规格差异大的品类比如手机有版本颜色比单列字段灵活得多这是我在电商数据实践里验证过的做法。3.2 商品匹配比价系统最难的环节商品匹配是整个项目里最棘手的问题。同一个iPhone 15京东标题写“Apple iPhone 15 128GB 蓝色 全网通5G手机”天猫写“苹果15手机正品官方旗舰店新机iPhone15 Pro Max 128G 256G”苏宁可能又是另一种写法。要让系统知道这是“同一件商品”就得做归一化匹配。我的匹配方案分三步走第一步品牌和品类归一。维护一个品牌别名表比如“Apple”和“苹果”统一成“apple”品类根据标题关键词初步分类。第二步关键规格抽取。用正则把内存、颜色、尺寸这些关键规格提取出来。比如用正则抓“128G”或“128GB”把不同写法归一成“128”。第三步标题相似度和规格相似度结合打分。规格完全一致且品牌一致的商品直接判定为同一商品规格有差异但标题高度相似的标记为“疑似同款”人工审核后再决定是否合并。from difflib import SequenceMatcher def normalize_spec(raw_title: str): memory re.search(r(\d\s*[GM]B), raw_title, re.I) color None for c in [黑色, 白色, 蓝色, 金色, 紫色]: if c in raw_title: color c break return {memory: memory.group(1).replace( , ).upper() if memory else , color: color or } def match_score(t1: str, t2: str): s1, s2 normalize_spec(t1), normalize_spec(t2) if s1[memory] and s2[memory] and s1[memory] ! s2[memory]: return 0.0 # 内存都不一样绝对不是同一件 brand_ok any(b in t1 and b in t2 for b in [apple, 华为, 小米, 索尼]) sim SequenceMatcher(None, t1, t2).ratio() return sim * 0.7 (0.3 if brand_ok else 0.0)这一步不是百分百自动化也不需要百分百自动化。商品库控制在几千个核心商品一次匹配运营审核两小时能搞定比写死复杂的NLP模型划算得多。对于课程设计或中小型比价应用这套半自动方案已经够用了。4. Django后端业务逻辑从API到比价核心Django在这个系统里承担两层角色一是给前端和移动端提供可用的API二是承载核心业务逻辑。下面讲讲项目布局和比价逻辑的实现。4.1 项目布局与API设计Django项目我按功能拆成多个app每个app只负责一块业务product_compare/ ├── manage.py ├── config/ # 项目配置settings/urls/wsgi ├── apps/ │ ├── collector/ # 爬虫管理采集任务和调度 │ ├── products/ # 商品、平台商品、价格历史的模型和查询逻辑 │ ├── users/ # 用户认证、收藏、行为记录 │ ├── recommend/ # 协同过滤推荐算法 │ └── dashboard/ # 数据统计和分析可视化接口API用Django REST Framework写接口按资源划分GET /api/products/ 搜索商品列表GET /api/products/1/ 获取商品详情和比价信息POST /api/users/1/behaviors/ 上报用户行为GET /api/recommend/ 获取个性化推荐GET /api/dashboard/summary/ 获取统计看板数据。用户认证我用JWTsimplejwt库直接集成前端拿到token后每次请求带上比session更适合前后端分离的场景。4.2 比价核心逻辑实现比价的核心逻辑不复杂关键在于查得准、查得快。实现思路是用户搜索关键词系统先查本地商品库本地命中就直接返回本地没命中就触发线爬虫去抓取新商品。查比价信息时系统根据商品ID关联所有平台商品按当前价格排序同时拉取最近30天的价格历史计算最低价和当前价差距。from django.db.models import Min from .models import Product, PlatformProduct, PriceHistory def get_compare_data(product_id: int): product Product.objects.prefetch_related(platform_products).get(idproduct_id) result [] for pp in product.platform_products.all(): history PriceHistory.objects.filter(platform_productpp) lowest_price history.aggregate(min_priceMin(price))[min_price] or pp.price current_price float(pp.price) result.append({ platform: pp.platform, platform_product_id: pp.platform_product_id, current_price: current_price, lowest_price: lowest_price, drop_percent: round((1 - current_price / lowest_price) * 100, 2) if lowest_price else 0.0, shop_name: pp.shop_name, sales: pp.sales, url: pp.url, trend_url: f/api/trend/{pp.id}/, }) return sorted(result, keylambda x: x[current_price])这段代码里最值得说的两个细节一是SQL查询优化。比价页要展示当前价格和历史最低价如果每个平台商品都单独做一次最低价聚合查询一个商品有5个平台就是5条SQL10个并发用户就直接把数据库打爆。我后来改成先用商品ID一次性查出所有平台商品再按platform_product_id做一次分组聚合查出最低价把查询次数从N1降到2性能翻了好几倍。二是缓存策略。热门商品的比价结果用Redis缓存10分钟key设计成product_id_norm:{id}。价格波动不大的时段完全没必要每秒钟都重新算最低价。5. 协同过滤推荐算法让用户看到自己想要的比价系统有了用户行为数据之后推荐算法就成了提升体验的点睛之笔。用户来到这个平台收藏了几个商品、点开了几个详情、比了几次价这些都是非常有价值的信号。协同过滤的核心假设就一句话跟你行为相似的人喜欢的东西你大概率也会喜欢。5.1 选UserCF还是ItemCF协同过滤有两种主流思路基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。UserCF是“人以群分”找到与你行为最相似的一批用户把他们喜欢而你没碰过的商品推荐给你。适合用户数量远小于商品数量、用户兴趣相对分散的场景。我的比价系统用户量初期不大商品数量几千个用户和商品的交互矩阵比较稀疏UserCF能算出更有惊喜感的推荐。ItemCF是“物以类聚”找到与你历史喜欢的商品最相似的一批商品推荐给你。适合用户量巨大、商品数量相对少、用户兴趣稳定的场景电商大厂普遍用这个。项目里两个都实现了默认跑ItemCF因为比价系统里同类商品很多同一部手机的多个店铺、多个版本基于商品的相似度更容易引起用户的“要比价”冲动。5.2 推荐流程的代码实现与冷启动处理推荐流程分离线计算和在线推荐两步。离线阶段每天凌晨跑一次相似度矩阵计算把结果存Redis在线阶段直接从Redis取TopN推荐延迟控制在几十毫秒。from math import sqrt from collections import defaultdict def build_item_similarity(user_item_matrix): # user_item_matrix: {user_id: {item_id: score}} item_user_map defaultdict(dict) for user_id, items in user_item_matrix.items(): for item_id, score in items.items(): item_user_map[item_id][user_id] score item_sim defaultdict(dict) for item_i, users_i in item_user_map.items(): for item_j, users_j in item_user_map.items(): if item_i item_j: continue common_users set(users_i.keys()) set(users_j.keys()) if len(common_users) 5: # 共同用户数太少的相似度无意义 item_sim[item_i][item_j] 0.0 continue dot sum(users_i[u] * users_j[u] for u in common_users) norm_i sqrt(sum(s * s for s in users_i.values())) norm_j sqrt(sum(s * s for s in users_j.values())) item_sim[item_i][item_j] dot / (norm_i * norm_j) if norm_i and norm_j else 0.0 return item_sim评分矩阵的数据来源我设计了三种行为加权点击算1分收藏算3分比价点击“对比”算5分。这样低门槛行为不会刷屏高价值行为能真正影响推荐结果。行为发生在最近7天的再乘一个时间衰减系数让推荐更敏感地反映当前兴趣。冷启动问题在比价系统里分两类新用户没有行为数据新商品没有交互记录。新用户我直接返回热门商品榜兜底就是过去7天收藏次数最多的商品Top10新商品则在ItemCF矩阵里没有位置我把它挂到同类目热门推荐后面做补充位。这个方案简单有效比硬套算法强很多。6. 数据分析可视化从价格曲线到消费洞察比价系统的数据价值一半在比价一半在分析。爬虫每天抓回大量价格数据不分析和不挖掘就只是躺在数据库里的数字。可视化这一步负责把数据转化成普通人能一眼看懂的信息。6.1 可视化维度的选择我从实际使用场景出发挑了这几类必要的可视化内容第一类是价格趋势图。单个商品的价格历史曲线这是用户最需要的。选择30天、90天、180天三个时间粒度X轴是日期Y轴是价格有平台对比的折线。同时标出历史最低点提示用户“当前价格比历史最低高5%可以再等等”。第二类是平台价格对比图。把同一商品在各平台的价格做成柱状图最低价的平台高亮。这个图直接对应比价的核心诉求放商品详情页顶部。第三类是价格区间分布图。针对某个品类比如“500元以下的手机有几款500到1000元有几款”用直方图展示。用户在筛选商品的时候能直观看到价格带分布。第四类是大盘统计图。平台商品量占比饼图、每天新增商品的折线图、近30天全网降幅最大的商品排行、历史价格最低点出现频率最高的品类。这些数据既能做运营报表也能反向指导爬虫扩充商品库。6.2 图表类型与前端选型图表方案我选的是ECharts。ECharts处理几万条数据的交互没压力折线图、柱状图、饼图、热力图都支持社区案例多遇到问题基本都能搜到答案。前端定时轮询后端接口拿到JSON直接渲染不用专门搭数据管道。后端接口用Django ORM做聚合统计比如降幅TOP10商品from django.db.models import F, Max, Min from .models import PlatformProduct, PriceHistory def top_drops(platform: str, days: int 30): # 找出30天内价格跌幅最大的商品 products PlatformProduct.objects.filter(platformplatform) result [] for pp in products[:200]: history PriceHistory.objects.filter( platform_productpp, created_at__gtenow() - timedelta(daysdays) ) agg history.aggregate(max_priceMax(price), min_priceMin(price)) if agg[max_price] and agg[min_price] and agg[max_price] agg[min_price]: drop (agg[max_price] - agg[min_price]) / agg[max_price] result.append({ title: pp.product.title, platform: pp.platform, max_price: agg[max_price], min_price: agg[min_price], drop_ratio: round(drop * 100, 2), }) return sorted(result, keylambda x: x[drop_ratio], reverseTrue)[:10]这里我用200个商品做循环聚合每个商品两次数据库存取共400次查询初始化大概2秒能接受。但如果商品总量继续涨就要改成分组聚合一次查出再交给Pandas处理。实际上超过1万商品后直接把这些原始id列表和价格历史放进Pandas DataFrame做groupby聚合速度快得多。7. 常见问题与排查技巧实录这个系统开发过程中我踩过的坑比预想的多。挑几个典型的列出来给后来人做个速查表。7.1 爬虫稳定性问题第一个大坑是IP被封。一开始写爬虫没有控制频率单线程跑没问题但开了并发之后半小时就被封。解决办法是双重限速全局加上延迟随机化单平台并发上限调低被封的时候立刻切换到备用代理。后来还加了自动熔断——如果某平台连续出现“访问异常”提示任务自动暂停15分钟而不是继续傻跑。第二个大坑是反爬验证码。遇到滑块或图形验证人工介入成本太高。我的解决思路是改变策略触发验证码说明请求频率或请求方式太像机器了先去降频、换User-Agent池、换请求头顺序大部分能规避。还有一部分是商品列表页有验证码但详情页没有那就在搜索阶段先拿商品ID详情页单独抓。7.2 数据质量与性能问题价格历史表膨胀很快。每天几万条记录三个月就几百万条。我开始没设索引画趋势图时查询要3秒加了created_at索引后变成0.2秒。再往后就要按月份做分区表或者定期把90天前的数据归档到冷表。商品匹配准确率上靠标题正则和规格归一化我发现两个容易出错的地方一是平台之间颜色命名不统一“深空灰”和“灰色”其实是同一个颜色需要维护颜色映射表二是促销活动导致同一平台同一商品出现多个价格这时要以“最终订单支付价”为准在采集时就要区分好。7.3 推荐效果不佳的调试思路协同过滤推荐出来一堆用户完全没兴趣的商品这是必经之路。我排查时先看行为数据量——用户平均只有3到5条行为记录矩阵稀疏度99%以上这种情况算出来的相似度置信度极低。我做的改进是把浏览时长超过30秒的页面也算高价值行为增加行为样本量在计算相似度时对共同用户数设置最低阈值避免两个只碰巧点了同一件商品的人被算成高相似度。另外一个实用技巧相似度矩阵计算完一定要做归一化和阈值过滤。把相似度低于0.2的边直接砍掉矩阵稀疏度大幅提升线上查询速度立竿见影。7.4 可视化看板数据对不上的排查有段时间看板显示的总商品数和数据库里对不上查了一圈发现是爬虫任务重复跑导致的商品重复录入。商品表里没有建唯一约束同一个平台商品ID被插了多条。解决办法是在PlatformProduct表上加UniqueConstraint字段是platform和platform_product_id联合唯一。数据清理用一条SQL按平台商品ID去重保留最新一条。# 模型层面加唯一约束 class PlatformProduct(models.Model): ... class Meta: constraints [ models.UniqueConstraint(fields[platform, platform_product_id], nameunique_platform_product) ]这个问题在很多爬虫类项目里很典型爬虫写得越勤快重复数据出现得越快数据库层面的唯一约束和Django的get_or_create一定要配合使用。8. 我个人在实际操作后的几点体会这个项目做完之后我最深的一个感触是比价系统真正难的不是算法层而是数据基础。协同过滤算法网上开源代码一堆爬虫框架文档也很全真正的功夫在于商品数据能不能保持完整、准确、及时。数据脏了再好的算法和可视化都是白搭。实际开发流程上我建议按“爬虫-清洗-模型-API-前端-推荐-可视化”这个顺序来做每一层验证通过再往下一层走。千万别一上来就把Django和推荐算法铺开否则大概率会陷入“网站搭好了却没数据可用”的尴尬。如果有朋友想在这个项目上继续扩展我推荐两个方向一是把比价数据接入邮件或微信通知降价自动提醒用户的“降价订阅”功能这是比价系统天然的增值点二是把价格历史和销量数据拿来做简单的销量预测或商品行情分析那就从“比价工具”升级成了“电商数据服务平台”。最后分享一个小技巧整个项目开发过程中一定要从第一天就开始给需要频繁访问的表加合适的索引、给重复插入的字段加唯一约束。等数据量上来了再回头改这些改表要锁表、要迁数据痛苦系数直接翻倍。数据是这套系统的命根子把数据的地基打好后面每一个功能都能建得踏实。
返回列表