news 2026/10/14 23:23:41

Python电商比价系统实战:爬虫+Flask+可视化+大模型Agent

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python电商比价系统实战:爬虫+Flask+可视化+大模型Agent

每到毕业季,总能看到一堆人在群里问同一个问题:“计算机毕业设计做什么题目好?”翻来覆去无非是管理系统、商城、博客,工作量看着不少,答辩一深入问就露馅。今天分享的这个项目——Python多平台电商比价系统,算是这几年我见过性价比比较高的选题之一。它把爬虫、后端、数据分析、可视化、甚至大模型Agent这几个热门方向全都揉在了一起,既撑得起毕业设计的技术广度,也具备实际的应用价值。

这篇文章就把这个系统的完整设计与实现思路拆开来讲,从需求分析到技术选型,从爬虫实现到Flask后端,从数据可视化到大模型智能比价助手的集成,全部走一遍,并把我踩过的坑、调过的bug一并交代清楚。无论是打算直接参考复现,还是想基于这个方向做自己的改动,都能少走不少弯路。

1. 系统定位与整体设计思路

1.1 这个项目到底解决了什么问题

如果只是做一个“查价格”的小工具,其实没有多大意义。真正有价值的,是它能把比价过程从纯手工操作变成一个自动化的数据闭环。

先说说用户的真实痛点。我们在电商平台买东西时,通常要在几个平台之间来回切换,自己记住型号、对比价格、翻评价,非常耗时。更重要的是,价格本身是一个动态信息,今天最低价可能明天就变了,纯粹靠眼睛盯根本不现实。而这个系统的核心价值就在于:定时采集多个电商平台的商品信息,入库后做清洗和结构化处理,再通过可视化看板和分析模块,把“某商品在哪买最划算”“历史价格走势如何”“当前是不是好价”这些判断自动化。

从毕业设计的角度来说,这个题目的好处在于它的技术链足够长,每个模块都可以单独拿出来做深入研究,也方便在答辩时展示工作量。从实际应用角度来说,它可以作为一个小小的价格监控工具来用,自己想买什么东西时先跑一遍怎么也比盲目下单强。

1.2 功能模块划分与技术选型思路

整个系统我按功能拆成了五个核心板块:

  • 数据采集层:负责从各电商平台自动搜索商品、获取价格信息、抓取商品详情。
  • 数据存储层:对采集到的原始数据进行去重、清洗、归一化,然后存储为结构化数据。
  • 后端服务层:使用Flask构建RESTful API,对外提供商品查询、价格查询、比价结果等接口,同时管理采集任务的调度。
  • 数据分析与可视化层:对历史价格数据进行走势分析和优惠力度评估,提供图表看板。
  • 智能交互层:接入大模型Agent,支持用自然语言进行比价、获得购买建议。

为什么选择Flask而不是Django或者FastAPI?一个很实际的原因是:毕业设计也好,个人项目也好,讲究的是快速出效果。Flask足够轻量,路由设计灵活,配合SQLAlchemy做ORM,一套完整后端下来代码量不会太冗余,非常容易和前端页面联合调试。当然如果你后续想提高吞吐量,切换到FastAPI也不难,因为底层逻辑是一样的。

爬虫部分我选择了Selenium,而不是requests或者Scrapy,也有我的考量。电商平台前端大量使用动态渲染技术,很多关键数据(比如促销价、实时库存)都是通过JavaScript异步加载的,单纯用requests拿到的静态页面里常常什么都没有。Selenium通过驱动真实浏览器来渲染页面,能最大程度模拟用户操作,兼容性也更好。虽然效率比异步爬虫低,但胜在稳定性强,对新手友好,出了问题也容易定位。

存储方面,如果数据量在百万级以内,直接用SQLite完全足够,省去安装数据库的麻烦。如果想让项目看着更“正式”一些,也可以换成MySQL,代码层面改动量很小,主要就是SQLAlchemy的配置串替换一下。

宏观架构一句话总结就是:Selenium负责采集,SQLAlchemy负责持久化,Flask负责提供接口,ECharts负责可视化,大模型负责智能交互。各层之间通过JSON数据进行通信,职责清晰,也方便后期单独扩展。

2. 数据采集层:多平台爬虫的实现细节

2.1 定义一个通用采集流程

做过多平台采集的都知道,最忌讳的是每个平台一套采集逻辑,维护量会翻倍。我的做法是先定义一套通用采集流程,再针对各平台页面结构差异做微调。

通用的采集流程可以抽象成四个步骤:

  1. 搜索商品:向目标平台发起搜索请求,得到结果列表页。
  2. 解析列表页:从结果列表中提取商品名称、价格、店铺名、销量等基本字段。
  3. 访问商品详情页:补充图片、规格参数、评价数等更详细的信息。
  4. 数据入库:将结构化数据写入数据库,同时记录采集时间和来源平台。

具体到Selenium的操作上,每一步都对应一组页面元素定位和操作方法。刚开始会走很多弯路,因为每个电商平台的页面结构差异真的很大。后来我学乖了,不追求一套选择器通吃,而是在每一个平台适配器中维护一组独立的定位策略,只是流程框架共用。这样后期某个平台的页面改版时,只需要修改对应适配器的选择器,不需要动整体架构。

2.2 Selenium的关键配置与反反爬策略

Selenium抓取电商网站,需要关注的几个点我挨个说一下。

浏览器驱动配置。使用Selenium Manager可以自动处理浏览器和驱动版本匹配的问题,不用再手动下载driver。下面这段配置是经过多次测试后比较稳定的一版:

from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(): options = Options() # 无头模式,降低资源占用,部署时建议开启 options.add_argument("--headless=new") # 禁用 GPU 加速,某些版本的无头模式不关掉会报错 options.add_argument("--disable-gpu") # 屏蔽自动化控制提示 options.add_argument("--disable-blink-features=AutomationControlled") # 设置窗口大小,避免某些网站对窄屏返回移动端页面 options.add_argument("--window-size=1920,1080") # 替换默认 UA options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") driver = webdriver.Chrome(options=options) # 隐藏 webdriver 特征,降低被识别风险 driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") return driver

这些参数的设置不是拍脑袋来的,每一个都有实际意义。比如--disable-blink-features=AutomationControlled和隐藏webdriver属性,都是用来降低自动化工具被网站识别风险的。无头模式一方面减少内存占用,另一方面能让采集任务在服务器上稳定跑,不会弹出浏览器窗口干扰其他操作。

等待策略是爬虫的另一个重头戏。我见过很多新手一上来就写time.sleep(3),这种方法看着简单但问题很大:网络慢的时候3秒不够,页面早已加载完的时候又在白白浪费时间。推荐的做法是显式等待,明确等待某个元素出现再继续执行:

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_for_element(driver, selector, timeout=15): element = WebDriverWait(driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, selector)) ) return element

这里有个实操经验:列表页加载出来了不等于商品数据全部渲染完成。很多网站的页面是分步渲染的,先出现页面框架,再异步填充商品数据。所以最稳妥的等待目标是某个“商品列表容器”的出现,同时追加一个条件,等待容器内部子元素数量大于0。如果仅仅等待外层节点出现,拿到的可能还是一个空列表。

2.3 商品清洗与同款匹配

采集回来的数据往往是脏的。最典型的问题包括:

  • 同一款商品在不同平台上的标题写法不一致。
  • 价格字段中混有促销文案,比如“满300减50后到手价¥2599”这种。
  • 价格单位不统一,有的平台展示为“2599”,有的展示为“2,599.00”。
  • 同一商品存在多个SKU版本,需要合并处理。

数据清洗的第一步是价格归一化。我的做法是使用正则表达式从价格文本中提取所有数值,再根据上下文判断哪个才是最终销售价。比如"¥2599.00"直接提取为2599.00;"券后价1999"则提取1999。遇到“满减”“秒杀”这类文本时,把活动规则也存到一个独立字段中,方便后续做优惠力度分析。

比价系统最核心的一个环节是同款匹配。不同平台对同一商品可能写着完全不同的标题,比如“Apple iPhone 15 Pro 256G 原色钛金属”和“苹果15 Pro手机 256G 钛金属色”,普通字符串匹配基本无法直接关联。我这里用了一个比较实用的方案:先做结构化特征提取,从标题中抽出品牌、型号系列、容量、颜色等关键属性,再基于这些属性构建特征向量计算相似度。

import re from difflib import SequenceMatcher def normalize_product_label(title): # 提取品牌、型号、规格等核心关键词 brand_pattern = r"(Apple|华为|小米|OPPO|vivo|荣耀|联想|戴尔|惠普)" model_pattern = r"([A-Za-z0-9]+ ?\d{2,})" brand = re.search(brand_pattern, title) model = re.search(model_pattern, title) features = [] if brand: features.append(brand.group(1).lower()) if model: features.append(model.group(1).lower().replace(" ", "")) # 提取容量规格,如 256G / 512G / 1TB capacity = re.search(r"(\d+ ?G[B]?)", title, re.IGNORECASE) if capacity: features.append(capacity.group(1).lower().replace(" ", "")) return " ".join(features) def is_same_product(title_a, title_b, threshold=0.75): feat_a = normalize_product_label(title_a) feat_b = normalize_product_label(title_b) if not feat_a or not feat_b: return False sim = SequenceMatcher(None, feat_a, feat_b).ratio() return sim >= threshold

这个方案的思路是:不需要理解标题的语义,只需要把真正能标识商品内核的特征提取出来,然后做相似度匹配。实际测试下来,对手机、笔记本这类标品准确率非常高,但对于服装等非标品效果比较差,因为颜色尺码变量太多。如果你也想把非标品纳入比价范围,建议额外引入商品图片的特征比对,这个属于另一个方向,可以根据自己的精力来考虑。

另一个隐蔽得多的问题是价格单位处理。有的平台会显示“万”级别的数字,比如“1.2万”代表12000,爬虫必须把这些中文数字表达转换为纯数值。还有的会显示“今日到手价 ¥2499,已降300元”,如果不加处理,数值提取结果会出现多个候选值。我的建议是优先匹配带有“到手价”“券后价”“成交价”这些明确语义前缀的数值,提取不到再回退到第一个出现的数字,并打上可疑标记,宁可交给人工确认也不要录入错误数据。

3. Flask后端:从接口设计到数据存储

3.1 数据表设计与存储方案

采集层把数据整理干净之后,后端要接得住。我的数据库设计初版包含三张核心表:

  • product表:存储商品基本信息,包括标题、品牌、型号、图片链接等。
  • sku表:存储商品的SKU规格信息,比如颜色、版本、内存大小。
  • price_record表:存储每次采集到的价格快照,这是整个系统的核心,历史价格走势全靠它。

其中price_record表的设计需要特别注意。它的每一个记录代表某个SKU在某个时间点的价格状态,字段大致可以设计成这样:

class PriceRecord(Base): __tablename__ = "price_record" id = Column(Integer, primary_key=True, autoincrement=True) sku_id = Column(Integer, ForeignKey("sku.id"), nullable=False, index=True) platform = Column(String(32), nullable=False) price = Column(Numeric(10, 2), nullable=False) original_price = Column(Numeric(10, 2)) discount = Column(Numeric(5, 2)) # 折扣率,比如 0.85 coupon_info = Column(String(255)) # 优惠券/满减信息 promotion_tag = Column(String(128)) # 秒杀、百亿补贴等标签 is_available = Column(Boolean, default=True) # 是否有货 crawled_at = Column(DateTime, default=datetime.utcnow, index=True)

price_record表要建索引的字段包括sku_id和crawled_at,因为高频查询都是“某商品的价格历史”和“某时间点的全平台价格”,这两个字段是索引的天然候选。数据量一旦到了几十万条,没索引的查询会慢到让你怀疑人生。

采集时间字段建议用时间为整数时间戳存储,是否使用 UTC 取决于你的部署环境。如果只在本地跑,直接用本地时间会更直观;但如果要部署到云服务器且涉及定时调度,统一存UTC时间戳,展示时再转换,能有效避免时区错乱带来的分析偏差。

3.2 后端API怎么设计

Flask后端不承载复杂业务逻辑,主要做三件事:提供查询接口、接收爬虫数据、维护采集任务状态。我这里定义了几组核心接口:

  • GET /api/search?keyword=手机&sort=price_asc:分页搜索商品,支持按价格排序。
  • GET /api/product/<id>:获取商品详情信息,包括所有SKU的实时价格。
  • GET /api/product/<id>/history?days=30:获取某个商品30天内的价格走势。
  • GET /api/compare?keyword=轻薄本:触发多平台比价,返回各平台最低价与该商品的历史价格对比。
  • POST /api/crawl/task:创建采集任务,启动针对某关键词的多平台采集。
  • GET /api/crawl/status/<task_id>:查看采集任务实时进度。

其中/api/compare这个接口逻辑是整个项目里最核心的,它就是系统“比价”能力的直接体现。执行逻辑是:先通过关键词匹配到对应的商品聚合组,然后查询该商品组在每一平台的最近一条价格记录,计算最低价、最高价、均价以及当前价格相对历史最低价的偏移率。

@app.route("/api/compare", methods=["GET"]) def compare(): keyword = request.args.get("keyword", "").strip() if not keyword: return jsonify({"error": "参数 keyword 不能为空"}), 400 # 查询匹配的商品 - 这里用 ILIKE 模式匹配做简化演示 products = Product.query.filter(Product.title.ilike(f"%{keyword}%")).limit(10).all() result = [] for p in products: latest_prices = [] for sku in p.skus: records = PriceRecord.query.filter( PriceRecord.sku_id == sku.id, PriceRecord.is_available == True ).order_by(PriceRecord.crawled_at.desc()).limit(5).all() if not records: continue # 取最近一次有效价格 latest = records[0] prices = [r.price for r in PriceRecord.query.filter( PriceRecord.sku_id == sku.id, PriceRecord.crawled_at >= datetime.now() - timedelta(days=30) ).all()] min_price = min(prices) if prices else latest.price current = float(latest.price) trend = (current - min_price) / min_price * 100 if min_price else 0 result.append({ "product_id": p.id, "title": p.title, "platform": latest.platform, "current_price": current, "min_price_30d": float(min_price), "price_fluctuation": round(trend, 2), # 正数表示高于最低价 "crawled_at": latest.crawled_at.strftime("%Y-%m-%d %H:%M:%S"), }) result.sort(key=lambda x: x["current_price"]) return jsonify(result)

这段代码的意图是:传入一个关键词,返回多个平台各自的最低价格选项,并标出当前价格与30天内最低价的波动百分比。使用latest = records[0]之前要注意,order_by(desc).limit(5)取到最近5条记录,而records[0]是最新的一条,逻辑上没问题,但这里想强调的是,每次采集都会新增记录,所以查询时一定记得过滤crawled_at时间范围,否则趋势分析全被历史数据污染。

3.3 并发采集任务如何调度

采集任务如果设计成同步请求,用户发起一次比价就得等爬虫把所有平台扫完,那体验基本是灾难。一两个平台还好,如果是五个平台,同步等待时间可能超过一分钟。所以后端设计需要考虑异步任务调度。

最轻量的方案是直接用Python的threading模块开线程跑采集,配合一个字典维护任务状态。数据量不大时完全够用。如果追求更规范的架构,可以换成Celery,但毕业设计我个人建议不搞那么复杂,线程池+任务状态字典反而好理解、好调试。

import threading import uuid crawl_tasks = {} def run_crawl_task(keyword): task_id = str(uuid.uuid4()) crawl_tasks[task_id] = {"status": "running", "progress": 0, "keyword": keyword} def _worker(): try: results = crawl_service.crawl_keyword(keyword) crawl_tasks[task_id]["status"] = "success" crawl_tasks[task_id]["progress"] = 100 crawl_tasks[task_id]["results"] = results except Exception as e: crawl_tasks[task_id]["status"] = "failed" crawl_tasks[task_id]["error"] = str(e) threading.Thread(target=_worker, daemon=True).start() return task_id

这个方案唯一的缺点是没有持久化能力,Flask一旦重启任务信息就丢了。对于毕业设计这个程度的项目来说,这点问题完全可以接受。实际使用时,把任务状态放到Redis或者数据库表中,就是标准的任务队列实现了。

4. 数据分析与可视化看板

4.1 价格走势与优惠力度的计算逻辑

数据采集的核心价值最终要靠分析来体现。我的分析模块包括三块内容:

价格走势分析:对单个SKU的价格记录按时间序列聚合,计算每日均价、日最低价和波动幅度。实现上可以用简单的时间分组查询。

优惠力度分析:把original_price(原价)和price(当前价)做对比,得到折扣率。同时结合promotion_tag,可以统计该平台什么时候的“百亿补贴”类商品最多,进而判断活动周期。

价格异常检测:通过比较当前价格和历史价格的分布区间,如果当前价低于历史最低价超过一定阈值,就触发“好价预警”。这个功能实现起来很简单,但实用价值非常高,尤其是对大促期间的购物决策很有帮助。

分析结果如果想要实现“趋势预测”,可以做一个最朴素的移动平均:通过过去N天的价格数据画一条趋势线,让人直观感受当前价格处于什么位置。注意,不建议强行上机器学习做价格预测,因为价格受活动、库存、竞争等多因素影响,简单模型不仅精度不理想,还会给自己增加不必要的复杂度,不容易向答辩评委解释清楚。

4.2 可视化看板的搭建方案

可视化展示我推荐直接使用ECharts,不自己手写图表组件。理由很直接:ECharts的文档非常完善,图表效果成熟且美观,而且支持异步加载数据,配合Flask的API接口非常顺手。

我的看板页面主要包括四个图表:

  • 价格走势折线图:展示某一SKU近30天/90天的价格变化,同时叠加各平台对比的均值线。
  • 平台价格对比柱状图:同一款商品在不同平台的当前价格横向对比,一目了然。
  • 折扣力度散点图:以时间为横轴、折扣率为纵轴,观察促销活动的发布节奏。
  • 热门关键词词云:基于商品标题分词结果生成词云,呈现当前热门品类。

举一个看板页面和前端的交互方式。前端在页面加载时向后端请求数据:

fetch('/api/product/1001/history?days=30') .then(response => response.json()) .then(data => { const prices = data.prices.map(item => item.price); const dates = data.dates.map(item => item.date); const chart = echarts.init(document.getElementById('priceChart')); chart.setOption({ title: { text: '近30天价格走势' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dates }, yAxis: { type: 'value' }, series: [{ type: 'line', data: prices, markLine: { data: [{ type: 'average', name: '平均价' }] } }] }); });

这段代码实现的就是一个标准的前后端数据交互逻辑。注意,ECharts的markLine在折线图中非常实用,可以一键画出平均价参考线,不需要自己额外计算底层的平均值逻辑,在后端已经返回了平均价格的情况下也可以直接用后端算好的值。

这里有个小技巧:价格走势图的Y轴价格单位如果差异太大(比如几百元的耳机和上万元的电脑放在一张图里),可以用yAxis的scale: true开启自动缩放,避免小波动被大尺度淹没。

4.3 看板前端怎么搭

前端部分我的方案是:使用现成的AdminLTE或Bootstrap模板搭建管理后台框架,页面内部用ECharts承载图表。这样工作量小,页面也专业。

整体的前端页面结构可以拆成三部分:

  • 顶部导航栏:展示系统名称、搜索框、跳转链接。
  • 侧边栏:列出每个功能模块的入口,如“商品比价”、“价格监控”、“数据分析”等。
  • 内容区:加载对应页面组件,核心页面以图表和数据表格为主。

前后端分离是首选,Flask仅作为API服务器,前端用HTML+JavaScript即可。如果非得用Flask的模板渲染也没问题,但项目后期前后端分离的扩展性会更好。

有一点要提醒:不要在JavaScript代码中硬编码任何数据库连接信息或重要的密钥。前端页面的一切数据都通过接口获取,接口入口在页面加载时统一请求。一旦密钥暴露在浏览器端,相当于把整个数据库的钥匙交出去了。虽然这只是一个毕业设计,但这种安全意识最好从一开始就养成。

5. 大模型Agent:让比价系统更“智能”

5.1 为什么把大模型和Agent引入这个项目

如果你是想做一个“有亮点”的毕业设计,纯爬虫+展示的比价系统已经不够新了。这两年的技术热点明显转向了大模型应用,把大模型Agent集成进比价系统,恰恰用得上“自然语言交互”这个方向,能让整个项目从“自动化工具”升级成“智能助手”。

一个典型的场景是:

传统搜索模式:用户输入“轻薄本”,系统返回一批商品列表。 大模型Agent模式:用户输入“帮我找一款价格在5000到6000之间、适合编程用的轻薄本,只要京东和某平台的价格对比”,系统自动理解意图,调用后端接口完成搜索、过滤、比价,最后用自然语言返回推荐结果。

这个能力不需要从零训练模型,只需要让大模型学会调用已有的工具和API。我们做的是“应用层”开发,而不是“模型层”开发,门槛比想象中低很多。

5.2 智能比价助手的实现方案

设计一个比价场景下的Agent,核心是定义好**函数调用(Function Calling)**机制。系统预先给大模型提供一组工具定义,大模型根据用户的自然语言输入自行判断需要调用哪个工具,以及传给工具什么参数。

我这里定义了三组核心工具,提供给大模型:

  • search_products(keyword, price_min, price_max, platforms):按条件搜索商品,调用后端查询接口。
  • get_price_history(product_id, days):获取某商品的价格走势数据。
  • get_best_platform(product_id):查询当前全网最低平台和价格。

大模型的工作流程是:接收用户输入,分析意图,依次调用上述工具,获取真实结果后组织成自然语言回复。下面是使用OpenAI兼容接口直接进行工具调用的一个简化示例(这里用某种通用表达,实际你可以替换成DeepSeek或其他平台的接口):

# 这里的代码仅作示意,实际使用时请替换为你调试通过的模型接口 from openai import OpenAI client = OpenAI( api_key="your_api_key", base_url="https://api.example.com/v1" ) tools = [ { "type": "function", "function": { "name": "search_products", "description": "根据关键词和价格区间搜索商品", "parameters": { "type": "object", "properties": { "keyword": {"type": "string", "description": "商品关键词"}, "price_min": {"type": "number", "description": "最低价格"}, "price_max": {"type": "number", "description": "最高价格"}, "platforms": {"type": "array", "items": {"type": "string"}, "description": "指定平台列表"} }, "required": ["keyword"] } } } ] def chat_with_tools(user_input): messages = [{"role": "user", "content": user_input}] response = client.chat.completions.create( model="your_chat_model", messages=messages, tools=tools, tool_choice="auto" ) # 如果模型决定调用工具,则解析并执行 if response.choices[0].message.tool_calls: call = response.choices[0].message.tool_calls[0] tool_name = call.function.name arguments = json.loads(call.function.arguments) result = dispatch_tool(tool_name, arguments) messages.append(response.choices[0].message) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False) }) # 将工具执行结果交还给模型,让模型组织回答 final = client.chat.completions.create( model="your_chat_model", messages=messages ) return final.choices[0].message.content return response.choices[0].message.content

这里的逻辑是:大模型本身并不拥有真实数据,它只是根据用户问题决定要不要调用工具、以及怎么调用。工具执行完把真实结果返回给模型,模型再根据结果组织语言回答用户。这种模式就是Agent的基本形态。

在我自己的实现中,我额外补了一层“会话记忆”,把历史对话追加进messages,这样用户可以追问“那这款内存呢”,Agent能结合上下文继续回答,体验会自然不少。

5.3 成本控制与安全注意

接大模型接口时最容易忽略的问题就是接口Key安全。不管用的是哪家大模型服务,API密钥都不应该出现在前端代码或Git仓库里。正确方式是放在后端的环境变量中,通过Flask的配置模块读取。

成本控制也是很现实的考量。一次对话中如果反复调用工具,可能会产生多次模型请求,每个请求都在消耗token。我的优化手段是:

  • 限制工具调用的最大轮数,避免模型陷入死循环反复查询。
  • 对查询结果做摘要和字段裁剪,只传递必要的核心信息给模型,不要把大量网页原文直接塞进上下文。
  • 将实时性要求不高的数据缓存到Redis或内存中,比如某商品的历史价格,如果15分钟内已被查过,直接把缓存结果交给模型,不重复调用大模型。

另外一个容易被忽视的问题是模型输出的幻觉。大模型在拿不到数据时,可能倾向于“编”一个价格出来。越是在实际比对场景中,越要警惕这点。我的做法是:在最终答案生成前强制拼接一个系统提示,“你只能根据提供的查询结果回答,如果用户要求的数据不在结果中,明确说明未查到,不要推测”。同时在输出前做一个价格合理性校验,如果模型给出的价格与数据库真实值差异过大,就直接用真实值覆盖再重新组织语言。

6. 完整流程串联与操作实录

6.1 一次完整的商品装载流程

讲了这么多模块,把它们串起来看一次实际操作可能会更直观。假设现在要对“iphone 15”进行一次全平台比价:

  1. 创建采集任务用户在前端看板输入关键词“iphone 15”,点击采集,前端调用POST /api/crawl/task,后端创建任务并启动线程。

  2. 多平台爬虫并行工作爬虫模块收到消息后,同时启动多个Selenium浏览器实例(每平台一个),各自执行搜索、解析、采集流程。这一步是最耗时的,通常需要30秒到2分钟。

  3. 数据入库与清洗采集到的每一平台原始数据先写入待清洗队列,由清洗模块统一执行价格提取、规格解析、标题标准化等操作,清洗结果存入sku表和price_record表。

  4. 实时比价查询前端自动调用GET /api/compare?keyword=iphone 15,后端联合查询多平台最新价格,按价格升序返回。页面柱状图显示各平台价格,折线图展示目标平台历史价格走势。

  5. 大模型智能解读如果这时用户打开智能助手页面提问“这个价格合适吗?”,Agent调用get_price_history和get_best_platform获取数据,输出类似这样的回复:“目前最优惠价格出现在某平台,为¥5188,近30天最低价为¥4999,当前价格高于历史最低价约3.8%,建议关注某平台的大促活动。”

这样一个流程走下来,整个系统才算真正“用起来”了。我在演示时一般只会跑3-5个关键词,避免等待时间过长让答辩场面陷入尴尬。

6.2 部署运行时的注意事项

如果你打算把这个系统在服务器上部署,有几个点值得提前考虑。

一是内存和CPU资源。Selenium非常吃内存,每个浏览器实例大约需要300到500MB。同时跑5个采集任务,至少需要4GB以上的内存配置,否则很容易触发内存溢出导致采集线程直接死掉。买最低配的1GB云服务器跑这个系统是行不通的,建议本地跑通后,部署时只保留单平台采集任务并适当延长调度间隔。

二是定时采集的计划设置。价格监控的核心是定期采集,推荐使用APScheduler或者系统自带的cron,把采集任务设置成每4小时或者每天凌晨执行一次。注意避开电商平台的高峰期,比如大促开场瞬间去做数据采集,不仅容易被反爬,就算能抓到也常常是拥堵时的异常价格。

三是前端静态资源的加载。ECharts的JS库建议下载到本地而不是用CDN,因为很多高校实验室的网络环境访问外网CDN速度很慢,答辩演示的时候卡在图表加载上就尴尬了。

7. 常见问题与避坑速查表

7.1 高频踩坑现场

我在开发这个系统的过程中,前前后后踩了不少坑,挑几个有代表性的列出来。

第一个大坑是Selenium元素定位不稳定。同样是某平台,别人的代码能跑,你的就报NoSuchElementException,去页面上一看发现结构完全对不上。原因通常是前端在登录状态下和未登录状态下渲染的DOM结构不一样,或者是页面做A/B测试导致不同用户看到不同版本。我的应对策略是写一个重试机制,元素找不到时自动刷新页面并重试,连续三次失败再放弃记录。

第二个坑是登录墙。部分平台对于未登录用户只展示部分数据,价格甚至显示为“登录后可见”。这种情况下,爬虫最简单有效的方法是:在本地把登录后的Cookie信息导出,拼接进Selenium的浏览器上下文:

from selenium.webdriver.common.by import By import pickle, time # 首次手动登录,将登录后的 Cookie 保存到本地 def save_cookies(driver, path="cookies.pkl"): pickle.dump(driver.get_cookies(), open(path, "wb")) # 后续采集时加载 Cookie,跳过登录 def load_cookies(driver, path="cookies.pkl"): cookies = pickle.load(open(path, "rb")) for cookie in cookies: driver.add_cookie(cookie)

要注意Cookie是有时效性的,特别是登录状态,通常几天到几周不等。代码里要做好异常处理,当发现访问的页面仍然跳转登录页时,主动清除本地Cookie并停止任务,等待人工介入重新登录。

第三个坑是滑块验证码。有些平台在频繁请求时会弹出滑块验证。基础的方案是把浏览器窗口大小设置为真实分辨率,并随机在页面停留几秒,模拟人工浏览节奏。更进阶一点可以用动作链模拟鼠标轨迹滑动滑块,实现起来比较复杂,但也不是不可行。我的建议是克制采集频率,降低对滑块验证的刺激,比什么绕过技巧都管用。

第四个坑是数据量膨胀。一开始可能会觉得多采集点数据更好,于是频繁触发全平台采集任务。到后期才发现,一张price_record表在几次集中采集后就堆了好几百万行。因此设计之初就应该规划好数据清理策略,比如只保留每个SKU最近90天的价格记录,旧数据定期删除或归档。不然系统响应越来越慢,最终无法正常演示。

第五个坑是部署环境不兼容。不少高校实验室的服务器是Linux环境,Chrome和ChromeDriver的安装版本需要严格匹配。有些系统上还缺一些动态库,装完Chrome仍然跑不起来。建议在本地Windows环境开发,部署时提前在目标服务器上完整跑一遍采集流程,别等答辩前两天才去做环境适配。

下面是问题速查表,方便大家直接排查:

现象可能原因解决思路
Selenium找不到元素页面结构变化 / 异步加载未完成显式等待,重试机制,及时更新选择器
价格提取到多个数值促销文案、折扣信息混入优先提取“到手价”“券后价”等语义字段
登录后可见价格登录墙限制手动登录保存Cookies,定期刷新
频繁出现滑块验证请求频率过高降低采集频次,随机等待,限制单次任务量
页面加载正常但数据为空前端渲染采用iframe框架需要切换到iframe内上下文定位元素
数据库查询越来越慢历史记录过多定期清理旧数据,为常用字段建索引
模型回答“编造”价格大模型幻觉强制工具调用,输出前校验真实数据

7.2 性能优化与扩展方向

如果精力有余,想让这个系统在技术层面再上一个台阶,可以从以下几个方向选一个去扩展:

  • 引入消息队列:把线程池换成Celery + Redis队列,采集任务状态持久化,支持分布式采集,多个服务器并行抓取不同平台。
  • 改用Scrapy+Playwright组合:Scrapy负责框架调度和去重,Playwright处理动态页面渲染,效率比纯Selenium高一截。
  • 增加价格预测模型:对某类商品的历史价格序列做简单的时间序列预测(线性回归、Prophet等),为“何时出手最好”提供参考依据。
  • 引入向量数据库做语义搜索:将商品标题、描述等字段做Embedding向量化,实现更精准的模糊搜索。
  • 完善移动端适配:把看板页面封装成PWA应用,或者在微信小程序上做一个轻量版本。

不管最后选择了哪个方向,核心逻辑都是让整个系统更稳定、更高效、更智能,这也正是毕业设计开题时评委希望听到的思路。

我个人做这个项目时,最大的体会是:比价系统的难点不在算法,而在细节的稳定性。页面结构会变,促销文案五花八门,平台的反爬策略也在持续更新,真正花时间的不是写爬虫、调接口,而是不断跟这些细节较劲。但也恰恰是这些“看起来不起眼”的环节,构成了整个系统最扎实的部分。面试时讲这些踩坑经历,比背概念有意思多了,也更能让人信服你真的动手做过。最后再分享一个小技巧:项目跑通后,记得把代码里的所有关键配置项集中放到一个config文件中,这样不管是在本地、服务器还是换一台机器重跑,都能快速恢复环境,不至于在答辩前一天手忙脚乱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 23:23:36

手写Tool Use的三种实战方案:从字符串解析到AST沙箱

1. “手写 Tool Use”到底在考什么&#xff1a;面试官没说破的三层潜台词“面试官让我手写一个 Tool Use&#xff0c;我用了 3 种方案”——这个标题乍看像段程序员日常吐槽&#xff0c;但背后藏着当前大模型应用开发岗位最核心的能力筛选逻辑。我带过十几轮校招和社招技术面&a…

作者头像 李华
网站建设 2026/10/14 23:22:17

new JSONObject(original)是浅拷贝?一文搞懂Java JSON深拷贝与工程实践

“new JSONObject(original) 是浅拷贝”&#xff0c;这句话最早是在一次代码评审里被前辈点出来的。当时我正准备用一行构造器&#xff0c;把一个 JSONObject 变成“独立副本”&#xff0c;然后放心地往里面塞业务字段。评审意见只有一句&#xff1a;这是浅拷贝&#xff0c;小心…

作者头像 李华
网站建设 2026/10/14 23:21:51

AI造AI:揭秘递归自我改进的技术原理与工程实践

1. 递归自我改进到底指什么&#xff1a;先拆掉"AI造AI"这层壳1.1 从"AI帮我写代码"到"AI改自己的训练方式"先别急着把"AI造AI"想象成科幻电影里那种机房里管道交错、机器自己给自己插电的场面。2026年语境下的"AI造AI"&…

作者头像 李华
网站建设 2026/10/14 23:21:24

JavaScript核心机制八支柱:执行上下文、事件循环与原型链深度解析

1. 这不是“入门教程”&#xff0c;而是一张 JavaScript 真实世界的地形图 很多人点开标题&#xff0c;心里想的是&#xff1a;“又一篇讲 var/let/const 区别的文章&#xff1f;”或者“是不是又要从 console.log 开始讲起&#xff1f;”——不。这篇不是给刚敲下第一行代码的…

作者头像 李华
网站建设 2026/10/14 23:21:02

PS5全场景优化指南:从系统初始化到远程游玩的实用配置

很多人买PS5回家第一周的心情是&#xff1a;机器是好的&#xff0c;忙是忙的。这话听起来像玩笑&#xff0c;但真不是。我刚拿到机器那阵子&#xff0c;客厅电视上一切顺利&#xff0c;搬到书房接显示器就出各种状况&#xff1b;内置硬盘装了三个游戏就报警&#xff1b;出差想趁…

作者头像 李华
网站建设 2026/10/14 23:21:02

t3code 实战:用端到端类型安全重构全栈开发效率

开发圈子里有个段子&#xff1a;后端写接口一时爽&#xff0c;前端联调火葬场。以前我挺认同&#xff0c;直到我完整用了一遍 t3code 这套 T3 Stack 方案&#xff0c;才意识到很多所谓的“联调痛苦”&#xff0c;本质上不是沟通问题&#xff0c;而是类型定义和信息流被切成了两…

作者头像 李华