化妆品商城推荐系统实战:从爬虫采集到可视化大屏的完整数据链路
很多人做商城类系统习惯一上来就写 Spring Boot CRUD把商品表、用户表、订单表建好再套一个协同过滤算法最后发现推荐接口返回的结果全是“猜你喜欢同款”因为根本没什么候选商品。我做这套化妆品推荐商城时把顺序倒过来了先用爬虫把化妆品数据灌进库再围绕商家后台设计运营规则最后用可视化看板验证推荐效果。整套系统跑下来最大的感受是——真正花时间的不是 UserController而是数据链路的脏数据治理。这篇内容会完整拆解这个项目的四条链路爬虫并发采集、推荐系统落地、商家后台权限与闭环、可视化大屏的实现。每个部分都带实际踩坑记录和最终方案。如果你正打算做一个能演示、能讲清楚、能不断迭代的电商或推荐类项目或者你正在写毕业设计、准备整理项目经验这篇应该能给你省下不少弯路。1. 业务拆解与整体架构爬虫、商城、推荐、可视化四条链路怎么各司其职1.1 业务闭环的起点不是商城而是数据通常商城系统的起点是商品录入。化妆品品类有个特点SKU 极多一个“精华液”可以按品牌、系列、规格、肤质分成几十个变体。如果纯靠后台人工录入数据质量和运营效率都跟不上。所以我把爬虫当成“数据供给系统”设计爬虫模块从公开数据源采集品牌、类目、成分、价格、销量、用户评价等原始信息写入 raw_data 表经过清洗和标准化之后再同步到商品主数据表。推荐系统、商城商品列表、可视化统计全部读主数据表不直接碰爬虫层的原始数据。这个设计解决了一个很经典的问题爬虫是易变的业务系统是稳定的。目标数据源的结构变了、接口加了验证、某个反爬策略升级最多只会影响 raw_data 层不会导致商城下单报错。这是我把爬虫独立成一个采集进程而不是写成 Spring Boot 里的一个 Service 的直接原因。如果你把爬虫代码和业务代码写在一起将来任何一个站点规则变化你都得重新打包部署整个 Spring Boot 应用这个成本在项目初期不明显一旦数据源多了之后会非常痛苦。1.2 技术选型为什么爬虫用 Python业务用 Spring Boot与其纠结“Spring Boot 能不能写爬虫”不如想清楚每个任务的生态。Spring Boot 负责商品管理、订单、权限、推荐算法调度、可视化数据接口优势是稳定、社区资料多、和 MySQL/Redis 的生态配合成熟。Python requests BeautifulSoup 负责化妆品数据采集原因很实际处理 HTML/JSON 的时候 Python 的库更顺手requests 的会话管理和重试机制也简单写一个小型任务脚本比用 Java HttpClient 快很多。有同学问为什么不直接上 Scrapy 或分布式爬虫。我的判断是当前这个项目的数据量级在万级 SKU单机并发 8~16 个线程就已经能跑完Scrapy 的中间件体系对我不产生额外收益分布式爬虫更是性能过剩还要引入消息队列、任务调度器复杂度明显增加。但我在采集脚本里预留了良好的接口抽象采集器只负责产出“已解析的字典对象”下一步如果真的要上分布式可以直接把同一个 parse() 逻辑丢到 Scrapy 的 item pipeline 或者 Kafka 里不需要改任何业务代码。选型不是越重越好而是够用和可演进之间的平衡。1.3 四层架构与数据库建模的核心表Spring Boot 部分我按经典的四层架构拆controller - service - mapper - model每个业务域内部再独立分包。这个分层看似老生常谈但在这个项目里有实际意义爬虫采集、推荐计算、商城订单、商家权限分散在不同 domain 包下改动某个模块时不会牵连其他模块的 controller 和 mapper。推荐算法是离线计算的它不该依赖订单接口商家后台权限是独立的也不该和商城前台用户混在一个 service 里。数据库建模是整个项目里返工次数最多的部分我最后确定的表结构大致如下表名作用关键字段t_product商品主数据id, brand_id, category_id, product_name, sku_id, price, stock, status, source_keyt_user_behavior用户行为流水id, user_id, product_id, behavior_type(click/cart/order/favorite), score, created_att_recommendation_rule商家推荐规则id, merchant_id, product_id, rule_type, weight, start_time, end_timet_merchant商家账号id, merchant_name, login_name, contact, statust_order订单表id, order_no, user_id, merchant_id, total_amount, statussource_key 字段存的是品牌名加原始商品编号这是整个爬虫幂等去重的命根子。t_user_behavior 的 score 按行为类型赋值浏览等于 1收藏等于 3加购等于 4下单等于 10。这样后面做协同过滤时不需要单独维护一张评分表直接从这个字段取值。推荐系统的输入不一定是用户主动打出的星星评分很多网站的评分功能形同虚设但行为日志一定有。把行为转成分数值是低成本高收益的做法。2. 爬虫模块从零实现requests 并发采集和数据清洗的每一步2.1 数据源怎么选优先公开数据合规才是长期主义爬虫模块的定位是“为项目提供足够的化妆品数据”不是专门去对抗某个大型电商平台。我的建议是优先选择三类数据源公开的化妆品成分评测数据集整理成 JSON 或 CSV 后直接入库品牌官网公开的产品规格、价格、成分页少量可以在商家授权范围内补充的结构化测试数据。提醒一句如果目标站点的 robots.txt 明确不允许采集或者接口做了复杂签名就不要硬碰。一套系统的价值在于稳定可复现而不是数据量越大越好。我实际能稳定采集的量已经足够支撑商城展示和推荐算法演示重要的是把“采集 - 清洗 - 入库”的工程链路跑通。合规、稳、可复现比爬得多重要得多。2.2 采集脚本的字段设计和主流程采集脚本用 Python 实现核心流程分四步。第一步构造种子列表从起始页拿到品牌列表生成待采集 URL 队列。第二步请求页面用 requests.Session 配置公共 header 访问详情页。第三步解析内容用 BeautifulSoup 按页面 DOM 结构抽取价格、规格、成分、销量。第四步结构化输出每条记录写成一个 dict再批量写入 MySQL或者先写 JSON 行文件由 Spring Boot 的后台功能读入到主库。字段设计上我不建议直接把页面看到的文本全部塞进表里。比如页面上“现价 299原价 329”这种字符串要拆成 price_now 和 price_origin 两个数值字段销量“10万”要换算成 estimated_sales_min 和 estimated_sales_max。推荐算法和可视化统计都依赖数值字段这一步如果不做后期清洗成本会成倍上涨。而且当你给商家展示“价格分布区间”这类图表时字段类型是字符串的表会让聚合查询非常难写。2.3 并发设计为什么我选 ThreadPoolExecutor 而不是 asyncio“爬虫并发设计到底哪个好”是每个写爬虫的人都会纠结的问题。我的结论是如果你的瓶颈在 I/O 等待也就是 HTTP 请求返回和数据库批量插入这些场景并发粒度控制在 8 到 16ThreadPoolExecutor 和 asyncio 的最终收益差别不大。但 ThreadPoolExecutor 的调试和异常处理更直观断点进去能看到每个线程的执行情况不怕某个协程忘记 await 导致整个事件循环挂住。我当时写的核心并发逻辑大概是这样的from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_one(url): # 带 retry 和超时控制的单个页面抓取逻辑 pass with ThreadPoolExecutor(max_workers8) as executor: futures [executor.submit(crawl_one, url) for url in urls] for future in as_completed(futures): result future.result() if result: all_results.append(result)两个关键细节要注意第一requests.Session 不能在线程间共享它不是线程安全的。我会给每个线程创建独立 Session或者用 threading.local 保存避免出现连接串用错、cookie 乱掉这种诡异问题。第二失败重试必须做在单个任务内部不要依赖外层统一重试。如果一个坏 URL 抛异常了它应该内部重试两次后记录失败日志而不是让整个线程池停下来。2.4 去重、幂等和增量更新的工程细节爬虫最容易埋的雷就是重复数据。我遇到过一个典型场景品牌详情页里有“相关推荐”区块如果不做过滤会把推荐位里出现但当前列表并不存在的商品也当详情抓下来导致同一商品出现多个不同的价格记录。去重策略我用了三层。第一层是 URL 去重用布隆过滤器记录已经处理过的详情页 URL避免同一个详情页被抓两次。第二层是业务主键去重入库时对 source_key 做唯一索引防止同一条商品记录在并发写库的竞争条件下重复插入。第三层是快照对比更新前先查旧记录如果价格、销量字段的差异超过预设阈值就插入一条变更历史否则只更新时间戳。增量更新的核心是把“删除-重插”改成“比对-更新”。我最后实现的是一个 upsert 语句source_key 冲突时更新指定字段。这样即使每天跑一次定时任务也不会把商城正在展示的商品记录搞丢。2.5 爬虫和 Spring Boot 模块的松耦合方式爬虫脚本和 Spring Boot 应用之间我只留了一个约定数据库表结构和状态字段。Python 脚本在采集完成后通过命令行参数指定要写哪张 raw 表Spring Boot 这边提供一个 AdminController 接口可以触发“把 raw_data 清洗成商品主数据”的同步任务。这样做的最大好处是爬虫挂掉了不会影响业务系统业务系统挂掉了也不会阻塞爬虫采集两边的开发节奏完全独立。实际运行中最省心的方案是在部署机配 crontab 每天凌晨跑脚本白天商城访问高峰期不受影响。这样既保证了数据每天更新又不会让爬虫任务占用白天的数据库连接和网络带宽。如果你希望更实时可以改成消息队列触发但项目初期真没必要。3. 推荐系统实现的关键决策两套召回加商家权重比堆复杂模型更有用3.1 用户行为收集从埋点到行为打分推荐系统的地基是行为数据。我在商品详情页和加购、下单接口里埋了行为上报点但要注意不是所有请求都值得进推荐计算。比如买家反复刷新页面、爬虫测试请求、用户连续点击同一商品超过五次这类噪声样本都需要在清洗阶段过滤掉。我给行为流水表增加了一个 batch_id 字段运行定时清洗任务时只处理最近 30 分钟的新增数据避免每次都对全表做扫描。行为打分公式我采用简单加权点击 1 分、收藏 3 分、加购 4 分、下单 10 分。具体权重可以根据业务调整但方向不能反——下单行为一定比浏览行为更能代表用户偏好。打分后数据写回 t_user_behavior 表供协同过滤计算使用。这套思路的关键不是说分数多精确而是把不同行为的价值差异体现出来让推荐算法有个可比较的排序信号。3.2 Item-CF 的实现步骤相似度计算和 Top-N 推荐推荐部分我选了基于物品的协同过滤也就是 Item-CF。原因很实际用户规模还没到千万级不需要上大规模向量召回商品数量在几千到几万的量级比用户数小得多相似度矩阵可以离线算好冷启动容易兜底新用户没有行为时直接推荐热门榜和商家加权榜。Item-CF 核心分三步。第一步构建“用户-商品”倒排索引从行为表中取出打分数据。第二步计算商品之间的余弦相似度把相似度矩阵写入 MySQL 表或 Redis定时离线更新。第三步推荐时找出用户最近交互过的商品集合按相似度加权求和生成 Top-N 候选列表。这里有个新手容易踩的坑不要试图把全量用户-商品矩阵读进内存计算相似度。正确做法是分页取最近 90 天的行为数据只对“出现过交互的商品”计算两两相似度对长期没有新行为的商品直接走离线任务兜底不要在用户访问推荐接口时实时计算。3.3 基于内容的推荐成分标签和类目标签的补充Item-CF 有一个天然问题新商品没有足够共现行为时很难被推荐出来。所以我同时实现了基于内容的召回用商品本身的标签做相似度计算。化妆品领域的标签非常典型类目有精华、面霜、防晒、洁面肤质有干皮、油皮、敏感肌成分有烟酰胺、玻尿酸、水杨酸、A醇。每个商品维护一个标签向量比如“类目精华肤质敏感肌成分烟酰胺”。对用户行为记录中的商品标签做统计得到用户的“标签偏好向量”再和候选商品的标签向量做余弦相似度。实现不复杂但新商品冷启动的表现比纯 Item-CF 好很多。当你给商家后台展示“推荐位为什么出现这个商品”的说明时内容标签也能作为解释依据因为用户偏好专注敏感肌修复而这个商品主打敏感肌可用。3.4 商家权重干预推荐系统不是黑盒商家要有控制权这是做商家后台时我最坚持的设计。推荐系统如果完全不透明商家无法理解为什么自己的商品排序靠后。于是我在推荐候选集合并入排名阶段加了一个商家可控权重最终排序分可以近似理解为协同过滤得分乘以 0.6加上内容匹配得分乘以 0.3再加上商家运营权重乘以 0.1。商家可以在商家后台设置“品牌曝光加成”和“指定商品置顶时段”在合理范围内影响排序结果。这样既保留算法的个性化能力又让运营有可操作空间。商家权重不能无限大。我在 rule_type 字段里规定了几种受限操作置顶最多 3 个商品一天最多申请 2 次每次不超过 4 小时。目的是防止商家恶意刷曝光导致用户体验崩坏。推荐系统不可能永远准确它能做的是在算法结果和商业运营之间找一个可配置的平衡点而不是让某一方彻底压制另一方。3.5 推荐结果缓存把 Redis 放到推荐接口前面推荐接口如果每次请求都实时算一遍QPS 一上来基本必挂。我的方案是离线任务每天凌晨把相似度矩阵和 Top-K 推荐结果写入 Redis接口只读取缓存结果只有缓存过期或用户首次访问时才触发一次同步计算。缓存结构用了两种用户维度是 recommend:user:{userId} 保存 Top-N 商品列表TTL 设为 2 小时商品相似度是 sim:product:{productId} 保存相似商品 JSONTTL 设为 24 小时因为商品相似度矩阵变化频率比用户推荐结果低得多。缓存击穿是容易踩的坑。如果推荐任务是每小时刷新正好在刷新瞬间有很多用户同时请求失效的数据就可能击穿。我的做法是加一个单飞线程当缓存为空时只允许一个线程回源计算其他线程阻塞等待回源成功后统一返回。配合 Redis 可视化客户端观察 key 的 TTL 和命中率基本能第一时间发现缓存策略失衡的问题。4. 商城与商家后台权限、库存、推荐位的业务闭环4.1 商品和商家权限模型的设计商城侧的常规功能是商品展示、搜索、详情、加入购物车、下单。需要重点说明的是商家权限模型因为多人共用一套后台最容易出权限事故。我设计了三类角色商城管理员管理所有品类、审核商家、配置全局推荐位商家只能管理自己的商品、库存、订单和推荐位运营角色只读统计报表允许导出数据。Spring Boot 侧用 Spring Security 加 JWT 做认证接口上通过 PreAuthorize(hasRole(MERCHANT)) 控制商家访问范围。我在这里踩了一个坑JWT 里只放 userId结果发现一个用户可能对应多个商家账号或者一个商家下有多个操作员。后来我把 user_id 和 merchant_id 都放进 Token 的 Claims 里扩展了商家管理员子账号表才算理顺。如果你做类似的商家系统一开始就规划好用户和商家是多对多还是多对一后面会少改很多代码。4.2 商家后台的关键操作与推荐位申请流程商家的核心诉求其实很朴素上架商品、改价格、盯库存、看销量、争取推荐位。推荐位申请流程是我觉得比较顺的一个闭环。商家先选择一个商品再选择推荐类型包括置顶、分类加权、品牌曝光三选一。系统校验商家的积分和价格限制比如商品价格不能高于同类目平均价的 30%防止价格虚高商品霸占曝光。管理员审核通过后数据写入 t_recommendation_rule 表。推荐系统在下一轮离线计算中读取新规则调整候选排序。这个流程和推荐算法接得比较自然。因为规则表是独立字段商家提交申请后不需要改任何推荐算法代码只是在最终排序分里增加一个权重来源。如果你把推荐规则硬编码在算法里商家每提一次申请都要发版运营效率和系统稳定性都会出问题。4.3 订单-库存-推荐榜的一致性设计可视化大屏上有一个“今日热卖 Top10”很多人以为榜单是从缓存里读的。我设计时明确要求这个榜单必须是订单库的实时聚合结果不能依赖推荐缓存。因为推荐缓存是基于行为的预估值而运营看板需要的是准确的成交事实。实现上我建了一张定时聚合表 t_stat_hourly每小时从订单表汇总一次销量、销售额和热卖品类大屏接口只查这张聚合表不直接扫订单表。否则高峰期查询量大订单表会被统计 SQL 拖垮。还有一个容易忽略的问题库存不足的商品必须从推荐榜剔除否则系统会把没货的商品反复推给用户。我把库存状态耦合进推荐候选过滤条件每次生成推荐列表时都检查库存字段。这个逻辑虽然简单但它的意义很大——推荐系统必备的准确性和可用性往往就是被库存、下架状态这些小细节决定的。5. 可视化大屏把系统数据变成商家能看懂的决策信息5.1 大屏技术选型ECharts 加独立前端工程可视化大屏我单独建了一个前端工程用 Vue3 加 ECharts数据接口由 Spring Boot 的 /api/stats 提供。之所以单独建工程而不是塞进商城管理后台是因为大屏页面的布局比例、刷新频率和普通后台差异太大混在一起会导致路由和样式互相干扰。ECharts 的文档资料最全社区方案也多对地图、折线图、柱状图、词云这些大屏常见图表的支持都很成熟。如果你自己搭数据可视化不建议一开始就上重量级 BI 平台ECharts 直接写配置项就可以满足绝大部分展示需求。5.2 商家端核心指标与接口聚合大屏上我展示的核心指标分成四块。第一块是实时成交今日订单量、销售额、客单价。第二块是商品分析热卖 Top10、滞销 Top10、库存预警。第三块是用户分析新增用户数、复购率、用户画像标签分布。第四块是推荐效果推荐位点击率、推荐转化率、各渠道曝光占比。推荐效果这一块最能体现系统价值。因为商城内有“自然列表”和“推荐位”两种曝光方式我在商品卡片点击上报时带上 position 字段标记来源是 natural 还是 recommend。可视化接口按来源分组对比点击率、加购率和成交率。这样运营就能直观回答一个问题推荐系统到底是不是在帮商城提升转化还是只是把流量从自然位挪到了推荐位并没有增量。这个对比口径比单纯展示推荐点击率更有说服力。5.3 大屏适配踩坑记录设计稿到 1080P 的缩放问题大屏设备分辨率常见的是 1080P 或 4K但设计稿有时候会按 3000 乘 2000 来做。直接写像素单位会导致在小屏上要么溢出、要么留白严重。我最后的适配方案是根元素字体大小不写死用动态 rem 以 1920 乘 1080 为基准缩放图表容器不设固定高度用 flex 布局每个图表的尺寸在 resize 事件里重新计算。对于极端尺寸比例的设计稿用 scale 方案把整体内容等比缩放保证不溢出。还有一个隐藏坑大屏页面上的轮播和闪烁效果如果用了 setInterval但组件卸载时没有清理定时器切换路由后会内存泄漏甚至出现叠影。这个坑在后台管理页面里不常见但在大屏项目里几乎必踩。我最后统一封装了一个 useInterval 工具让定时器生命周期和组件绑定页面销毁自动清理。5.4 数据刷新与任务调度的配合可视化数据接口每 5 分钟刷新一次刷新的节奏不是前端频繁请求数据库而是后端先把数据聚合到 t_stat_hourly 表再通过 /api/stats 接口提供结果。前端用 setInterval 每隔 5 分钟轮询一次接口。这个间隔足够展示趋势变化也不会对数据库造成压力。我还给大屏接口加了一层 Redis 缓存聚合任务结束后主动刷新缓存大屏请求永远读缓存数据。这样即使有几张大屏同时挂在会议室后端也能毫无压力地扛住。如果你想做更实时的推送可以在本地上用 WebSocket 把聚合结果主动推给前端但对一个商家展示场景来说5 分钟轮询已经足够。6. 上线前必须处理的稳定性与安全细节6.1 定时任务与数据一致性的取舍现在系统里至少有三类定时任务每天凌晨的爬虫采集、商品数据清洗、推荐矩阵重算、缓存预热每小时一次的订单聚合统计每 5 分钟一次的大屏数据刷新。这些任务我用 Spring Task 调度最轻量也够用。唯一的注意点是任务之间有先后依赖比如商品清洗必须等爬虫采集完成推荐矩阵重算又必须等商品主数据稳定。我把整个任务链拆成不同 job按固定时间错峰执行而不是写一个巨型方法把每件事全部塞进去。数据一致性上我的原则是最终一致而不是强一致。爬虫凌晨写 raw_data 时商城用户正在看的商品主数据不受影响等到清洗任务跑完主数据才整体更新。就算某个任务执行失败上一版数据仍然可以正常支撑商城展示和推荐不会出现线上商城拿不到数据的情况。6.2 Spring Boot Actuator 未授权访问差点把系统资料暴露了这是一个非常值得写出来的坑。Spring Boot 2.x 引入 actuator 后只要依赖存在而且没有显式配置很多端点默认暴露比如 /actuator/env、/actuator/heapdump、/actuator/threaddump。我前期在本机开发没注意结果部署到服务器后用端口扫描工具检查发现外部可以直接访问 /actuator/env里面带数据库连接串、Redis 密码等敏感配置。这个问题如果上线才发现等于把整个系统底裤亮给所有人看。处理方式其实很简单关闭非必要端点只保留 health 和 info设置独立管理端口并且让管理端口只允许内网访问如果不对公网提供监控能力就不要把 actuator 放出去。这个坑在上线前一定要排查一遍它的优先级高于推荐算法调参。项目能跑通只是第一步安全稳不稳才是能不能拿来展示和交付的分水岭。6.3 Redis 和 MySQL 的数据一致性策略商城用 Redis 缓存了商品详情和推荐列表MySQL 是最终数据源。更新商品价格后缓存如果不删用户看到的还是旧价格。我的策略是先更新数据库再删缓存而不是先删缓存再写库。因为并发请求下删缓存后写库完成前可能有其他线程把旧数据读回缓存最终导致脏读。后来我用了 Canal 监听 MySQL binlog 变动的方案把商品表变更自动同步成 Redis 删除动作这样只要 MySQL 里发生变化缓存也会跟着更新一致性保障更稳。如果你觉得 Canal 部署太重先在代码里做双删也可以更新数据库后删除缓存延迟几百毫秒再删一次。但项目演示和真实生产环境我推荐 Canal因为代码侵入更小依赖关系更清晰。6.4 部署流程与演示环境的注意事项整个项目部署结构比较标准后端 Spring Boot 打包成 jar用 systemd 守护进程运行前端大屏和管理后台打包成静态文件用 Nginx 托管动态请求反向代理到后端 8080MySQL 8.0 和 Redis 6 跑在同一台服务器。这套部署方案足够支撑一个演示级别甚至小范围商用的系统。演示环境有一个经验可以分享比如明天要给客户或者老师演示不要等到现场才去跑爬虫现场网络很可能访问不了目标站点或者目标站点的页面结构已经变了。我会在演示前把商品数据快照导入演示库把爬虫脚本的开关停在只展示历史采集数据的状态保证演示时商城、推荐、大屏都是正常可用的。这个细节不复杂但能让演示效果稳定很多。这个项目做完我最大的体会是一套系统里最复杂的往往不是某个独立技术点而是数据如何在模块之间顺畅流动。爬虫负责把外部的商品信息接进来推荐系统负责把行为转成偏好商家后台负责让人能干预排序可视化大屏负责让所有数据变得可读。每一环的单点实现都不算高深但串起来后它就是一个能演示、能运营、能持续迭代的完整产品。如果你打算照着这个思路做一个类似的项目我建议你先花一半时间把数据链路跑通包括爬虫采集、清洗入库、行为埋点、统计分析。推荐算法再花哨也是建立在数据质量之上的。先把最朴素的两套推荐候选跑稳把商家可干预的权重设计好把可视化指标口径定义清楚比什么都重要。等这套闭环真正运转起来你也就理解了为什么说一个毕业设计级别的系统同样能体现架构能力和工程素养。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →