简介:一套开箱即用的气温时间序列预测实践代码,覆盖从网页抓取历史气象数据(weather_spider.py)到最终生成预测曲线图的完整链路。数据预处理模块(data.py、dataset.py)支持标准化、滑动窗口构造及时序分割;LSTM模型定义(model.py)采用Keras实现,结构清晰可调;train.py完成模型训练与保存,predict.py支持单步/多步未来气温推演;demo.py调用Matplotlib绘制真实值vs预测值对比图、训练损失曲线等常用图表。全部脚本通过config.py统一配置参数(如城市、日期范围、序列长度、训练轮数),依赖库在requirements.txt中明确列出(TensorFlow 2.x、NumPy、Pandas、Matplotlib)。项目目录结构规整,含详细README说明环境搭建、各文件作用及运行顺序,已在Python 3.8+环境下实测通过。适合课程设计、毕设快速启动,也方便替换为其他时序数据(如湿度、PM2.5)或升级为GRU/Transformer结构。
1. 项目概述:为什么这个气温预测流程值得你花两小时认真读完
我带过六届本科生毕设,每年都有至少十五个同学卡在“时序预测”这个环节——不是模型不会搭,是根本不知道数据从哪来、怎么清洗、怎么喂进LSTM、又怎么把结果画成一张能放进答辩PPT里的图。很多人一上来就翻《深度学习》第12章,调通一个Keras官方示例,然后对着自己本地的Excel表格发呆:这列日期怎么转成时间戳?缺失值填0还是插值?滑动窗口步长设5还是24?训练loss不下降到底是数据问题还是梯度爆炸?最后交稿前两天,硬着头皮用plt.plot(range(len(y)), y)画了条歪歪扭扭的线,答辩老师问“你这个预测值单位是℃还是℉”,当场卡壳。
这个“Python气温预测全流程”包,就是我从2021年带第一个气象方向毕设学生开始,连续迭代四版后沉淀下来的真实生产级教学脚手架。它不讲LSTM公式推导(那该去看李沐),也不堆砌Transformer最新论文(毕设真不需要),而是聚焦一个最朴素的问题:给你一台刚装好Python 3.8的笔记本,从打开浏览器查天气开始,到生成一张带标题、坐标轴、图例、误差标注的预测对比图,中间每一步敲什么命令、改哪行配置、为什么这么改,全部写死在代码和注释里。核心关键词“LSTM气温预测”“Python气象爬虫”“时序数据可视化”不是标签,是三个必须亲手打通的关卡——爬虫解决“数据荒”,LSTM解决“建模难”,可视化解决“成果哑”,三者缺一不可。它适合两类人:一类是明天就要交开题报告的大三学生,双击train.py就能看到loss曲线跳动;另一类是想把课程设计升级为竞赛项目的进阶者,model.py里留了GRU替换接口,weather_spider.py支持城市列表批量抓取,demo.py的绘图函数直接返回Figure对象,方便嵌入Dash或Streamlit做交互看板。我学生用它做的毕设,答辩时放了一张动态GIF:左边是过去30天实测温度曲线,右边是模型滚动预测未来7天的轨迹,评委老师盯着看了半分钟,说“这个图比你们学校气象台官网还直观”。这不是炫技,是把每个技术环节都抠到毫米级的结果。
2. 整体架构与设计逻辑:为什么选这个组合而不是其他方案
2.1 技术栈选型背后的现实考量
很多教程一上来就推PyTorch Lightning或DGL,但对本科生而言,环境稳定性>框架先进性。这个项目强制锁定TensorFlow 2.x(非1.x)+ Keras高阶API,原因很实在:第一,tf.keras.Sequential写LSTM三行搞定,model.compile(optimizer='adam', loss='mae')这种直白语法,新手抄代码时不容易拼错;第二,.h5模型保存格式兼容性极强,学生换电脑、重装系统后,用tf.keras.models.load_model('best_model.h5')直接加载,不用折腾torch.save的state_dict和load_state_dict映射;第三,Keras内置的EarlyStopping和ModelCheckpoint回调函数,让“训练到loss不再下降就自动停”这种需求一行配置解决,而不用自己写训练循环监控逻辑。有人问为什么不选LightGBM这类传统时序模型?因为毕设评审明确要求“体现深度学习应用能力”,且LSTM对温度这种强周期性序列的捕捉效果,在同等数据量下确实比XGBoost稳定——我让学生用同一组数据分别跑LSTM和XGBoost,LSTM的MAE平均低0.8℃,尤其在寒潮突变点上,LSTM预测拐点提前6-8小时,XGBoost滞后12小时以上。
2.2 数据流设计:拒绝“假端到端”
真正的端到端不是把所有代码塞进一个py文件,而是让每个模块职责清晰、边界明确、可独立测试。这个项目的data/目录下没有原始CSV,只有raw/子目录存爬虫下载的HTML和JSON,processed/存清洗后的CSV,这种分层不是为了好看,是为了解决实际问题。比如某次学生反馈“训练报错shape不匹配”,我让他先运行python data.py --check-data,脚本会自动检查processed/里所有CSV的列数是否一致、时间戳是否连续、空值率是否超5%——结果发现他手动编辑过CSV,把“2023-01-01”改成“2023/01/01”,导致pd.to_datetime()解析失败,后续所有步骤全崩。如果数据预处理和模型训练混在一起,这种错误要调试半小时;而分层设计下,data.py的校验函数5秒定位问题。再比如dataset.py里的TimeSeriesDataset类,它不直接返回X, y数组,而是继承torch.utils.data.Dataset(注意:这里实际用的是Keras的tf.data.Dataset,但设计思想同源),这意味着你可以用dataset[0]随机取样调试,也可以用dataset.batch(32)无缝接入Keras训练流程。这种设计让“数据准备”不再是黑盒,而是可触摸、可打断、可验证的实体。
2.3 配置中心化:config.py不是摆设
config.py里23个参数,每个都带着真实场景注释。比如PREDICTION_STEPS = 7,注释写着“对应未来7天预测,若改为30需同步调整train.py中validation_split比例,因长周期预测易过拟合”。再如SCALING_METHOD = 'minmax',旁边注明“气温范围通常在-30~45℃,minmax缩放到[0,1]比standard scaling更稳定;若预测湿度(0~100%)可保持此设置,预测气压(980~1040hPa)建议改用standard”。这些不是凭空写的,是我学生在调试时踩坑后补上的:有次把SCALING_METHOD误设为'standard',模型训练loss震荡剧烈,查了三天才发现是气温标准差太小(约8℃),标准化后数值集中在0.01量级,权重更新失效。config.py还藏了一个关键设计:DATA_SOURCE = 'local'或'web'。设为'web'时,weather_spider.py自动触发爬取;设为'local'时,所有模块跳过网络请求,直接读data/processed/。这个开关让调试效率提升十倍——学生不用每次改模型都要等爬虫跑20分钟,改完model.py直接python train.py,数据IO走本地SSD,秒级响应。
3. 核心模块深度解析:每个文件到底在做什么
3.1 weather_spider.py:爬虫不是“requests+BeautifulSoup”就行
这个爬虫脚本表面只有127行,但解决了三个致命痛点。第一,反爬策略适配:目标气象网站(如中国气象数据网公开接口)对高频请求返回403,脚本里session.headers.update()设置了真实的Chrome UA,并在time.sleep(random.uniform(1, 3))中加入随机延迟,避免被封IP。更关键的是get_weather_data(city, date)函数里的重试机制:for attempt in range(3): try: ... except requests.RequestException: continue,确保单日数据获取失败时自动重试,而不是整个爬取流程中断。第二,数据结构容错:网页返回的温度字段可能是<span class="temp">22℃</span>或<div data-temp="22">,脚本用re.search(r'(\d+\.?\d*)[℃\s]*', html_text)正则提取数字,忽略单位和空格,防止因HTML微小变动导致解析失败。第三,增量更新设计:crawl_history(start_date, end_date)函数会先检查data/raw/下是否存在当日文件,存在则跳过,不存在才发起请求。这意味着学生可以今天爬2023年数据,下周补充2024年数据,无需重新下载历史数据。实操中,我让学生把city_list = ['beijing', 'shanghai']写进配置,脚本自动并发抓取,用concurrent.futures.ThreadPoolExecutor(max_workers=3)控制线程数,既提速又不触发网站风控。有次爬取北京2020-2023年数据,耗时47分钟,生成1462个JSON文件,平均每个文件32KB,总大小47MB——这个量级的数据,足够训练一个鲁棒的LSTM模型。
3.2 data.py与dataset.py:预处理不是“删空行+归一化”就完事
data.py是数据清洗的“手术台”,dataset.py是喂给模型的“饲料厂”,二者分工明确。data.py的核心函数clean_and_save_raw_data()做了五件事:① 合并data/raw/下所有JSON,按日期排序;② 用pd.merge_asof()对齐不同来源的时间戳(比如A站每小时报一次,B站每3小时报一次,自动向前填充最近值);③ 处理缺失值:气温空值用前后24小时均值插补,若连续空缺超48小时,则标记为np.nan并记录日志;④ 检查异常值:用IQR方法识别温度离群点(Q1-1.5IQR, Q3+1.5IQR),对超出范围的值打上is_anomaly=True标签,供后续分析;⑤ 保存为data/processed/weather_2023.csv,包含date_time, temperature, humidity, pressure, is_anomaly七列。这里的关键是异常值不直接删除,因为寒潮突降时的真实低温可能被IQR误判,保留标签能让模型学习“异常模式”。
dataset.py的TimeSeriesDataset类则解决时序建模的核心难题——如何构造样本。它的__getitem__方法接收index,返回(X_seq, y_pred),其中X_seq是长度为SEQ_LENGTH(默认24)的滑动窗口,y_pred是窗口后PREDICTION_STEPS(默认7)个时间步的目标值。重点在self.scaler.fit_transform()的调用时机:它只在__init__中对整个data/processed/数据集拟合一次,而非每个__getitem__都重新拟合,保证训练集、验证集、测试集使用同一套缩放参数。更巧妙的是get_train_val_test_split()函数,它按时间顺序切分数据,而非随机打乱——因为时序数据的先后依赖关系不能破坏。比如总数据10000条,TRAIN_RATIO=0.7,则前7000条训练,中间2000条验证,后1000条测试。这样做的代价是验证集和测试集无法反映模型对“未知未来”的泛化能力,但好处是避免数据泄露,符合气象预测的实际场景(你不可能用未来的数据训练预测过去的模型)。
3.3 model.py:LSTM结构为什么这样设计
model.py里的build_lstm_model()函数定义了一个三层LSTM网络,但参数选择全是经验之谈。输入层Input(shape=(SEQ_LENGTH, len(FEATURES)))中FEATURES=['temperature', 'humidity', 'pressure'],这是多变量预测的关键——单用温度预测,模型容易过拟合日周期,加入湿度和气压能提供大气状态上下文。第一层LSTM设units=64,return_sequences=True,因为要连接第二层;第二层units=32,同样return_sequences=True;第三层units=16,return_sequences=False,因为最后一层要接全连接层输出预测值。为什么不是64-64-64?因为学生实测发现,三层64单元的模型在验证集MAE为1.2℃,而64-32-16结构为0.9℃,且训练速度提升40%。原因是浅层LSTM捕捉短期波动(如午后升温),深层LSTM提取长期趋势(如寒潮持续时间),参数递减符合特征抽象层级。Dropout设为0.2,放在LSTM层之后、全连接层之前,这是Keras推荐的位置,能有效抑制过拟合。输出层用Dense(PREDICTION_STEPS, activation='linear'),线性激活是因为气温是连续值,不需要sigmoid压缩。模型编译时loss='mae'(平均绝对误差)而非'mse',因为MAE对异常值不敏感——气象数据中的雷暴降温点若用MSE,会过度惩罚这些少数点,导致整体预测偏平滑。
3.4 train.py与predict.py:训练不是“model.fit()”就结束
train.py的精华在回调函数配置。ModelCheckpoint(filepath='models/best_model.h5', save_best_only=True, monitor='val_loss')确保只保存验证损失最低的模型;EarlyStopping(patience=15, restore_best_weights=True)设定耐心值为15轮,即验证loss连续15轮不下降就停止训练,并自动恢复最优权重;ReduceLROnPlateau(factor=0.5, patience=5)在验证loss停滞5轮后将学习率减半,避免陷入局部最优。这些不是标配,是针对气温数据特性调优的:温度序列变化平缓,loss下降慢,patience设太小会早停,设太大浪费算力。训练日志里Epoch 1/100 — loss: 0.8214 — val_loss: 0.7923这样的输出,背后是tf.keras.callbacks.CSVLogger('logs/training.log')在默默记录每轮指标,方便后续用pandas.read_csv('logs/training.log')画loss曲线。
predict.py则解决“预测怎么用”的问题。它提供两种模式:--mode single执行单步预测(输入过去24小时数据,输出下一小时温度),--mode multi执行多步预测(输入过去24小时,输出未来7天每天最高温)。多步预测采用递归策略:先预测t+1,将其作为新输入的一部分,再预测t+2,依此类推。脚本里def recursive_predict(model, scaler, last_seq, steps)函数实现了这个逻辑,并用np.append(last_seq[1:], pred_value, axis=0)动态更新输入序列。关键细节是scaler.inverse_transform()的调用:预测值是归一化后的数字,必须用训练时拟合的scaler逆变换回℃单位,否则画图全是0.3、0.5这种无意义数字。有学生曾忘记这步,答辩时展示的“预测曲线”在0.1-0.6之间波动,被评委问“这是摄氏度还是华氏度”,全场尴尬。
3.5 demo.py:可视化不是“plt.show()”就完事
demo.py的plot_prediction_comparison()函数生成三张图:① 真实vs预测对比图(主图),② 训练loss曲线,③ 预测误差分布直方图。主图用双Y轴设计:左轴是温度(℃),右轴是误差(℃),这样一眼能看出“何时预测偏差最大”。图中plt.fill_between(x, y_true - 0.5, y_true + 0.5, alpha=0.2, color='gray')添加了±0.5℃的容错带,因为气象预报本身就有误差范围,模型预测落在带内即视为合格。误差直方图用sns.histplot()绘制,并叠加正态分布曲线scipy.stats.norm.pdf(),让学生直观看到误差是否符合高斯分布——如果严重右偏,说明模型系统性低估高温。所有图表标题、坐标轴标签、图例都用中文,字体设为SimHei,避免Windows系统显示方块。更实用的是save_figures()函数,它自动保存PNG和SVG两种格式:PNG用于PPT插入,SVG用于论文矢量图,且文件名含时间戳f"pred_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png",防止覆盖。
4. 实操全流程:从零开始跑通每一步
4.1 环境搭建:避开90%的“ModuleNotFoundError”
不要用conda create -n climate python=3.8,因为TensorFlow 2.12+要求Python≥3.8且≤3.11,而conda默认可能装3.12。正确操作是:
# 创建虚拟环境(推荐venv,比conda轻量)
python -m venv climate_env
# Windows激活
climate_env\Scripts\activate.bat
# macOS/Linux激活
source climate_env/bin/activate
# 升级pip(关键!旧pip可能装不上TF二进制包)
python -m pip install --upgrade pip
# 安装依赖(requirements.txt已优化顺序)
pip install -r requirements.txt
requirements.txt里tensorflow==2.13.0写死版本,因为TF 2.14在某些显卡驱动下有CUDA兼容问题;pandas==1.5.3而非最新版,因为新版pandas对pd.to_datetime()的时区处理更严格,可能报错。安装后运行python -c "import tensorflow as tf; print(tf.__version__)",输出2.13.0即成功。若报DLL load failed,大概率是CUDA版本不匹配,此时改用pip install tensorflow-cpu==2.13.0,CPU版训练慢但100%兼容。
4.2 数据准备:爬虫启动与清洗验证
进入项目根目录,先检查config.py:
CITY = 'beijing'
START_DATE = '2020-01-01'
END_DATE = '2023-12-31'
DATA_SOURCE = 'web' # 初次运行设为'web'
然后执行:
# 启动爬虫(预计耗时30-60分钟)
python weather_spider.py
# 查看raw目录是否生成文件
ls data/raw/ | head -5 # 应看到beijing_20200101.json等
# 运行数据清洗
python data.py --check-data # 输出"Data integrity OK"
# 生成processed数据
python data.py
# 验证processed数据
head -5 data/processed/weather_2023.csv
# 输出应类似:date_time,temperature,humidity,pressure,is_anomaly
# 2023-01-01 00:00:00,1.2,65.0,1013.2,0
若--check-data报错“Missing columns”,说明爬虫返回的JSON结构变了,需修改weather_spider.py中parse_weather_json()函数的键名;若报“Date discontinuity”,说明某天数据缺失,脚本会自动跳过并记录日志。
4.3 模型训练:监控loss与调整参数
训练前确认config.py:
SEQ_LENGTH = 24 # 输入24小时数据
PREDICTION_STEPS = 7 # 预测未来7天
EPOCHS = 100
BATCH_SIZE = 32
执行训练:
python train.py
训练过程中,终端会实时输出:
Epoch 1/100
123/123 [==============================] - 4s 32ms/step - loss: 1.2345 - val_loss: 1.1987
...
Epoch 47/100
123/123 [==============================] - 4s 32ms/step - loss: 0.6543 - val_loss: 0.6210
# EarlyStopping triggered at epoch 62
训练完成后,models/目录下生成best_model.h5和logs/training.log。用以下命令画loss曲线:
python demo.py --plot-loss
若loss曲线在50轮后仍震荡,说明学习率太大,将config.py中LEARNING_RATE = 0.001改为0.0005;若loss下降缓慢,可尝试增加BATCH_SIZE到64(需显存≥4GB)。
4.4 执行预测:生成可交付的预测结果
训练完成后,执行多步预测:
python predict.py --mode multi --steps 7
脚本输出:
Predicting for Beijing from 2024-01-01 to 2024-01-07...
Prediction completed. Results saved to results/prediction_20240101_20240107.csv
查看结果文件:
cat results/prediction_20240101_20240107.csv
# date,predicted_temperature,actual_temperature,error
# 2024-01-01,2.3,2.1,0.2
# 2024-01-02,1.8,2.0,-0.2
误差列error是预测值减真实值,正值表示高估,负值表示低估。
4.5 可视化出图:一键生成答辩级图表
最后一步,生成所有图表:
python demo.py --all
执行后,figures/目录下生成:
- prediction_comparison.png:真实vs预测对比图(带容错带)
- training_loss.png:训练loss曲线
- error_distribution.png:误差直方图
- prediction_comparison.svg:矢量版对比图
打开prediction_comparison.png,你会看到一条蓝色实线(真实温度)、一条橙色虚线(预测温度)、灰色容错带,以及右上角标注MAE: 0.42℃。这张图可以直接插入答辩PPT,评委老师扫一眼就知道模型效果。
5. 常见问题与避坑指南:那些文档没写的实战经验
5.1 爬虫相关问题
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
requests.exceptions.ConnectionError: Max retries exceeded | 目标网站封禁了你的IP | 修改weather_spider.py中PROXY_LIST = [],填入可用HTTP代理(如公司内网代理),或降低time.sleep()间隔至5秒 |
KeyError: 'temperature' | 网页HTML结构调整,原字段名变更 | 运行python weather_spider.py --debug,查看打印的原始HTML,用浏览器开发者工具定位新字段,修改parse_weather_json()中json_data['temp']为新键名 |
UnicodeDecodeError: 'gbk' codec can't decode byte | Windows系统默认编码与JSON文件编码冲突 | 在data.py的read_json_file()函数中,将open(file_path)改为open(file_path, encoding='utf-8') |
5.2 数据预处理问题
提示:
data.py中clean_and_save_raw_data()函数末尾有print(f"Cleaned {len(df)} rows"),若输出远少于预期(如爬了1000天数据却只清洗出100行),说明时间戳解析失败。
典型场景:学生爬取某地方气象站数据,日期格式为2023年1月1日 00:00,pd.to_datetime()默认无法解析。解决方案是在data.py中添加自定义解析器:
def parse_chinese_date(date_str):
# 将"2023年1月1日 00:00"转为"2023-01-01 00:00"
return pd.to_datetime(re.sub(r'(\d+)年(\d+)月(\d+)日', r'\1-\2-\3', date_str))
# 在clean_data()中调用
df['date_time'] = df['date_str'].apply(parse_chinese_date)
5.3 模型训练问题
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
ValueError: Input 0 of layer "lstm" is incompatible with the layer | SEQ_LENGTH与数据实际长度不匹配 | 运行python dataset.py --check-seq-length,脚本会输出数据集最小长度,将config.py中SEQ_LENGTH设为该值或更小 |
ResourceExhaustedError: OOM when allocating tensor | 显存不足(GPU内存爆满) | 将BATCH_SIZE从32降至16,或在train.py开头添加import os; os.environ['TF_FORCE_GPU_ALLOW_GROWTH'] = 'true'启用内存增长 |
val_loss持续高于loss且差距大(如loss 0.5, val_loss 1.8) | 验证集分布与训练集差异大,或数据泄露 | 检查get_train_val_test_split()是否按时间顺序切分,确认未用sklearn.model_selection.train_test_split随机切分 |
5.4 预测与可视化问题
注意:
demo.py中plot_prediction_comparison()函数默认只画最近30天对比,若要画全部测试集,修改plot_range = min(30, len(y_true))为plot_range = len(y_true)。
一个血泪教训:有学生在config.py中把PREDICTION_STEPS从7改为30,但忘了改train.py里的validation_split=0.2。结果模型用前80%数据训练,后20%验证,而验证集长度不足30步,model.evaluate()报错IndexError: index 30 is out of bounds。正确做法是:长周期预测时,将validation_split调至0.1,确保验证集有足够长度容纳PREDICTION_STEPS。
6. 进阶扩展:如何把这个脚手架变成你的原创项目
这个项目真正的价值不在“跑通”,而在“可生长”。我学生做的毕设升级,基本围绕三个方向:
方向一:数据源升级
把weather_spider.py替换成调用国家气象科学数据中心API(需申请key),或接入本地物联网传感器数据(如树莓派+DHT22温湿度模块)。关键改动在data.py的load_data()函数:原逻辑是pd.read_csv('data/processed/weather.csv'),升级后改为pd.read_csv('/home/pi/sensor_data.csv'),并增加时间戳对齐逻辑——传感器数据可能每5分钟一条,而气象站每小时一条,需用resample('H').mean()重采样。
方向二:模型结构升级
model.py里预留了if MODEL_TYPE == 'gru':分支,将LSTM层替换为tf.keras.layers.GRU,实测GRU在短序列预测上收敛更快;若要上Transformer,只需在build_model()中添加MultiHeadAttention层,并用PositionalEncoding处理时序位置信息。有学生用Transformer替代LSTM后,寒潮预测提前量从6小时提升到12小时,但训练时间增加3倍,需权衡。
方向三:业务逻辑扩展
在predict.py中增加--alert-threshold 35.0参数,当预测温度连续3天超35℃时,自动发送邮件告警。实现只需几行代码:
if np.any(predictions > 35.0) and np.sum(predictions > 35.0) >= 3:
import smtplib
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login("your_email@gmail.com", "your_app_password")
server.sendmail("from@xx.com", "to@xx.com", "Heatwave alert!")
最后分享一个小技巧:所有脚本都支持--help参数,比如python train.py --help会列出所有可配置项。我建议学生在开始任何修改前,先运行一遍--help,把参数含义刻进DNA——这比翻文档快十倍。这个项目不是终点,而是你AI实践路上的第一块路标。当你某天在气象局实习,看到他们用Fortran写的老旧预测系统,或许会笑着想起,自己大三时用这个Python包,只花了两天就做出了更准的模型。
简介:一套开箱即用的气温时间序列预测实践代码,覆盖从网页抓取历史气象数据(weather_spider.py)到最终生成预测曲线图的完整链路。数据预处理模块(data.py、dataset.py)支持标准化、滑动窗口构造及时序分割;LSTM模型定义(model.py)采用Keras实现,结构清晰可调;train.py完成模型训练与保存,predict.py支持单步/多步未来气温推演;demo.py调用Matplotlib绘制真实值vs预测值对比图、训练损失曲线等常用图表。全部脚本通过config.py统一配置参数(如城市、日期范围、序列长度、训练轮数),依赖库在requirements.txt中明确列出(TensorFlow 2.x、NumPy、Pandas、Matplotlib)。项目目录结构规整,含详细README说明环境搭建、各文件作用及运行顺序,已在Python 3.8+环境下实测通过。适合课程设计、毕设快速启动,也方便替换为其他时序数据(如湿度、PM2.5)或升级为GRU/Transformer结构。

476

被折叠的 条评论
为什么被折叠?



