每年毕业设计季,总有读者来问:Python方向选什么题才不吃亏?我的答案一直很明确——电商数据采集分析与销量预测系统。这个方向一个人能包揽爬虫、数据清洗、机器学习建模和Web可视化四件事,用到的技术栈也够全,Flask、Selenium、机器学习模型一个不落,做完等于把Python从入门到实战的路重新走了一遍。今天就把这套系统的完整实现思路拆给你,从环境准备到模型上线,每一步该干什么、为什么要这么干,都讲清楚,正在选题目或者已经开题的朋友可以直接参考。
1. 项目整体定位与技术选型思路
1.1 这个选题为什么值得做
电商数据是互联网上最好获取、也最有分析价值的数据之一。用户在平台上的搜索、点击、加购、成交行为,最终都会落到商品的销量、价格、评价这些看得见的字段上。把这些字段抓下来,清洗整理成结构化数据,再通过机器学习算法挖掘规律、预测未来销量,本身就是一条非常完整的商业分析链路。
从毕业设计评审的角度看,这个题目有一个天然优势:复杂度够,但不是"高不可攀"的复杂度。采集层考察你的爬虫功底,分析层考察数据处理能力,预测层考察机器学习建模水平,展示层考察Web开发基础——五个方向全串起来了,但每一环门槛都不算离谱,认真做完就能讲清楚整个系统的逻辑。而且数据是真实存在的,拿出来的截图、图表都有说服力,答辩的时候也不会显得空洞。
从实用角度看,这套系统的思路可以直接迁移到很多真实场景。比如中小卖家想知道下一周哪个品类可能爆,运营人员想判断调价后销量会不会跌,供应链想提前备货——本质都是基于历史销量数据做预测。做完这个项目,简历上能写的项目经历也会更扎实。
1.2 技术栈逐项拆解
这套系统的技术选型,我比较推荐下面的组合,每一环都有自己的理由。
| 模块 | 选型 | 理由 |
|---|---|---|
| 语言 | Python 3.8+ | 生态最全,爬虫、数据分析、机器学习、Web开发一站式解决 |
| 数据采集 | Selenium + requests | 用Selenium处理动态渲染页面,requests处理简单的JSON接口 |
| 数据持久化 | MySQL(或SQLite) | 结构化数据存储,方便后续查询和特征统计 |
| 数据分析 | pandas + NumPy | 清洗、去重、聚合、特征工程都靠它们 |
| 机器学习 | scikit-learn + XGBoost | 建立基线模型和进阶回归模型,快速验证特征有效性 |
| 深度学习 | TensorFlow / PyTorch(LSTM) | 处理时间序列销量数据,捕捉长期依赖关系 |
| Web框架 | Flask | 轻量、灵活,写几个路由就能把结果展示出来 |
| 前端可视化 | ECharts + HTML/CSS/JS | 图表交互能力强,折线图、柱状图、散点图都很好用 |
选Flask而不选Django,是因为这个项目的展示端没那么重,不需要Django自带的Admin后台、ORM这些重量级组件。Flask的灵活性更高,写几个路由渲染模板就够了,而且部署也简单。机器学习部分我用scikit-learn先跑通线性回归和XGBoost,再上LSTM对比效果,这样论文里既有对比实验,又能体现深度学习的能力。
1.3 系统架构与数据流转
整个系统的数据流是单向的:采集层从电商平台抓取商品基础信息和销量数据,经过清洗后存入数据库,分析层从库里读数据做特征工程,再把构造好的特征喂给预测模型,最终预测结果由Flask渲染到可视化页面。各个环节解耦做得越干净,后面调试越省心。
我实际开发时把项目分成了五个目录:spider管采集,preprocess管清洗,features管特征工程,model管训练和预测,webapp管Flask展示。每个模块之间通过数据库或文件传递数据,不互相调用内部函数。这样改爬虫的时候不动模型代码,换模型的时候也不影响页面展示。毕业设计答辩被问"模块怎么设计的",这样回答也最稳。
2. 数据采集层:Selenium搞定动态页面
2.1 为什么不用Requests而选Selenium
很多教程教爬虫上来就是requests + BeautifulSoup,但电商类页面现在基本都不走单纯的服务端渲染了。你打开一个商品列表页,能看到几十上百个商品,但源码里可能只有几个空的容器标签——真实数据是页面加载后用JavaScript异步请求接口再填进去的。拿requests去抓静态源码,什么都得不到。
Selenium的思路就直白多了:它直接驱动一个真实的浏览器(像Chrome),让浏览器自己跑完所有JS,等页面渲染完成再取数据,你在浏览器里能看到什么,就能抓到什么。打个比方,requests像是站在门口按门铃问里面有没有人,Selenium是直接进门翻抽屉看里面到底放了什么。动态加载这个问题在Selenium面前不存在。
当然Selenium也有缺点:慢,非常慢,因为它要真正启动一个浏览器进程。抓几百个商品页面可能要跑十几分钟。所以我在项目里做了个妥协——只有那些明显是动态渲染的页面才用Selenium,能直接找到JSON接口的(比如某些异步接口直接返回结构化数据),就用requests去调,速度快很多。这个取舍很实用,想省时间的同学可以重点参考。
2.2 采集器的核心实现流程
写Selenium采集器,核心步骤可以拆成五步:启动浏览器、打开目标页面、等待内容加载、定位并提取数据、翻页循环。每一步都有坑,我一个个说。
启动浏览器这一步,最常见的问题是Chrome版本和驱动版本不匹配。建议直接用WebDriver Manager自动匹配驱动版本,几行代码就能解决。固定写法大概是:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager option = webdriver.ChromeOptions() option.add_argument("--window-size=1920,1080") option.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=option)拿到driver之后,打开商品列表页,第一件要做的就是等元素出现。电商页面的商品卡片通常是异步渲染的,页面没加载完就去找元素,十有八九报NoSuchElementException。用显式等待是最稳的:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 10) cards = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.product-card")))定位到商品卡片之后,就可以在卡片内部继续找标题、价格、销量这些字段。这里有个经验:能用CSS选择器就不要用XPath,CSS选择器更简洁,解析速度也更快。提取数据时注意先获取页面源码里的文本,再用正则或字符串处理清掉多余字符,比如销量字段经常是"已售300+"这种格式,要自己统一规范。
翻页这个环节,电商平台常用的翻页方式有两种:一种是点击"下一页"按钮,另一种是下拉滚动加载更多。点击按钮就定位下一页元素再click,滚动加载就循环执行一段JavaScript滚动到页面底部:
for i in range(5): driver.execute_script("window.scrollTo(0, document.body.scrollHeight)") time.sleep(2)滚动间隔要控制好,等数据加载完再滚下一轮,不然滚动太快会出现空白。我实测下来,每次滚动后等2到3秒比较合适。
2.3 反爬应对与断点续爬
电商平台对爬虫的识别手段这几年升级了不少,但毕业设计场景下做到"降低被识别概率"就够了,不需要也没必要去搞绕过。我常用的几个措施,第一个是设置合理的User-Agent,第二个是每次请求之间加随机延时,第三个是避免浏览器特征暴露。
随机延时这个细节容易被忽略,但非常重要。如果你每个页面都精确间隔2秒,反而像一个定时任务,更容易被识别。我用的是随机区间:
import random import time time.sleep(random.uniform(1.5, 4.0))另外,Selenium启动的浏览器在JS环境里会有webdriver标记,大厂的反爬系统能通过这个标记直接识别出自动化工具。处理方式是在启动参数里加一行disable-blink-features=AutomationControlled,实测下来能有效隐藏大部分特征。
断点续爬是我强烈建议做的。采集几百个页面跑十几分钟,中间网络抖动或者浏览器崩溃是常有的事。我的做法很简单:每抓完一页就把当前页面的URL存到一个progress.txt文件里,重新启动采集器时先读这个文件,从上次断掉的位置继续跑,而不是从头再来。这个设计投入很小,但稳定性提升巨大,一旦遇到崩溃不用一次次重跑全量。
3. 数据清洗、特征工程与销量预测模型
3.1 清洗脏数据:去重、补缺失、造特征
采集下来的原始数据一定不能直接丢给模型,先过一遍清洗。最典型的几个问题:同一个商品在多天采集中被重复抓取,价格字段可能带"¥"符号,销量字段可能带"万"单位,评价数可能为空。不去处理这些脏数据,后面训练出来的模型就是"垃圾进、垃圾出"。
去重我用的是pandas.DataFrame.drop_duplicates(),以商品ID为主键,保留最新一条记录。对于那些评价数缺失的行,我先看缺失比例,如果低于5%,直接用中位数填充;如果比例高,就放弃这个字段,换成其他特征。价格统一的逻辑是先把字符串里的"¥"和"%s"去掉,再批量转成float,同时把"万"换算成数字乘以10000。
特征工程这一步,是整个预测系统里我觉得最出效果的地方。除了直接用价格、评论数这几个原始字段,我额外构造了几个特征,比如价格区间分箱(把商品按价格分成低、中、高三档)、评论数对数(评论数分布太偏,取log后更接近正态分布)、上架天数(用采集日期减去上架日期计算)等。这些特征对销量预测的增益非常明显。
销量预测本质是时间序列问题,所以我还构造了滞回特征:把前7天(或前3天)的销量作为当前特征。这个做法能把"上周卖得好这周通常也不差"这个直觉编码进模型,XGBoost用上之后效果提升很明显。
3.2 机器学习基线模型:从线性回归到XGBoost
模型选型我建议从简单到复杂一步步来,不要直接上深度学习。先用线性回归跑通全流程,看效果,再换更强的模型。这么做有两个好处:一是快速验证整个数据管道有没有问题,二是论文里能写出"从基线到进阶"的对比实验,答辩更有说服力。
线性回归实现起来最简单:
from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False) model = LinearRegression() model.fit(X_train, y_train)注意时间序列数据不能随机打乱,所以shuffle=False这个参数很关键。
下一步换成XGBoost,它能自动学习特征之间的非线性关系,对缺失值也有一定容忍度,在工业界表格数据上一直是性价比最高的选择之一。调参上我会重点关注n_estimators、max_depth和learning_rate这三个,先固定一个粗范围,再用GridSearchCV做小范围搜索。实际项目里XGBoost的效果通常能比线性回归高出20%以上的误差降低,这也是为什么它在销量预测场景这么受欢迎。
3.3 深度学习方案:LSTM预测销量
深度学习部分我选了LSTM,没有选Transformer一类更复杂的结构,原因是这套系统的数据规模是"中小型"的,几千到几万条记录,LSTM完全够用,而且LSTM在销售额这类连续时间序列上的表现非常稳定,训练时间也能接受。
LSTM要求输入是一个三维张量,形状是(样本数, 时间步长, 特征数),所以数据要重新组织成滑动窗口的形式。比如用过去7天的销量预测第8天,那每个样本就是一个7 x 特征数的矩阵。窗口大小为7是经验值,销量通常存在明显的周周期规律,周一到周日不同,7天窗口恰好能覆盖一个完整周期。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense model = Sequential() model.add(LSTM(64, activation="relu", input_shape=(window_size, n_features))) model.add(Dense(1)) model.compile(optimizer="adam", loss="mse")训练时我加了EarlyStopping,监控验证集的loss,连续10个epoch没下降就停止,避免过拟合。数据集按时间顺序切成70%训练、15%验证、15%测试,绝对不洗牌。
3.4 模型评估:别只盯着准确率
很多初学的同学一上来就问"准确率多少",但销量预测是回归任务,不是分类任务,准确率这个指标根本不适用。回归任务要看的是均方根误差(RMSE)、平均绝对误差(MAE)和拟合优度(R平方)。RMSE对大误差更敏感,预测爆炸的情况会被它放大;MAE更直观,就是平均差多少。
我最后交出来的报告中一定要带一张预测值对真实值的散点图:越贴合对角线越好。如果点都挤在一个方向偏出,说明模型有系统性偏差,需要检查特征里是不是漏了关键信息。这种情况下先不要急着调模型,回去看看数据有没有问题。
4. Flask可视化系统:从数据到图表
4.1 Flask项目结构与路由规划
到这一步,数据有了、模型也训练好了,接下来要让系统"看得见"。我用Flask搭了一个轻量的可视化站点,整体结构如下:
webapp/ ├── app.py ├── templates/ │ ├── index.html │ ├── analysis.html │ └── predict.html ├── static/ │ ├── css/ │ └── js/ ├── models/ │ ├── xgboost_model.pkl │ └── lstm_model.h5 └── data/ ├── processed/ └── predictions/路由规划上,我设计了三个主要页面:首页展示系统概览,数据分析页展示清洗后的统计图表,销量预测页展示预测结果。每个页面对应的路由很简单:
@app.route("/") def index(): return render_template("index.html") @app.route("/analysis") def analysis(): return render_template("analysis.html")最关键的是数据接口路由,页面图表的数据都通过接口请求拿到,前端不直接读取数据库。我推荐用jsonify返回JSON数据,ECharts接收起来非常方便。比如销量Top10榜单的接口:
@app.route("/api/sales_top10") def sales_top10(): df = load_processed_data() top10 = df.nlargest(10, "sales")[["name", "sales"]] return jsonify({"names": top10["name"].tolist(), "sales": top10["sales"].tolist()})4.2 ECharts可视化集成实战
ECharts是目前前端图表库裏综合体验最好的之一,中英文文档齐全,配置项也不算复杂。我在项目里用了三种常用的图表类型:折线图展示每日销量趋势、柱状图展示销量Top10商品、散点图展示价格与销量的关系。
引用ECharts最简单的方式是直接拿官方CDN链接,在HTML里引入echarts.min.js,然后准备一个带ID的div容器,在<script>里初始化图表并setOption。以折线图为例:
var chart = echarts.init(document.getElementById("trendChart")); chart.setOption({ xAxis: { type: "category", data: dates }, yAxis: { type: "value" }, series: [{ type: "line", data: sales, smooth: true }] });要注意的一点是,图表容器的div必须有明确的宽度和高度,不然ECharts渲染不出任何东西。我踩过这个坑,起初图表一直白屏,查了半天发现是容器高度为0。
4.3 预测接口对接与前端交互
销量预测页面是整套系统最有演示效果的部分。用户在页面上选择一个商品类别,系统返回未来7天的预测销量,前端画成折线图展示预测趋势。这个交互逻辑是:前端通过fetch请求后端预测接口,带上商品ID和预测天数参数,后端调用已保存的模型进行预测,返回JSON数据。
@app.route("/api/predict", methods=["POST"]) def predict(): payload = request.get_json() product_id = payload.get("product_id") days = payload.get("days", 7) forecast = run_lstm_prediction(product_id, days) return jsonify({"product_id": product_id, "forecast": forecast})模型加载这里有个很重要的性能问题:如果每次请求都重新加载一次模型文件,响应会非常慢,用户一点预测按钮就要等好几秒。我是在Flask应用启动时就加载模型,存到全局变量里,后面请求直接复用,响应时间能降到毫秒级。LSTM模型加载需要的时间更长,放到启动时加载效果更明显。
5. 实操中踩过的坑与排查技巧
5.1 Selenium采集中最常踩的坑
第一个坑是元素定位超时。页面有时候加载慢,有时候因为网络原因卡住,等到10秒还没等到元素就抛异常了。我的解决办法是写一个重试装饰器,定位失败后重试3次,每次间隔2秒,大幅减少偶发失败导致的崩溃。
第二个坑是无头模式被识别。我一开始为了省资源用了headless模式,结果发现采集到的商品数量远少于预期,很多页面返回的是验证码页面。检查之后发现无头模式更容易暴露浏览器自动化的特征。方案是改成有头模式,把窗口开到1920x1080,同时在后台最小化运行,不占用太多视野,采集成功率明显提升。
第三个坑是driver没有及时关闭导致内存泄漏。采集几千个页面后,Chrome进程会越堆越多,内存占用轻松超过2GB。我最后在采集器主循环外面用try...finally包裹,确保每次任务结束都调用driver.quit(),同时在每抓完50页后主动重启一次浏览器,内存就稳定住了。
5.2 模型训练与预测的坑
预测结果特别差时,先排查数据问题再调参,这是我自己定的原则。常见的数据问题有三个:训练集和测试集数据分布差异大(可能因为测试集恰好包含了促销时段的数据,异常值拉高了误差);特征里混入了未来信息(比如不小心把"当天实际销量"当特征又预测当天销量,属于典型的泄露问题);特征没有归一化导致梯度爆炸。
时间序列的交叉验证和分类任务完全不一样,不能用普通的KFold。我改用TimeSeriesSplit,保证每次训练集都严格在测试集前面,这样验证结果才真实。XGBoost的模型文件我在保存时用了joblib而不是pickle,就是因为joblib对大数组的处理效率更高,加载也更快。
5.3 Flask部署与模型加载的注意事项
Flask应用在本地跑起来很简单,但最终给老师演示的时候,我建议跑在局域网里,让老师直接通过IP访问,演示效果会比本地开浏览器好得多。启动时加一句app.run(host="0.0.0.0", port=5000)就行,注意关闭调试模式。
模型文件的路径问题最容易忽略。本地开发时的相对路径,换一台电脑就很容易找不到文件。我的做法是动态获取__file__的目录再拼接绝对路径,这样项目拷到任何机器上都不会出路径问题:
import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MODEL_PATH = os.path.join(BASE_DIR, "models", "lstm_model.h5")另外,LSTM这类深度学习模型对底层库版本很敏感,如果你在训练机器上用TensorFlow 2.10保存的模型,在演示机器上装的是2.15,可能加载会报错或者结果有差异。我在交付项目时直接额外提供了一个环境配置文件,列出了所有包的精确版本号,从根源上避免了这种问题。
写在最后
这个系统做下来,我最深的体会是:毕业设计项目拼的不是单个技术有多深,而是把完整链路走通的能力。从采集到展示,每一环都踩了一遍实际工程里的坑——元素等待超时、数据丢字段、模型过拟合、图表白屏——这些经验比任何教程都值钱。如果你也打算做类似的项目,我建议先不用急着上深度学习或过多的功能,第一步把采集和清洗跑通,用线性回归出一版预测图和可视化报表,再逐步加XGBoost、加LSTM、加交互功能。每加一层都要能跑、能看、能解释,这样的项目想不被认可都难。