首页 / 资讯中心 / 文章详情

LSTM+Python实战:从爬虫到Flask展示的蔬菜价格预测完整方案

LSTM+Python实战:从爬虫到Flask展示的蔬菜价格预测完整方案 ★ FEATURED ARTICLE
简介一套基于深度学习LSTM的蔬菜价格预测完整实现方案面向具备Python基础、希望实战时序预测与Web展示的开发者、研究生及课程设计者。项目串起数据采集、清洗、建模、对比、可视化到部署的完整链路覆盖scrapy爬虫抓取菜价与天气、pandas/numpy预处理、statsmodels/PyFlux/sklearn多种时序模型对比、Flask页面展示及微信公众号查询。资源共181个文件以142个csv菜价数据集、25个py源码、pyc字节码、docx项目说明及md笔记为主压缩包约1.78MB目录按“爬虫-数据处理-预测-web”划分便于逐步对照学习。已有251人学习下载。通过源码可复现从数据抓取到价格预测结果展示的全过程项目说明文档逐阶段拆解实现思路帮助快速迁移到其他农产品或时序预测场景是课程设计、毕业设计与LSTM入门的实用参考。1. 蔬菜价格预测不是玄学LSTM Python 把历史数据变成明天的参考价做生鲜电商采购或者菜市场批发的人最头疼的问题就是明天菜价怎么定。蔬菜价格受天气、节气、供需影响波动大且频繁靠经验估往往翻车。这个项目给的是一整套可落地的方案用 Python 爬虫把蔬菜价格和天气数据抓下来pandas 清洗成规整的时间序列再交给深度学习 LSTM 模型做预测最后通过 Flask 网页和微信公众号把预测结果展示给用户。资源里已经包含了 9 份 CSV 本地蔬菜价格数据集分别是本地菜心、云南小瓜、小塘白菜、本地芹菜、矮脚白菜、青皮冬瓜、西红柿、红尖椒等常见品类还有完整的 python 源码和项目说明文档。适合有 Python 基础、想接触时间序列预测或者需要完整「爬虫预测Web 展示」参考项目的人新手也能照着代码一步步跑通。2. 数据底座从爬虫到清洗菜价数据怎么变成 LSTM 能吃的样本2.1 Scrapy BeautifulSoup 抓取行情robots 协议、下载延迟和字段设计项目的第一段是爬虫。蔬菜价格数据通常分布在各个农产品批发市场的行情网站里页面结构不算复杂但有个共同点价格数字往往嵌在表格标签里且不同网站的表头命名不一样。项目用的是 scrapy 做框架beautifulsoup 做页面解析这是爬虫场景里最稳的组合——scrapy 管并发和调度beautifulsoup 负责把 HTML 里那td标签里藏的价格数字抠出来。一个典型的 scrapy spider 长这样import scrapy from bs4 import BeautifulSoup from veggie_predict.items import PriceItem class MarketSpider(scrapy.Spider): name market_price start_urls [https://example-market.com/price_list] # 替换为实际行情页 custom_settings { DOWNLOAD_DELAY: 1.5, # 每次请求间隔1.5秒防止被封IP ROBOTSTXT_OBEY: True, # 遵守robots.txt正规爬虫的底线 CONCURRENT_REQUESTS: 1, # 单并发慢但安全 } def parse(self, response): soup BeautifulSoup(response.text, html.parser) rows soup.select(table.price-table tr) # 定位价格表的每一行 for row in rows[1:]: # 跳过表头 cols row.find_all(td) if len(cols) 4: continue item PriceItem() item[name] cols[0].text.strip() # 蔬菜名称如本地菜心 item[price] float(cols[1].text) # 单价单位元/斤 item[date] cols[2].text.strip() # 日期格式YYYY-MM-DD item[source] market_a # 数据来源标记后续清洗用 yield item这里有两个参数值得细说。DOWNLOAD_DELAY设成 1.5 秒看起来慢但蔬菜行情网站基本都是小型站点服务器扛不住高并发一旦触发反爬整个任务就废了。ROBOTSTXT_OBEY我一般习惯开着正规场景下没必要跟网站对着干。另外字段里我加了source标记因为后续如果同时爬多个市场的价格不同来源的同一蔬菜价格可能差异很大清洗阶段需要按来源做去重或加权。2.2 pandas 清洗缺失值、休市日、异常价格的三种处理爬下来的数据不能直接用。蔬菜价格序列有两个天然脏点一是批发市场有休市日通常是周日或者法定节假日那几天的价格不存在二是网站偶尔会把「元/公斤」和「元/斤」混着标同一个菜心价格出现 6.5 和 13.0 两个版本。清洗的核心就两件事——补缺失、去异常。项目里的 CSV 数据已经做了初步清洗但你自己的数据还得走一遍。我一般习惯用 pandas 做这三步import pandas as pd import numpy as np # 读取爬虫落库的价格数据 df pd.read_csv(本地菜心.csv, parse_dates[date]) df df.sort_values(date) # 时间序列必须按时间排序 # 1. 缺失日期补全生成完整日期范围缺失价格用前向填充 full_range pd.date_range(df[date].min(), df[date].max(), freqD) df df.set_index(date).reindex(full_range).rename_axis(date).reset_index() df[price] df[price].ffill() # 休市日的价格沿用前一天 # 2. 异常值处理用3倍标准差过滤离谱价格 mean_price df[price].mean() std_price df[price].std() df[price] df[price].mask( (df[price] mean_price - 3 * std_price) | (df[price] mean_price 3 * std_price), np.nan ) df[price] df[price].interpolate(methodlinear) # 异常值用线性插值替代 # 3. 单位统一如果存在元/公斤的数据除以2转成元/斤 df[price] np.where(df[unit] 元/公斤, df[price] / 2, df[price])reindex这一步是很多人会漏掉的。pandas 的 DataFrame 不会自动帮你补齐不存在的日期如果直接拿去训练 LSTM序列是断的模型学到的全是断点处的错误规律。休市日价格用ffill()前向填充是行业惯例——前一天的价格是最合理的「无交易」近似值。而异常值用 3 倍标准差做掩码再用线性插值填补是为了避免直接把异常值砍掉后留下空洞。2.3 数据集快速体检九份 CSV 的共同结构和时间跨度资源里附带的 CSV 文件每一份都对应一个蔬菜品类。文件名就是蔬菜名比如「本地菜心.csv」「红尖椒.csv」「青皮冬瓜.csv」等。这些文件的列结构基本统一我拆开看过的标准格式是这样的列名类型示例说明datedatetime2023-06-01交易日期日粒度pricefloat3.85当日均价单位元/斤marketstr本地市场数据来源市场名称categorystr叶菜类品类分组可选用于后续分组预测拿到数据先别急着跑模型做一次「时间跨度体检」很重要。用一行代码就能看每份 CSV 的覆盖范围import pandas as pd for f in [本地菜心.csv, 红尖椒.csv, 青皮冬瓜.csv]: df pd.read_csv(f, parse_dates[date]) print(f, df[date].min(), →, df[date].max(), 共, len(df), 条)这个动作帮你快速判断两件事一是数据量够不够——LSTM 做日粒度预测至少需要几百条数据才有意义二是时间窗口是否覆盖完整的季节周期——如果只爬了三个月数据模型根本学不到菜价的季节性规律。项目里的 CSV 是按日粒度采集的覆盖了完整的交易周期但具体每条数据有多少行你打开文件看一眼就知道了。如果发现某个品类只有几十条记录我建议直接放弃对这个品类做 LSTM 预测改用简单的移动平均反而更靠谱。3. LSTM 预测核心序列窗口、归一化与模型结构3.1 为什么是 LSTM菜价序列的三个特征决定了模型选型菜价时间序列有三个明显的特征一是非线性台风一来菜心价格一天翻倍隔几天又跌回去ARIMA 这种线性模型对这种跳跃响应很迟钝二是存在趋势和季节性夏季叶菜便宜、冬季贵这是周期性的三是受短期脉冲影响大节假日、暴雨、寒潮都会造成价格尖峰。LSTM 的优势在于它的门控机制——遗忘门、输入门、输出门。遗忘门决定模型记住多长的历史依赖这对菜价序列特别重要一个月的价格走势会影响今天的价格但昨天的一场大雨可能让今天价格突变。标准 RNN 处理这种「长期依赖 短期突变」的组合会丢信息LSTM 的细胞状态相当于一个可读写的记忆单元能把一个月前的价格水平存住同时把昨天的异常波动作为新的输入状态写入。这就是项目选 LSTM 而不是普通 RNN 或 ARIMA 的理由。3.2 窗口序列构建look_back 参数决定模型能看到多远的历史LSTM 的输入不是单条数据而是一段连续的历史窗口。look_back这个参数直接决定模型「回头看多远」窗口设小了模型看不到周期规律设大了数据量会被稀释训练效率下降。我一般会按业务逻辑来定蔬菜价格的周期以周为单位所以 look_back 先取 7即用过去 7 天的价格预测明天。序列构建的标准写法import numpy as np def create_sequences(data, look_back7): 将一维价格序列转成监督学习的X, y格式 data: 归一化后的一维数组 look_back: 窗口长度默认7天 返回: X shape (样本数, look_back, 1)y shape (样本数, 1) X, y [], [] for i in range(len(data) - look_back): X.append(data[i:i look_back]) y.append(data[i look_back]) return np.array(X), np.array(y) # 使用示例假设 price_data 是已经归一化的一维数组 X, y create_sequences(price_data, look_back7) print(fX shape: {X.shape}, y shape: {y.shape})注意X的最终形状是(样本数, look_back, 1)第三维是特征数。这里只有价格一个特征所以是 1。如果你后续把天气温度、降雨量也拼进来第三维就要改成对应的特征数。look_back的取值我建议做一轮网格搜索——7 天是基准10 天和 14 天各跑一遍看 validation loss 哪个低用哪个。别一上来就堆 30 天窗口数据量不够的时候窗口越长训练越慢效果反而更差。3.3 归一化陷阱MinMaxScaler 只能 fit 训练集不能碰测试集LSTM 用 sigmoid 和 tanh 做激活函数输入数据不归一化的话大数值会直接把梯度压死。但归一化有个特别隐蔽的坑MinMaxScaler如果用全量数据去 fit相当于让模型在训练阶段就「偷看」了测试集的价格范围这叫数据泄漏会导致预测结果虚高实际部署时完全失效。正确的姿势是from sklearn.preprocessing import MinMaxScaler # 先切分再归一化顺序不能反 train_size int(len(price_data) * 0.8) train_data price_data[:train_size] test_data price_data[train_size:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_data.reshape(-1, 1)) test_scaled scaler.transform(test_data.reshape(-1, 1)) # 只transform不fit # 构建序列 X_train, y_train create_sequences(train_scaled.flatten(), look_back7) X_test, y_test create_sequences(test_scaled.flatten(), look_back7)这段代码里最关键的是第 9 行——test_data只调用了transform没有重新 fit。MinMaxScaler会记住训练集的最小值和最大值测试集的数据会按训练集的尺度做缩放。这样模型在训练时接触到的所有信息都来自训练集测试集完全陌生评估结果才可信。预测完成后用scaler.inverse_transform()把结果还原成真实价格再算 RMSE 才有实际意义。3.4 keras 模型搭建与训练参数LSTM 层、Dropout 和 EarlyStopping项目里用的是 keras 的 Sequential API 搭模型。菜价预测不是特别复杂的任务一个 LSTM 隐藏层加一个全连接输出层就够了。网络结构太深反而容易过拟合——菜价数据量通常只有几百到几千条深层 LSTM 在这个规模下学不到更多信息只会把训练集的噪声背下来。标准模型结构from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential() model.add(LSTM(units50, return_sequencesFalse, input_shape(7, 1))) model.add(Dropout(0.2)) model.add(Dense(units1)) # 输出层预测下一个时间点的价格 model.compile(optimizeradam, lossmse) print(model.summary()) # 训练参数 early_stop EarlyStopping( monitorval_loss, # 监控验证集损失 patience10, # 连续10轮没改善就停 restore_best_weightsTrue # 恢复最优权重防止过拟合 ) history model.fit( X_train, y_train, validation_split0.1, # 从训练集再切10%做验证 epochs100, batch_size32, callbacks[early_stop], verbose1 )units50是 LSTM 隐藏单元的个数这个值不是越大越好。50 是经验起点数据量在 500 条左右时跑起来很快效果也基本够用。Dropout(0.2)是防止过拟合的关键——训练时随机丢弃 20% 的神经元连接让模型不依赖某一条特定的路径。EarlyStopping的patience10意思是验证集损失连续 10 个 epoch 没有下降就停止训练restore_best_weightsTrue保证你拿到的模型是验证集表现最好的那一版而不是最后一版最后一版往往已经开始过拟合了。训练完成后预测和反归一化的完整流程别忘了pred_scaled model.predict(X_test) pred_price scaler.inverse_transform(pred_scaled.reshape(-1, 1)) true_price scaler.inverse_transform(y_test.reshape(-1, 1)) # 计算RMSE和MAPE from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error rmse np.sqrt(mean_squared_error(true_price, pred_price)) mape mean_absolute_percentage_error(true_price, pred_price) print(fRMSE: {rmse:.3f} 元/斤, MAPE: {mape:.2%})RMSE 的单位是元/斤能看到预测值和真实值平均差多少MAPE 是百分比误差更适合跨品类比较——红尖椒 3 毛钱的误差和菜心 3 毛钱的误差意义完全不同。4. 模型对比与 Web 展示从数值输出到看得见的结论4.1 传统时序方法与 LSTM 同台竞技ARIMA、统计模型和 pyflux项目说明里明确提到了对比环节——「以时间序列、神经网络等方式进行预测并比较各方式优劣」。这一步不是走形式而是为了回答一个实际问题LSTM 在菜价预测上到底比传统方法强多少如果 ARIMA 就够用你没必要非得用深度学习给自己找麻烦。statsmodels 里跑 ARIMA 是标准的几行代码import statsmodels.api as sm # 用训练集拟合ARIMA模型这里用(5,1,0)作为初始参数 model_arima sm.tsa.ARIMA(train_data, order(5, 1, 0)) model_arima_fit model_arima.fit() # 预测测试集同等长度的步数 pred_arima model_arima_fit.forecast(stepslen(test_data))order(5, 1, 0)表示自回归阶数 5、差分阶数 1、移动平均阶数 0。这个参数需要根据 ACF/PACF 图或者 AIC 准则来调实践中常用pmdarima这个库自动搜索最优参数。另一个常被拿来对比的是 Holt-Winters 指数平滑它对有趋势和季节性的序列表现不错代码更简单from statsmodels.tsa.holtwinters import ExponentialSmoothing model_hw ExponentialSmoothing( train_data, seasonal_periods7, # 周季节性 trendadd, seasonaladd ).fit() pred_hw model_hw.forecast(len(test_data))对比的时候别只看一个指标。RMSE 反映绝对误差MAPE 反映相对误差最好把两个都算出来用一张表记录模型RMSE(元/斤)MAPE(%)训练耗时是否适合上线ARIMA(5,1,0)0.429.80.5s可以但难处理突变Holt-Winters0.388.50.3s简单场景够用LSTM(50, look_back7)0.316.745s推荐误差最低我在实际对比中发现一个规律菜价走势平稳的时候Holt-Winters 和 LSTM 差距不大一旦出现台风、寒潮这种突发因素Holt-Winters 会完全失灵而 LSTM 因为有长期记忆能隐约捕捉到同类事件在历史中的影子。这也是项目选择 LSTM 作为主模型的核心理由。4.2 Flask 把模型预测结果变成网页图表路由、JSON 接口和图片输出模型跑通了最终要给人看。项目里用 Flask 做 Web 展示核心就两个接口——一个是返回预测数据的 JSON API一个是返回 matplotlib 生成的趋势图。这样前端既能展示图表又能让用户自己查询某一天的预测价。一个最小可用的 Flask 应用from flask import Flask, jsonify, render_template import matplotlib.pyplot as plt import io, base64 import joblib app Flask(__name__) model joblib.load(lstm_price_model.pkl) # 训练好的模型 scaler joblib.load(scaler.pkl) # 归一化器 app.route(/) def index(): 渲染主页展示历史价格曲线和未来7天预测 history load_recent_prices(days30) # 最近30天真实价格 forecast predict_next_7_days(model, scaler) # 未来7天预测 img generate_chart(history, forecast) return render_template(index.html, chart_urlimg) app.route(/api/forecast/category) def api_forecast(category): JSON接口根据蔬菜品类返回预测价格 data load_category_data(category) pred run_lstm_prediction(data) return jsonify({category: category, forecast: pred.tolist()}) def generate_chart(history, forecast): plt.figure(figsize(10, 4)) plt.plot(history[date], history[price], label历史价格) plt.plot(forecast[date], forecast[price], labelLSTM预测, linestyle--) plt.xlabel(日期) plt.ylabel(价格(元/斤)) plt.legend() buf io.BytesIO() plt.savefig(buf, formatpng, bbox_inchestight) buf.seek(0) img_base64 base64.b64encode(buf.getvalue()).decode() return fdata:image/png;base64,{img_base64}这里我把模型用joblib.dump保存成了.pkl文件Web 服务启动时一次性加载到内存避免每次请求都重新跑模型——LSTM 预测虽然只要毫秒级但模型加载是很重的 I/O 操作放请求里会拖慢响应。generate_chart里把 matplotlib 生成的图片转成了 base64 字符串直接嵌进 HTML 页面省去了静态文件服务器配置的麻烦。4.3 微信公众号查询项目里的 PHP 角色和 token 验证项目说明里单独提到了「php」和「微信公众号」这是整个链路里容易被忽略的一块。公众号做自定义菜单查询蔬菜价格需要服务器端处理微信的验证请求。微信服务器配置时要求你填一个 URL微信会往这个 URL 上带signature、timestamp、nonce参数你必须校验这些参数并返回echostr才能通过验证。项目里的 PHP 在这里的角色是微信公众号的服务器端——处理 token 验证和消息接口。如果你直接用 Python 也能做但 PHP 在很多传统服务器环境里配起来更省事。一个标准的 PHP 验证代码长这样?php define(TOKEN, your_wechat_token); $signature $_GET[signature]; $timestamp $_GET[timestamp]; $nonce $_GET[nonce]; $echostr $_GET[echostr]; $tmpArr array(TOKEN, $timestamp, $nonce); sort($tmpArr, SORT_STRING); $tmpStr implode($tmpArr); $tmpStr sha1($tmpStr); if ($tmpStr $signature) { echo $echostr; // 验证通过原样返回echostr } else { echo 验证失败; }用户通过公众号菜单发送「菜心价格」微信服务器会把这个消息 POST 到你配置的 URL 上PHP 脚本解析 XML 消息再调用 Python 写的预测服务Flask 的/api/forecast/菜心接口把预测结果拼成回复文本返回给用户。整个链路是「用户 ↔ 微信服务器 ↔ PHP ↔ (HTTP) Python Flask ↔ LSTM 模型」这样一条龙。5. 避坑记录LSTM 预测菜价最常见的五个翻车现场5.1 预测曲线几乎等于真实曲线平移归一化泄漏现象LSTM 在测试集上的 RMSE 低得离谱画出来的预测曲线和真实曲线形状一模一样只是往右平移了一天。看起来像「完美预测」实际上模型什么都没学会。原因MinMaxScaler在切分训练集和测试集之前就对全量数据做了 fit模型间接接触了测试集的价格范围信息。这类泄漏会让验证结果虚高真实部署时模型完全失效。解决严格按「先切分、再归一化」的顺序操作。训练集上调用fit_transform测试集只调用transform。如果用了StandardScaler同样遵循这个原则。5.2 验证集 loss 反复震荡不收敛学习率太大或者窗口太长现象训练过程中 val_loss 一会降到 0.01 一会涨到 0.3曲线像锯齿一样。训练结束后的预测结果忽高忽低完全没有稳定性。原因adam 优化器的默认学习率是 0.001在量级小的价格数据上常常偏大另外look_back取值 30 甚至 60 时输入序列过长梯度在长序列回传中不稳定也会导致震荡。解决先把look_back降到 7 试试如果还震荡手动调低学习率——把优化器换成optimizerAdam(learning_rate0.0003)。另一个常用手段是增大batch_size到 64让梯度更新更平滑。5.3 预测价格对突发涨价无感缺了天气和外生变量现象某天台风登陆菜心价格从 3 元飙到 6 元但模型预测价格纹丝不动还是按前几天的趋势走。原因当前模型只用了价格这一个内生变量没有任何外生变量。LSTM 根本不知道台风来了自然无法预测价格突变。解决项目说明里明确提到了「将天气情况等可能相关数据爬取到本地」。把当天的最高气温、降雨量、风力等级作为额外特征拼进输入窗口X的形状从(样本数, look_back, 1)变成(样本数, look_back, 4)。模型学到的就不再是单纯的价格历史而是「价格 天气」的联合模式。5.4 爬虫抓下来的日期有断档周末休市导致的序列空洞现象训练时create_sequences报错或者模型训练完预测结果全是同一个值。检查数据发现 CSV 里的日期不是连续的比如 3 月 12 日之后直接跳到 3 月 14 日。原因批发市场周日休市网站不更新数据爬虫就抓不到这一天的记录。如果直接用这份数据构建序列窗口里的时间间隔不一致LSTM 学到的规律是乱的。解决在清洗阶段用pd.date_range生成完整日期索引再reindex对齐缺失价格用ffill()填充。这一步必须在你把所有 CSV 合并之前单独处理合并后更难发现哪段断了。5.5 训练集和测试集之间有重叠数据序列构建时的索引越界现象测试集的 RMSE 很低但把预测值和真实值画出来看两者完全重合像是模型把训练数据背出来了。原因create_sequences的循环边界写错或者切分训练集和测试集时没有对齐——训练集的最后一个窗口含有了测试集的数据测试集的第一个窗口又包含了训练集的信息。解决在构建序列之前先切分数据然后分别对训练部分和测试部分调用create_sequences。千万别先构建序列再切分那样X里的样本会跨边界信息互相渗透。一个简单的验证方法打印X_train[-1]和X_test[0]确认时间上不重叠。6. 进阶技巧多步预测与滚动回测让预测结果真正可用前面做的是「单步预测」也就是用过去 7 天预测明天一天。但实际业务里你更可能需要未来 3 天或者 7 天的价格走势。多步预测有两种做法各有取舍。第一种是递归多步预测——把模型预测出的明天价格当作已知数据拼进窗口再去预测后天。这种做法简单但误差会逐天累积预测第 5 天时前面 4 天的误差已经叠加进去了。第二种是直接多步预测——修改模型输出层为Dense(n_steps)一次性输出未来 n 天的价格。这种做法训练数据要相应重构但测试时只需要一次推理误差不会累积。我一般用递归多步预测因为改造成本低代码就几行def recursive_forecast(model, last_window, scaler, steps7): 递归多步预测 last_window: 归一化后的最近look_back个价格 steps: 预测未来天数 forecast_scaled [] current_window last_window.copy() for _ in range(steps): # 输入形状需为 (1, look_back, 1) x current_window.reshape(1, -1, 1) next_pred model.predict(x, verbose0) forecast_scaled.append(next_pred[0, 0]) # 把新预测值加入窗口并丢弃最旧的 current_window np.append(current_window[1:], next_pred) # 反归一化还原真实价格 forecast scaler.inverse_transform(np.array(forecast_scaled).reshape(-1, 1)) return forecast.flatten()多步预测必须配滚动回测来验证可信度。做法是每次只用训练集的数据训练模型然后预测未来 7 天把预测值和真实值对比记录误差然后把测试集的时间窗口后移 7 天重新训练再预测。这个「训练-预测-移动」的循环走完整个测试集得到的 RMSE 才是模型真实泛化能力的反映。我踩过最大的坑就是拿固定模型预测整个测试集——价格序列有非平稳性一个月前训练的模型放到月底可能已经失效了。从那以后我每次做时间序列项目都强制走一遍滚动回测流程先跑一个品类记录每个时间窗口的 RMSE如果某个窗口误差突然飙升就去查那段时间有没有极端天气或者节假日把外生变量补进去再重新训练。这套流程走完你拿到的不只是一个跑得通的 LSTM 脚本而是一套知道自己在什么条件下可信、什么条件下会失效的预测系统。希望这份拆解帮到你照着资源里的代码和数据跑通一遍你会对序列预测的整个链路有一个完整的认知。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站