尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python电商比价系统:爬虫、协同过滤与可视化实战

Python电商比价系统:爬虫、协同过滤与可视化实战 做电商场景下的比价系统最头疼的往往不是“比价”本身而是数据从哪来、数据准不准、推荐怎么跟上去。我最近完整跑通了一个基于Python的商品比价系统技术栈用了Django做Web框架爬虫负责采价协同过滤做个性化推荐最后用可视化把价格趋势和商品对比展示出来。这篇就把我的设计思路、关键代码思路、以及踩过的坑一次说清楚。这个项目适合谁参考如果你正在做电商数据分析、想自己搭一个商品监控工具或者准备写毕业设计/简历项目都可以直接对照落地。我尽量把每一步的“为什么”也讲明白而不是只丢一段能跑的代码。1. 项目整体设计与思路拆解1.1 核心需求拆解比价系统到底在比什么先把“比价”这件事拆开。用户打开一个商品详情页看到的不是一个孤零零的价格而是三样东西这个商品在不同平台的历史价格走势、同款或近似款在当前市场的价格区间、以及“和你偏好相似的人都在买什么”。所以比价系统的核心需求可以拆成四层数据采集层从多个电商平台抓取商品标题、价格、销量、店铺等信息数据治理层清洗脏数据统一价格格式对齐商品的规格和单位服务层提供价格查询、价格对比、走势分析、推荐接口展示层用图表直观呈现价格差异和趋势这个拆法决定了系统架构不会像一个普通CRUD项目那样简单堆功能而是每一层都有独立的难点。比如数据采集层要处理反爬数据治理层要处理“同一件商品在不同平台写法完全不同”的语义问题服务层的推荐算法又依赖用户行为数据的积累。1.2 技术选型背后为什么是Django Scrapy Pandas选Django做Web框架最直接的原因是它自带Admin后台和ORM数据模型改完不用写一堆SQL直接跑迁移就能同步数据库表。配合Django REST Framework前端或者小程序端要接接口也方便。爬虫这块我用的是Scrapy加requests混着来。Scrapy适合做大规模的站内爬取自带并发调度和去重但有些平台的页面是异步加载的Scrapy默认的DownloadHandler拿不到渲染后的数据这种场景我就用requests直接调它的内部接口配合Selenium处理极少数必须浏览器渲染的页面。技术栈具体如下模块技术选型选型理由Web框架Django 4.x自带ORM、Admin、迁移工具适合快速迭代API层Django REST Framework序列化、视图集、权限控制齐全爬虫Scrapy requests批量抓取用Scrapy单页直取用requests数据清洗Pandas处理DataFrame格式的价格、销量数据很高效推荐算法scikit-learn自实现UserCF灵活可控方便理解算法逻辑可视化pyecharts / ECharts生成交互式图表适合嵌入Web页面数据库MySQL RedisMySQL存业务数据Redis做缓存和推荐加速定时调度APScheduler轻量级配合Django生命周期启动这套组合避开了两个常见坑一是纯Scrapy没有Web服务能力二是纯Django做并发抓取太吃力。各取所长才能让系统既跑得动爬虫也扛得住用户查询。1.3 系统架构与数据流设计系统的数据流可以概括成一条流水线定时爬虫抓取平台数据 → 数据清洗与归一化 → 存入MySQL → 触发推荐算法增量计算 → 写入Redis缓存 → Django对外提供API → 前端可视化展示这里有一个容易被忽略的设计点爬虫和数据入库不能耦合得太死。我最初的版本是爬一个商品就写一次库结果平台页面偶尔晚加载导致字段缺失脏数据直接进了正式表后续清洗成本极高。后来改成爬虫只负责产JSON文件单独的数据清洗任务再去读这些文件用事务批量入库出问题也好回滚。推荐算法作为数据流里的一个旁路模块不参与实时请求链。用户点击商品、收藏商品、加购等行为记录会落地到一张行为日志表系统每天凌晨计算一次离线推荐结果写入Redis。这样用户在打开推荐接口时拿到的是已经准备好的结果响应速度很快。2. 核心细节解析与实操要点2.1 商品数据采集层的三要素做爬虫先想清楚的不是怎么写代码而是抓什么和怎么抓。第一个要素是入口怎么找。我的做法是维护一张种子商品表专门记录品类关键词和对应的商品ID。爬虫启动后先访问平台搜索页拿到商品列表再逐条进入详情页抓取。这样比直接写死商品链接要灵活换品类只需要改种子表。第二个要素是字段怎么对齐。不同平台对“价格”的表达差异很大有的展示“¥299.00”有的写着“269元起”还有的“到手价219”这些都必须先归一化再入库。我在清洗层单独写了一个parse_price函数专门处理这些格式后面会细讲。第三个要素是频率和数据量控制。我的项目是一个中等规模监控系统不需要实时的全网数据所以爬虫频率控制在每5-10分钟一轮每个品类并发数为2并用requests的Session复用连接减少握手开销。如果你一上来就开20个并发大概率半天就被封。数据表方面最关键的是这张商品历史价格表CREATE TABLE price_history ( id INT AUTO_INCREMENT PRIMARY KEY, product_id BIGINT NOT NULL, platform VARCHAR(50) NOT NULL, price DECIMAL(10, 2) NOT NULL, title VARCHAR(255) NOT NULL, shop_name VARCHAR(100), capture_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_product_time (product_id, capture_time) );这张表是价格走势分析的数据基础。我专门加了复合索引(product_id, capture_time)因为后续所有围绕价格趋势的查询都会用这两个字段做条件索引加与不加查询速度差好几倍。2.2 价格归一化与商品对齐比价数据的核心处理比价系统最怕的不是没数据而是数据对不上。一个典型案例某平台卖的是“iPhone 15 Pro 256G 原色钛金属”另一个平台写的是“Apple iPhone 15 ProA3104256GB 原色”第三家写的是“苹果15Pro 256G 手机”。这三者其实是同一个商品但靠字符串精确匹配根本对不上。我的清洗方案分三步第一步字段级归一化。价格统一转成Decimal类型单位统一转成基本单位。比如“斤”和“千克”在商品规格里会出现不统一的话比较结果就是错的。销量字段也是有的平台显示“已售10w”需要转成数字100000。第二步文本规范化。去掉所有空格和特殊符号把全角字符转半角品牌词统一映射。比如“Apple”和“苹果”都归一化成同一个品牌ID。第三步关键属性对齐。针对手机、白酒这类规格敏感的商品单独抽品牌、型号、容量、版本这几个核心属性做组合匹配。先按品牌型号分桶再在桶内用编辑距离做模糊匹配相似度大于0.86就判定为同一商品。这三步做完商品对比表才真正有业务含义。我在这部分花的时间和精力比写爬虫本身还要多但这是比价系统区别于普通爬虫项目的价值所在。2.3 Django数据模型与API接口设计要点Django的模型设计要面向业务场景而不是面向数据库。我设计了这几个核心模型Product商品基础信息、PriceHistory价格历史、UserBehavior用户行为、Recommendation推荐结果。其中UserBehavior是推荐算法的数据来源class UserBehavior(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) product models.ForeignKey(Product, on_deletemodels.CASCADE) behavior_type models.CharField(max_length20) # view / favorite / cart / purchase score models.FloatField(default1.0) # 不同行为权重不同 created_at models.DateTimeField(auto_now_addTrue)这里有个细节我给不同行为设了不同权重。购买给5分加购给3分收藏给2分浏览给1分。这样后面算用户的商品评分矩阵不需要再做一层映射。API接口我全部走Django REST Framework的视图集配好序列化器之后一个商品详情页的数据组装就简单了。class ProductViewSet(viewsets.ReadOnlyModelViewSet): queryset Product.objects.select_related(brand).prefetch_related(price_history) serializer_class ProductSerializer action(detailTrue, methods[get]) def price_trend(self, request, pkNone): product self.get_object() history PriceHistory.objects.filter(product_idproduct.id)\ .order_by(capture_time).values_list(price, flatTrue) # 返回给前端做折线图这里特意用了select_related和prefetch_related避免N1查询问题。商品详情页同时要展示基础信息、规格、历史价格如果不做关联优化一次页面请求可能打十几条SQL数据库压力很大。3. 实操过程与核心环节实现3.1 爬虫模块从Scrapy框架到异步接口的完整落地我实际跑通的爬虫分为三类任务搜索页抓取、详情页抓取、价格更新轮询。搜索页抓取的核心逻辑是这样class SearchSpider(scrapy.Spider): name search_spider def start_requests(self): seed_keywords ProductSeed.objects.values_list(keyword, flatTrue) for keyword in seed_keywords: yield scrapy.Request( urlfhttps://example.com/search?q{quote(keyword)}, callbackself.parse_search, meta{keyword: keyword} ) def parse_search(self, response): item_links response.css(a.item-link::attr(href)).extract() for link in item_links[:10]: # 每页只取前10个商品控制抓取量 yield scrapy.Request( urlresponse.urljoin(link), callbackself.parse_detail, meta{keyword: response.meta[keyword]} )这里的[:10]不是随便写的。我做抓取量控制时算过一笔账每个种子关键词产出10个商品链接50个关键词就是500个商品每个商品一天抓24次价格更新一天大约是12000条请求。这个量级对于一台普通云服务器完全能承受但又不会因为请求太多触发平台风控。对于异步加载的商品详情我会先用requests请求详情页的XHR接口直接拿到JSON数据。这种接口一般都在页面Network面板里能看到返回的数据结构通常比HTML更规整。价格更新轮询我用了APScheduler每隔10分钟扫描一次商品表里的活跃商品ID逐个请求详情页价格并写入PriceHistory表。这里要特别注意写入用update_or_create防止同一时间点产生重复记录。3.2 协同过滤推荐算法从评分矩阵到TopN推荐推荐模块是整个系统的亮点。我选的是基于用户的协同过滤UserCF它的核心思想很简单喜欢相似商品的人品味也相似。算法整体分四步第一步构建用户-商品评分矩阵。从UserBehavior表里读取用户行为按行为权重换算成评分。def build_user_item_matrix(behaviors): matrix {} for behavior in behaviors: user_id behavior[user_id] product_id behavior[product_id] score behavior[score] matrix.setdefault(user_id, {})[product_id] score return matrix第二步计算用户之间的相似度。我用的是余弦相似度公式是similarity A·B / (||A|| × ||B||)。之所以用余弦而不是皮尔逊相关系数是因为用户行为数据里0值非常多余弦相似度对“共同评分项的相对方向”更敏感也更容易解释。第三步找出和目标用户最相似的K个用户。K我设的是10。这个值不是拍脑袋定的我试过K5、10、20三组参数用离线评测数据集对比了一下推荐的精确率K10时效果最好。第四步生成推荐结果。把近邻用户有评分、但目标用户没有评过的商品按加权评分排序取TopN输出。def recommend_for_user(user_id, user_item_matrix, sim_matrix, k10, n5): scores {} for neighbor, similarity in sim_matrix[user_id][:k]: for product, rating in user_item_matrix[neighbor].items(): if product in user_item_matrix[user_id]: continue scores[product] scores.get(product, 0) similarity * rating return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:n]这段代码跑通之后我就把它做成每天凌晨的定时任务。全量计算一次用户相似度矩阵然后为每个活跃用户生成TopN推荐列表写入Redis。用户请求推荐接口时直接从Redis拿结果毫秒级返回。3.3 数据分析与可视化让价格对比一眼看懂可视化部分我用了pyecharts好处是生成的HTML可以直接嵌入Django模板而且图表自带交互。一个典型的比价展示页面包含三个图表第一张是价格走势折线图。同一个商品在不同平台的90天价格走势用不同颜色的线叠加在一起消费者一眼就能看出哪个平台近期价格更有优势。第二张是价格分布箱线图。输入一个商品品类系统把所有平台同品类商品的价格拉出来用箱线图展示中位数、四分位数和异常值。这张图对选品分析特别有用。第三张是推荐商品的对比条形图。用户在商品详情页看到推荐商品时条形图直接显示推荐商品的价格和当前商品的价差方便用户快速判断是否值得换一家买。我生成图表的方式是先写一个独立的chart_service.py专门负责数据获取和图表配置Django视图只负责把渲染后的HTML片段返回给前端def price_trend_chart(price_data): from pyecharts import options as opts from pyecharts.charts import Line line Line() line.add_xaxis([x[date] for x in price_data]) for platform in platforms: line.add_yaxis(platform, [x[price] for x in price_data if x[platform] platform], is_smoothTrue) line.set_global_opts(title_optsopts.TitleOpts(title近90天价格走势)) return line.render_embed()render_embed()返回的是带完整JS依赖的HTML片段直接丢进模板的{% autoescape off %}里就能显示不需要单独处理静态文件路径非常省事。4. 常见问题与排查技巧实录4.1 爬虫被反爬不是所有反爬都要正面硬刚做商品采集一定会遇到反爬。我的经验是先分清反爬的层级再决定对策不要一上来就上重型工具。最常见的反爬是User-Agent检测和请求频率检测。对付这个只要在Scrapy的Downloader Middleware里轮换UA加上随机延时就能解决大部分问题。class RandomUAMiddleware: def process_request(self, request, spider): ua random.choice(UA_LIST) request.headers[User-Agent] ua return None延时这块我在项目里用的是random.uniform(1.5, 3.5)不是固定延时。固定延时会被识别成机器行为随机延时配合人工浏览行为模拟反而更安全。遇到验证码我的选择是“绕”而不是“破”。京东、淘宝这类大平台的验证码体系非常成熟正面去破解成本极高。我现在的方案是只在低峰期比如凌晨2点到6点跑批量采集这个时段触发验证码的概率明显低。如果某些商品必须日间更新就单独走人工确认机制。还有一个容易被忽略的点请求头里的Accept-Language和Referer要保持一致性。有些平台会检查Referer是不是从站内跳转过来的直接请求详情页URL容易被标记为异常流量。4.2 数据一致性爬虫入库和人工修正的冲突这个坑我印象特别深爬虫跑久了数据库里会出现一批“死数据”和“脏数据”。“死数据”是指商品下架了但还在库里“脏数据”是指价格解析出错比如把“¥299.00起”解析成299但实际起步价是249。后来我加了两道保障第一道写入前校验。parse_price解析后的价格必须大于0且小于10万元超出这个范围直接丢弃并记录日志。这个阈值看着粗糙但能挡住绝大多数解析错误。第二道定期失效标记。每天晚上跑一个定时任务检查PriceHistory表中连续24小时没有更新记录的商品自动标记为“疑似下架”不再参与前端展示。人工修正方面我在Admin后台开放了价格修正入口。运营人员发现某条数据明显不对可以直接在后台改修改记录会写入一个price_correction_log表。这样既保证了前台展示的数据准确性又保留了对账链路。4.3 推荐冷启动新用户和新商品都没行为数据怎么办协同过滤最大的问题是冷启动。我在系统刚上线时没有用户行为数据推荐接口返回的是空列表页面很难看。我用的是策略叠加的笨办法对没有行为的新用户推荐当前平台综合热度最高的Top10商品也就是“热门推荐”。对行为数据少于5条的用户用基于商品内容的相似度推荐也就是根据商品类目和品牌属性做规则匹配。只有行为数据超过5条的用户才真正走UserCF。这个策略在系统上线早期很关键。等行为数据积累到一定量级我把它改成按用户行为量自动降级行为多走协同过滤行为少走热度没有行为走类目热门。整个逻辑用一个简单的判断函数就搞定def get_recommendation_strategy(user_id, behavior_count): if behavior_count 5: return user_cf elif behavior_count 0: return content_based else: return hot_rank4.4 查询性能优化从慢SQL到Redis缓存项目跑了两周后商品表数据量到了20万量级价格历史表到了100万量级。几个核心页面开始变慢最典型的是价格趋势接口一次请求要扫一大片历史数据。优化的第一步是看慢查询日志。我发现最大的问题不是单表查询慢而是关联查询时索引失效。Product表和PriceHistory表关联查询时因为PriceHistory的product_id字段没有单独建索引导致全表扫描。修复方案是在PriceHistory表上把(product_id, capture_time)联合索引加回去同时把Django查询里的.filter(product_id...).order_by(-capture_time)改成直接查主键。第二步是加缓存。价格趋势数据其实不需要实时生成我把每个商品的价格走势序列缓存到Redis缓存时间为30分钟。用户请求时先查缓存缓存没有再查MySQL。这个改动让价格趋势接口的平均响应时间从1.8秒降到了80毫秒。第三步是分页。商品搜索结果接口一次性返回上百条数据是没必要的我改成API里强制分页默认每页20条前端通过滚动加载拉下一页。数据量降到原来的五分之一页面初始化速度自然上来了。几个让我后来少走弯路的设计经验整个项目跑通之后我复盘了一下有几点体会想单独分享。第一别在一开始就追求“全网比价”。把两个平台、三个品类的数据做好做准比十个平台每样都半吊子强得多。平台越多字段对齐和数据清洗的复杂度是指数上升的。第二爬虫代码和业务代码一定要分开部署。我后来把爬虫单独拆成一个服务用命令行触发和定时任务触发和Django进程完全隔离。这样爬虫出了问题不会影响Web服务的稳定性。第三推荐算法不是越复杂越好。很多人一听协同过滤就觉得应该用矩阵分解或者深度学习模型但实际上在数据量只有几万条时UserCF配上合理的缓存效果已经足够好而且逻辑清晰、容易调试。先跑通简单的再根据数据量和业务反馈逐步升级这才是务实的做法。最后再分享一个小技巧如果你的项目也需要做价格监控不要直接把所有商品每10分钟抓一遍。把商品按热度分成AB两类A类热销商品高频监控B类长尾商品一天抓两次。这样数据量直接减半服务器压力小很多而且用户关心的热销品价格反而是最及时的。
返回列表