做电商比价系统这个想法,我一开始以为核心难点在爬虫。等真正动手做完这套基于 Flask 与 Python 的多平台电商比价可视化系统,我才反应过来:爬数据只是地基,真正费精力的是把不同平台千奇百怪的商品信息拉到同一个坐标系里比较,再把比较结果用图表和自然语言讲清楚。这篇文章就按我实际开发的顺序,把 Selenium 采集、数据对齐、Flask 可视化、以及最后接入 DeepSeek 大模型做智能问答的全过程拆开讲一遍。如果你正准备做类似的毕业设计,或者单纯想给自己写一个顺手的价格监控工具,这篇文章应该能帮你省掉不少试错时间。
1. 为什么做比价系统,而不是只想做一个爬虫
1.1 这个项目真正要解决的需求
很多人在网上看到一个商品,习惯性会开两三个平台去搜同款,来回切换标签页比价格、比运费、看评价。这个动作看起来很简单,但一旦商品数量上去了,人就比不过机器了。比价系统的本质是把"人肉比价"这个重复劳动自动化:定时抓取各平台的商品信息,统一清洗、对齐、存储,最后用可视化看板和自然语言问答把结论推给用户。
这套系统的使用场景主要有三个。第一是个人购物决策:搜一个商品名,立刻能看到各平台价格横评和历史价格曲线,判断现在是不是入手的好时机。第二是行情监控:比如盯着一款热门电子产品,价格跌破某个心理价位自动提示。第三是作为数据展示项目:完整跑通采集、存储、分析、可视化、智能问答的链路,这比纯写一个增删改查管理系统要有说服力得多,也是很多毕业设计选题喜欢往这个方向靠的原因。
1.2 技术选型:为什么是 Flask + Selenium + MySQL
选型这件事,我在动手前花了不少时间权衡,最后定下来 Flask、Selenium、MySQL 三者组合,理由其实很简单:各选各领域里最省心的方案。
后端框架没有选 Django,也没有追新用 FastAPI。Django 功能全,但 ORM、中间件、Admin 后台这些东西对比价系统来说有点重,学习成本全堆在框架本身;FastAPI 性能好、写起来爽,但异步模型对新手不友好,而且市面上能找到的参考项目相对少。Flask 刚好卡在中间:轻量、灵活、文档多、任何报错基本都能搜到对应解决方案。对一个要写完整论文、又要现场演示的项目来说,可维护性和可解释性比极致性能重要得多。
爬虫这块没有选 Requests + 解析接口的方案,而是直接用 Selenium。现在主流电商平台的前端基本都是重度动态渲染,接口层普遍有签名加密,直接构造 HTTP 请求的维护成本极高,一天可能就要改一次参数,碰上传参反爬直接卡死。Selenium 模拟的是完整浏览器行为,几乎不存在拿不到页面的情况。代价是速度和资源占用,但对毕业设计级别的数据量来说,每天跑一轮几百个商品,完全在可接受范围内。
存储用的是 MySQL,没有上 MongoDB 或者其他时序数据库。商品信息、价格记录、用户行为这些都是结构化数据,用关系型数据库做关联查询很顺手;而且后面做历史价格趋势查询时,MySQL 的索引优化空间足够,不需要额外引入一套大数据组件把架构搞复杂。等数据量真的到了百万级,再考虑迁移也不迟。
1.3 系统模块划分与数据流
整个系统我拆成了五个模块,每个模块职责单一、边界清晰:
- 采集层:Selenium 驱动的多平台爬虫,按任务队列抓取商品列表页和详情页信息。
- 数据层:MySQL 数据库,包含商品表、价格历史表、采集任务表、平台配置表。
- 服务层:Flask 提供 RESTful API,负责搜索、比价、趋势、排行榜等业务逻辑。
- 展示层:基于 ECharts 的可视化看板,展示价格对比、历史趋势、平台分布。
- Agent 层:接入 DeepSeek 大模型,把自然语言问题转成工具调用,返回智能分析结论。
数据流是单向的:爬虫把各平台原始网页抓回来,解析成结构化字段,写入原始数据表;清洗脚本做规格统一和字段修正,进入正式商品表;Flask 从库里取数并加工成前端图表需要的 JSON 结构;Agent 层则通过 API 把用户的问题路由到对应查询函数,拿到结果后交给大模型生成回答。
这个架构的好处是每一层都能单独测试和替换,比如以后想换个前端框架,后端 API 不用动;想加一个平台,只需要新增一个爬虫模块,不用改其他任何地方。
2. Selenium采集层的完整链路,从环境配置到增量更新
2.1 动态页面为什么绕不开 Selenium
比价系统的数据源头是各平台的商品页面。很多人会问:为什么不用 Requests 直接拿 HTML?问题在于现在电商页面基本都是动态渲染的,商品价格、促销信息、库存状态这些关键数据,很多是通过 JavaScript 在页面加载后才从接口拉到前端渲染出来的。直接请求 HTML 只能拿到一个空壳子,关键词和部分静态描述还能看到,价格、销量这类核心字段大概率是空的或者占位符。
用 Selenium 就绕开了这个问题。它启动一个真实浏览器内核,页面里所有 JavaScript 正常执行,最终渲染出来的 DOM 树就是用户看到的样子,爬虫只需要在正确的时机去提取文本就行。这也是 Selenium 这类浏览器自动化工具在竞对监控、比价采集场景里经久不衰的根本原因——它能模拟最真实的用户访问路径,而不是伪造一个简单的 HTTP 请求。
2.2 无头浏览器的关键配置与反检测经验
Selenium 配置本身不难,难的是让它不被检测出来。直接用一个默认配置启动 Chrome,明文写着webdriver=true,很多平台识别到自动化特征后会直接弹验证码,或者返回一个空列表页。
我实际使用的核心配置分三层,每一层都有对应的意图:
第一层是启动参数。强制走无头模式(--headless=new),但记得保留 GPU 和沙箱相关设置,否则部分页面渲染会异常。设置窗口大小,因为有些页面布局会根据视口宽度改变 DOM 结构,固定成一个常规笔记本分辨率可以避免解析错位。加上--disable-blink-features=AutomationControlled,这是关闭自动化特征标记的关键参数。
第二层是浏览器指纹特征。Selenium 默认的 User-Agent 会带上HeadlessChrome字样,一眼假,需要手动设置一个常规 Chrome UA。同时固定 viewport 尺寸、时区、语言,形成一套前后一致的"身份"。我自己踩过的一个坑是:UA 改成了正常的,但时区没改,页面上的推荐算法立刻发现异常,导致滑动验证码出现频率明显上升。
第三层是行为模拟。所有请求之间加随机延时,延时范围控制在 2 到 5 秒之间,模仿真实用户浏览节奏。鼠标轨迹这个我试过,意义不大还拖慢速度,最后只做随机滚动页面。这里要特别克制的是请求频率:宁可采集速度慢一点,也不要让目标平台产生明显的压力。合规、长效地采集,远比一次性抓取来得有价值。
以下是经过实践验证的核心配置代码:
from selenium import webdriver from selenium.webdriver.chrome.options import Options import random import time def get_browser(): options = Options() options.add_argument('--headless=new') options.add_argument('--disable-gpu') options.add_argument('--no-sandbox') options.add_argument('--disable-blink-features=AutomationControlled') options.add_argument( 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/125.0.0.0 Safari/537.36' ) options.add_argument('--window-size=1920,1080') options.add_argument('--lang=zh-CN') options.add_experimental_option('excludeSwitches', ['enable-automation']) options.add_experimental_option('useAutomationExtension', False) driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})' }) return driver def random_delay(): time.sleep(random.uniform(2, 5))这段配置里最容易被忽略的是最后那个execute_cdp_cmd,它通过 Chrome DevTools 协议在页面加载前注入脚本,把 navigator.webdriver 属性改掉。很多反爬体系就是靠这一个字段区分真人用户和自动化工具的,实测加上它之后,验证码出现频率降了一个数量级。
2.3 每个平台一套解析器:多平台采集的架构设计
多平台采集最容易犯的错误,是想写一个"万能解析器"一次适配所有平台。实际情况是每个平台的 DOM 结构、类名命名、数据埋点完全不一样,甚至同一个平台的不同页面类型(搜索页、详情页、活动页)之间也没有复用性。
我最终采用的架构是按平台建包、按页面类型建类,每个平台一个独立解析模块:
spiders/ ├── base_spider.py # 公共基类:URL构建、请求、字段容器 ├── platform_a/ │ ├── search_parser.py # 搜索列表页解析器 │ └── detail_parser.py # 商品详情页解析器 ├── platform_b/ │ ├── search_parser.py │ └── detail_parser.py └── common/ ├── utils.py # 价格解析、时间格式化 └── constants.py # 各平台字段映射常量每个解析器的核心任务就是把页面 DOM 里的文本信息转成统一的字段结构。这里有一个很关键的工程细节:不要把解析逻辑和采集逻辑混在一起。采集逻辑(打开页面、滚动、等待)放在基类或者公共方法里,解析逻辑(提取标题、价格、销量)放在各平台的 Parser 类里。这样某个平台的页面改版了,只需要改对应平台的解析器,不会影响其他模块。
商品详情页解析时还要额外处理几个异常分支:价格被隐藏(部分平台对未登录用户隐藏价格)、促销标签导致价格文本结构变化、商品 SKU 有多个档位。处理方案是先定位主体价格区域,再通过区域内的结构化标签区分当前价、原价、促销价,拿不到完整 SKU 内容时,至少记录默认展示的档位价格并打上标识。
2.4 断点续采与增量更新:让采集任务跑起来不用管
采集任务一旦跑起来,中断几乎是必然的:网络超时、页面改版、反爬验证码弹出、数据库连接断开、服务器重启。如果每次断掉都要从头开始抓,那这套系统就没有实用价值了。
我的方案是建一张采集任务表,把采集单元从"一个平台"细化到"一个商品在某个平台的某个页面"。每次启动采集时,优先取状态为失败或者上一次运行时间过久的任务,实现断点续采。核心表结构如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增 | 任务主键 |
| platform | VARCHAR | 平台标识 |
| item_key | VARCHAR | 商品唯一标识 |
| page_url | VARCHAR | 待采集页面链接 |
| status | TINYINT | 0=待采集,1=成功,2=失败 |
| retry_count | INT | 失败重试次数 |
| last_attempt_at | DATETIME | 最后尝试时间 |
增量更新策略分两个层次。第一层是定时任务,我用系统自带的定时任务每天凌晨跑一轮全量更新,重点刷新价格和库存字段。第二层是变化检测,对历史价格记录做比对,当天价格与上次记录偏差超过阈值时才写入新的价格快照,避免每天都存一堆无意义的数据。
价格变化检测的代码如下:
def save_price_snapshot(cursor, product_id, platform, current_price): cursor.execute( 'SELECT price FROM price_history ' 'WHERE product_id = %s AND platform = %s ' 'ORDER BY record_time DESC LIMIT 1', (product_id, platform) ) row = cursor.fetchone() if not row or abs(row['price'] - current_price) > 0.01: cursor.execute( 'INSERT INTO price_history ' '(product_id, platform, price, record_time) ' 'VALUES (%s, %s, %s, NOW())', (product_id, platform, current_price) ) # 价格变化超过5%时记录一条提醒 if row and abs(row['price'] - current_price) / row['price'] > 0.05: insert_price_alert(product_id, row['price'], current_price)3. 价格数据和商品对齐:比价的真正难点不在"爬"而在"比"
3.1 数据清洗:让人看懂的数据才能让程序看懂
爬虫能抓到数据,但抓到的原始数据几乎都"脏"。不同平台的字段格式五花八门,不处理干净,后面的分析和展示全是错的。
第一个要解决的是价格字段。有些平台显示¥1,999.00,有些显示1999元起,还有一些直接把活动价和划线价渲染在同一个元素里。统一的做法是用正则提取数字和符号,再根据平台标识确定是否需要乘 10000(有些平台用分做单位),最后统一为 Decimal 类型存储。这个坑我在前期栽过跟头:默认字符串一路存到 MySQL,结果可视化阶段排序全部错乱,因为数据库按字典序排列。
第二个是标题清洗。电商标题最爱干的事就是堆砌促销前缀,比如官方旗舰店、赠品信息、物流说明。要对齐同款商品,这些噪声词必须去掉。我的处理方式不是简单替换,而是维护一个停用词表,做遍历过滤。这里不追求 NLP 级别的分词精度,规则过滤基本够用。
标题清洗代码片段:
import re STOP_WORDS = ['官方', '旗舰店', '赠品', '免运费', '顺丰包邮', '保税仓发货'] def clean_title(title): if not title: return '' for word in STOP_WORDS: title = title.replace(word, '') title = re.sub(r'【.*?】', '', title) title = re.sub(r'\[.*?\]', '', title) return re.sub(r'\s+', ' ', title).strip()3.2 同款识别:一条商品在三个平台长着三张脸
比价系统最核心的难题有两个:其一,同一个商品在不同平台的标题千差万别;其二,不同平台的店铺可能给商品起不同的名字。如果只按标题字符串相等匹配,那基本什么都匹配不上。
我采用的方案是分层的匹配策略,先粗后细逐级过滤:
第一步做品牌级预筛选。维护一份品牌别名词典,把常见的别名映射到标准名。比如某个品牌的游戏本系列,在平台 A 叫"天选",在平台 B 叫"TUF"。同义词映射后,通过正则把品牌和型号从清洗后的标题中抽出来,作为稳定键值。
第二步做配置级确认。笔记本、手机这类商品,型号还不算唯一标识,同一个型号可能对应不同 CPU、内存、显卡的配置组合,一般通过解析标题中的规格关键词来锁定。这一步的召回率做不到 100%,但能满足比价场景里 90% 以上的需求。剩下的边缘情况(比如只有部分平台标注完整规格),我选择保留一条记录并打上"规格待确认"标记,避免错误匹配。
第三步,如果前两步都失败了,再对清洗后的标题做分词,计算 Jaccard 相似度,设定阈值 0.7。这一步会引入少量误匹配,所以还要加一道人工校验手段:把模糊匹配结果挂到一个待确认列表页面,由用户在后台手动合并,合并结果会回写数据库,后续采集自动学习。
3.3 到手价与价格趋势:比价算法的计算口径
比价的本质不是比"标价",而是比"到手价"。不同平台的定价策略差异很大:有的平台标价虚高但优惠券力度大,有的平台可以跨店满减,有的平台直接显示折后价。如果爬虫只抓挂架价,系统报出来的"最低价"可能和用户最终付款金额差了十万八千里。
我在系统中定义了一个统一的"到手价"计算模型:
到手价 = 挂牌价 - 店铺券优惠 - 平台券优惠 + 运费问题在于怎样获取优惠券信息。有些平台的优惠券是页面加载后异步请求得到的,在 DOM 上不一定直接可见。我的处理策略是:优先解析详情页内已展示的优惠标签(例如满减、立减),取最大值作为预估优惠;没有展示优惠标签的商品,默认不估计,还是用挂牌价参与比价,但会打上"优惠未核算"的标签。这样系统给出的比价结果能保证下限,同时提醒用户部分数据可能存在偏差。
历史价格趋势是比价系统里最容易被低估的模块。用户买一件高价商品,往往不是要看今天谁便宜,而是想知道现在是价格高点还是低点。为此设计了价格历史表,每次采集都追加一条记录。趋势图展示近 30 天、90 天、180 天的价格曲线,同时标注出历史最低价和当前价相对低点的偏离比例。这部分在论文里的价值很高,因为它是纯采集数据展示层面之外,真正体现"数据分析"能力的地方。
4. Flask后端与可视化:把数据库里的数字变成一张图
4.1 API接口设计:给前端和Agent准备数据
系统后端采用 Flask 蓝图模式,所有业务都通过 RESTful API 暴露。好看的页面只是一个壳,核心要把接口设计得整洁、语义清晰,方便前端页面和后面的大模型 Agent 调用。我最终保留了四个核心接口,功能划分如下:
GET /api/search?keyword=笔记本&platform=all:符合关键词的商品列表,支持平台筛选和排序。GET /api/compare?id=1001:指定商品在各平台的比价结果,返回各平台价格、优惠信息、近 7 天最低价。GET /api/trend?id=1001&range=30:指定商品的历史价格趋势数据,返回按天的价格序列。GET /api/rank?category=computer&sort=price_desc:类目排行榜,供首页展示。
Flask 接口实现需要注意一个细节:数据库返回的 Decimal 类型不能直接做 jsonify 序列化,会报类型错误。我统一封装了一个序列化工具,把 Decimal 转成浮点字符串,同时把 datetime 格式化好。这个小坑很容易让第一次跑通后端的人卡个十分钟。
from decimal import Decimal from datetime import datetime import json class ApiEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Decimal): return float(obj) if isinstance(obj, datetime): return obj.strftime('%Y-%m-%d %H:%M:%S') return super().default(obj) app.json_encoder = ApiEncoder4.2 可视化组件选型:为什么选 ECharts
可视化方案方面,我对比过 ECharts 和 Plotly,最终确定 ECharts,原因是它在毕业设计场景里几乎是最优解。ECharts 由国内团队维护,中文文档完善,典型图表的官方示例非常丰富;折线图、柱状图、雷达图都只需要几十行配置;而且可以选用按需引入的 minimal 包,前端加载体积可以控制在合理范围内。Plotly 的交互能力更强,但图表样式偏重,而且每次渲染都会携带较大的 JS 文件,页面首屏速度不快。
可视化页面总共需要四个图表模块:
- 价格对比柱状图:同一商品在各平台的到手价横评,柱子颜色按平台区分。
- 历史趋势折线图:近 N 天价格走势,标出当前价和历史最低价。
- 平台价格分布散点图:某个类目下所有商品在不同平台的价格分布,一眼看出版本差价。
- 降价提醒列表:一个低成本实现,直接在页面上渲染变化率超过阈值的商品。
前端页面用的是原生 HTML + JavaScript + ECharts,通过 fetch 调用 Flask 接口拿到 JSON 后渲染。没有引入 Vue 或 React,因为系统本身的数据展示逻辑不复杂,引入前端框架反而让项目体积变大、答辩时解释起来更费劲。
4.3 查询性能与缓存优化
比价系统的数据量虽然不大,但趋势图查询这类高频接口,如果没有正确的索引和缓存策略,响应时间会肉眼可见地变长。一个常见现象是页面加载速度从 100 毫秒上升到 5 秒,原因是每次刷新都全表扫描价格历史表。
优化分两层。第一层是数据库索引优化,这是最关键的:price_history表的product_id加索引,product_id和record_time建联合索引,搜索接口的keyword字段在前端做分词后生成搜索条件,配合 MySQL 的全文索引或 LIKE 前缀查询。加了联合索引之后,趋势查询的响应时间从 800 毫秒降到了 20 毫秒以内,这个优化幅度非常明显。
第二层是缓存。我用了 Flask-Caching 的简单配置,给搜索结果和排行榜数据加了 5 分钟缓存。理由很简单:比价数据来自定时采集,不是实时产生,5 分钟内数据的准确率几乎没变化,但能让高并发访问时后端承受的压力大幅下降。生产部署时我也建议再加一层 Nginx 缓存静态资源,不过后端缓存已经足够支撑演示场景了。
from flask_caching import Cache cache = Cache(app, config={ 'CACHE_TYPE': 'simple', 'CACHE_DEFAULT_TIMEOUT': 300 }) @app.route('/api/rank') @cache.cached(timeout=300, query_string=True) def rank(): # 查询逻辑 ...5. DeepSeek大模型与Agent:从"看数据"到"问数据"
5.1 大模型能解决哪些传统查询解决不了的问题
可视化看板把数据摆在了用户面前,但用户面对一堆折线和柱状图,脑海里往往还是带着具体问题来的:"这台电脑最近价格怎么样?我要不要等一等再买?"传统的搜索接口只能做精确匹配,它无法理解"性价比高一点、预算六千以内、适合编程"这种带有模糊语义和多重条件的复杂问题。
大模型的价值在于把自然语言转成系统内的确定性操作,再把结构化的查询结果转成用户能直接理解的答案。这个"翻译"过程,正是 Agent 层的核心价值。传统接口解决"怎么查",大模型介入后解决的是"怎么问"——用户无需学习任何查询语法,用自己的话就能获得和人工分析几乎一致的结论。
我在实际测试中试过一句典型问题:"这个牌子的游戏本,四个平台哪家便宜?未来一个月适合入手吗?"这个问题放在传统搜索框里,基本无法处理。但经过 Agent 拆解,它被路由成两个工具调用:一个查当前各平台到手价,一个查近 90 天价格趋势,然后把结果交给大模型撰写结论。
5.2 Agent工作流与工具调用的设计
Agent 的设计核心是"能调用工具,而不是直接生成答案"。这样既能保证回答有实时数据支撑,也能避免大模型编造不存在的价格信息。我采用的模式是经典的工具调用(Function Calling)方式,整体工作流如下:
- 用户输入自然语言问题,发送到 DeepSeek API,附带一份可用工具列表的说明。
- 模型判断需要调用哪些工具,返回结构化的工具调用参数(例如调用
search_product,参数是关键词和平台)。 - 后端拿到参数后,实际执行查询代码,从 MySQL 取出真实数据。
- 把查询结果作为上下文,连同用户的原问题一起再次提交给大模型。
- 模型综合上下文生成自然语言回答,附上数据来源和时间戳。
这个流程里最关键的设计点:大模型永远看不到真正的数据库,它只能通过"工具"间接获取数据,因此不存在幻觉污染数据库的风险。同时每一次回答都附带了数据生成时间,用户能判断信息的时效性。
5.3 集成DeepSeek的具体步骤与提示词要点
接入 DeepSeek 比想象中简单,因为它的 API 兼容当下主流的接口格式,直接用 HTTP 请求就能跑通。在这里我用了一个比较直观的示例来展示工具调用的完整流程:
import json from openai import OpenAI client = OpenAI( api_key="你的APIKey", base_url="https://api.deepseek.com/v1" ) tools = [ { "type": "function", "function": { "name": "search_product", "description": "根据商品关键词搜索平台在售商品", "parameters": { "type": "object", "properties": { "keyword": {"type": "string", "description": "商品关键词"}, "platform": {"type": "string", "description": "平台标识"} }, "required": ["keyword"] } } } ] messages = [{"role": "user", "content": "华硕天选5在各平台多少钱"}] response = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=tools, tool_choice="auto" ) # 解析模型返回的工具调用 tool_calls = response.choices[0].message.tool_calls if tool_calls: for call in tool_calls: arguments = json.loads(call.function.arguments) print(arguments) # 拿到参数后去数据库查询提示词设计是这套系统里最值得打磨的地方。我给系统的系统提示词(System Prompt)定了三条铁律:第一,所有价格数据必须引用查询结果,没有查到就明确说"未找到",绝不猜测;第二,回答要分点列出各平台对比,最后给一个明确的建议;第三,涉及趋势说明时,必须引用趋势数据中的最低点和最高点日期,让用户自己校验。
另一个容易踩坑的地方是 token 控制。工具调用阶段的输入包含工具定义和对话历史,长度一变长,成本就上去了。我的对策是只保留最近两轮对话历史,工具定义只保留当前会话用到的几个,这样单次请求的 token 消耗可以压在一个很低的水平。
5.4 一个小场景演示:从提问到回答的完整链路
用一个真实问题来把链路串一遍。假设用户输入:"我想在 618 买一台轻薄本,预算 5500 左右,主要用于写代码,偶尔打打游戏,推荐哪款?什么平台买便宜?"
系统处理过程如下:
- 大模型判断这个问题应该调用工具
search_product,参数为关键词"轻薄本"、平台all。 - 后端查询数据库,返回符合条件的所有商品和各平台到手价、近 30 天价格趋势。
- 大模型将这批数据整理成表格要点和文字建议,最终输出类似这样的一段话:"在您预算区间内,推荐关注 A 平台的某型号,比 B 平台低 320 元;该型号近 30 天最低价出现在 5 天前,目前价格处于中位水平,若不急可以等 618 当天的活动价,历史规律显示大促首日降幅通常在 8% 左右。"
这个回答结合了实时数据、历史趋势和常识规律,已经超过了一堆图表能提供的价值。实际上,我在演示环节最常收到的反馈就是"这个功能比光看图直观多了"——这其实就是 Agent 层最大的意义。
6. 项目后记:哪些地方值得重做,哪些坑不必再踩
6.1 高频踩坑清单
做完整套系统,我把自己踩过、也看到周围同学踩过的坑整理成了一张表,按触及频率排序。这些坑非常真实,几乎每个人都会遇到:
| 问题现象 | 根因 | 解决思路 |
|---|---|---|
| 爬虫跑一段时间后价格抓不到 | 页面请求被风控拦截,或前端价格结构变更 | 检查反爬特征设置,动态调整解析策略 |
| 数据库 Decimal 字段写入报错 | 爬虫返回的价格字符串未清洗,含逗号和货币符号 | 统一价格解析工具,入库前强制类型转换 |
| 前端图表数据为空 | Flask 返回的 JSON 缺失字段,或者字段类型不对 | 统一使用 ApiEncoder,前后端字段名保持一致 |
| 定时采集任务莫名中断 | 服务器休眠、内存不足导致 Chrome 进程被杀 | 使用稳定运行环境,定期重启浏览器实例 |
| DeepSeek 回答中价格与图表不一致 | 模型未走工具调用,直接生成了常识性回答 | 在提示词中强制要求引用搜索结果,忽略直接生成 |
其中第一个坑是最容易反复出现的。平台页面每隔几个月会改版一次,但在毕业设计的时间尺度里通常只会在初采和答辩前各遇到一次。我的经验是:把解析器的容错能力做强,当某平台连续失败超过 5 次,自动禁用该平台并给运维页面发标记,而不是让整个采集流程卡死。
6.2 给后来者的三条建议
项目收尾之后回头看,有三条建议我觉得无论对毕业设计还是个人练手项目都适用。
第一,爬虫部分不要过度追求数据量。很多同学一上来就想抓几十万条数据,结果把时间全耗在处理反爬、绕过限制上。对毕设来说,抓取几千条覆盖多个类目的干净数据,足以支撑全部功能演示。数据质量远比数据量重要,宁可花时间打磨清洗和比价逻辑,也不要陷入违规采集的泥潭。
第二,一定要保留历史价格数据。答辩时展示一个折线趋势图和展示一张静态表格,效果天差地别。趋势图能证明系统在持续运行、持续采集,能讲出"数据积累"的故事。从项目第一天开始就让定时采集跑起来,哪怕一开始只监控 20 个商品,积累三周后图表就会很饱满。
第三,大模型 Agent 是低成本亮点。很多比价系统都停在图表展示这一步,只要你接入了 DeepSeek 做自然语言问答,就天然多了一个有区分度的功能点。而且现在各大模型的 API 文档都很清晰,照着官方示例跑通一个工具调用只用半天时间,投入产出比非常高。
6.3 系统还能怎么扩展
这套比价系统目前的边界很清楚:采集频率受限于平台风控,比价范围限于已入库平台的商品,Agent 能力只覆盖了工具调用这一种大模型集成方式。如果想继续扩展,有三个方向可以做:
一是增加价格预测模块。基于历史趋势数据,用简单的时间序列模型预测未来一周的价格区间,让"什么时候买"这个问题从经验判断变成数据驱动。二是引入用户订阅机制,允许用户关注商品并设置目标价,触发降价提醒通过页面通知推送。三是把 Agent 升级成多轮对话,让用户能连续追问,比如先问"推荐一款手机",再问"那它能玩原神吗",系统需要维护多轮对话状态并持续调用工具。这些都是可以逐步迭代的方向,核心的框架已经打好了。
我自己的体会是,这套系统最有价值的地方不是某个单点功能,而是把前端、后端、爬虫、数据库、大模型这五个环节完整串了起来。做完它,你对一个真实业务系统从数据采集到智能交互的整个链路会有真正的体感,这是任何单一技术栈练习都给不了的。