news 2026/9/15 23:34:00

LSTM股票价格预测毕设实战:从数据到模型的工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSTM股票价格预测毕设实战:从数据到模型的工程化落地

1. 这不是“预测明天涨跌”的玄学,而是一套可复现、可验证、能写进毕设的工程化方案

你搜“深度学习 股票预测”,满屏都是“90%准确率”“涨停板预警”“稳赚不赔”的标题党,点进去要么是模糊不清的截图,要么是几行调用Keras的代码加一句“效果很好”。但作为带过十几届计算机专业毕业设计的过来人,我得说句实在话:真正能放进毕设论文、通过答辩、让导师点头认可的股票价格预测项目,核心从来不是“猜对明天收盘价”,而是完整呈现一个时间序列建模的闭环——从数据清洗的脏活累活,到LSTM结构设计的取舍权衡,再到评估指标选择背后的统计学逻辑。这个项目标题里藏着三个关键信号:“深度学习”是技术栈,“股票价格预测”是任务类型,“计算机毕业设计”是落地场景。它面向的不是量化交易员,而是大四学生——需要代码能跑通、文档能讲清、答辩能自圆其说。所以,我们拆解的起点不是“如何暴富”,而是“如何让一段LSTM代码,既符合学术规范,又具备工程可读性”。关键词里反复出现的“LSTM”“源码”“LW文档”,指向的是实操层面的硬需求:模型不能只停留在理论公式,必须有可调试的Python实现;文档不能只有摘要和结论,得包含数据预处理的每一步归一化参数、模型保存的.h5文件路径、甚至测试集上MAE/RMSE的具体数值。我带过的毕设里,80%的失败案例都栽在同一个坑里:用yfinance随便抓1000条数据,直接喂给默认参数的LSTM,训练完发现loss曲线像心电图,最后硬凑出一张“预测vs真实”的重叠图交差。这不行。真正的毕设级项目,得让你能指着代码里的某一行说:“这里用Min-MaxScaler是因为股价量纲差异太大,如果用StandardScaler,开盘价和成交量会互相淹没”;得让你在答辩时被问到“为什么不用Transformer”时,能拿出回测对比表格,而不是背诵教科书定义。接下来的内容,就是按这个标准拆解的——没有速成秘籍,只有踩过坑后整理出的、能直接抄作业的细节。

2. 为什么选LSTM?不是因为它“最先进”,而是因为它最适配毕设场景的约束条件

2.1 时间序列建模的本质:记忆与遗忘的平衡术

股票价格不是孤立的点,而是由前N天的价格、成交量、MACD指标等共同编织的时间链条。传统机器学习(如随机森林)把每一天当作独立样本,强行切开时间依赖,就像把一本小说撕成单页去猜下一页剧情——信息必然丢失。而深度学习中的循环神经网络(RNN)家族,天生为序列建模而生。但标准RNN有个致命缺陷:梯度消失。当你要预测第100天的价格,模型需要记住第1天的关键信息,但RNN在反向传播时,梯度会像漏水的水管一样层层衰减,传到开头几乎为零。这就导致模型“记性太差”,长期依赖失效。LSTM(长短期记忆网络)正是为解决这个问题而生。它的核心不是增加层数,而是改造神经元内部结构——引入“门控机制”:遗忘门决定丢掉哪些旧记忆,输入门决定吸收哪些新信息,输出门决定释放哪些当前状态。你可以把它想象成一个带智能开关的仓库管理员:每天收盘后,他先清点库存(遗忘门),再根据今日K线和新闻情绪(输入门)决定补什么货,最后按明日策略需求(输出门)取出相应货物。这种结构让LSTM能稳定捕捉数周甚至数月的价格周期性,比如A股常见的“五穷六绝七翻身”效应,或季报发布前后的波动放大现象。我在指导毕设时,曾让学生对比RNN、GRU和LSTM在同一组数据上的表现:RNN训练10轮后loss就卡在0.8不动,GRU降到0.35后开始震荡,而LSTM平稳收敛到0.22——差距不在最终精度,而在训练过程的可控性。毕设答辩最怕什么?怕导师问“你的模型为什么收敛不了”,而不是“为什么没达到95%准确率”。

2.2 LSTM vs Transformer:毕设选型的现实主义考量

热搜词里“LSTM”和“Transformer”并列,但作为毕设选题,我强烈建议初学者从LSTM起步。原因很实际:第一,代码复杂度。一个基础LSTM模型,PyTorch里50行以内就能写完核心结构;而Transformer需要理解位置编码、多头注意力、层归一化等概念,光是实现一个标准Encoder Block就要200+行,调试成本指数级上升。第二,数据需求量。Transformer需要海量序列(通常>10万条)才能发挥优势,而A股单只股票日线数据,十年也就2500条左右,喂给Transformer容易过拟合。第三,解释性要求。毕设论文里“模型结构图”是必选项,LSTM的单元结构图(三个门+细胞状态)在教材里随处可见,导师一眼能看懂;而Transformer的注意力权重热力图,除非你真做了可视化分析,否则画出来就是一片彩色马赛克。我见过最典型的反面案例:学生硬上Transformer,为了凑数据量把沪深300所有成分股拼在一起,结果模型学会的是“大盘股比小盘股波动小”这种行业常识,而非个股特有规律,答辩时被问“你的模型预测的是个股还是板块”,当场哑火。LSTM的局限性当然存在——它对超长期依赖(如政策周期)建模能力弱,但这恰恰是毕设可以诚实讨论的“工作展望”部分,而不是致命缺陷。

2.3 为什么不是CNN或XGBoost?技术选型背后的学术合规性

有些同学会想:“CNN处理图像很厉害,把K线图当图片输入不行吗?”技术上可行,但学术上危险。毕设的核心是验证“深度学习方法在金融时间序列上的适用性”,而CNN本质是提取局部空间特征(比如识别“双底形态”),这已经滑向了技术分析流派,与“基于市场微观结构的深度学习建模”这一主流研究范式脱节。更关键的是,K线图作为输入,需要固定尺寸裁剪,会丢失开盘/收盘价的精确数值信息——而毕设要求的是可复现的数值预测,不是形态分类。至于XGBoost这类树模型,虽然在Kaggle金融竞赛中常胜,但它属于“浅层模型”,无法体现“深度学习”这一课题要求。导师看到毕设标题写着“深度学习”,正文却全是XGBoost参数调优,第一反应就是“这学生没搞懂课题定位”。真正的毕设价值,在于展示你理解了LSTM的隐藏状态(hidden state)如何编码时间信息,理解了为什么堆叠两层LSTM比一层效果好(第一层捕获日内波动,第二层整合周级别趋势),这些才是答辩时能展开的技术纵深。

3. 从雅虎财经API到可训练数据集:数据准备环节的12个致命细节

3.1 数据源选择:为什么yfinance是毕设最优解,而非Tushare或akshare

国内学生常纠结用哪个库,我明确推荐yfinance。理由很实在:第一,免认证。Tushare需要注册获取token,akshare更新频繁易报错,而yfinance一行pip install就能用,避免答辩前夜因API失效推倒重来。第二,字段完整。它能直接获取Adj Close(复权收盘价)、Volume(成交量)、High/Low(最高最低价),这些是构建技术指标的基础。第三,历史久远。支持获取2000年至今的数据,满足“至少5年训练数据”的毕设硬性要求。我让学生试过用akshare抓创业板指,结果返回的DataFrame里日期列是字符串类型,后续时间序列处理全崩;而yfinance返回的index自带datetime64[ns]类型,pandas.resample()直接可用。操作示例:

import yfinance as yf # 获取贵州茅台2018-2023年日线数据 stock = yf.Ticker("600519.SS") df = stock.history(start="2018-01-01", end="2023-12-31") # 关键!检查缺失值 print(df.isnull().sum()) # 如果Volume有NaN,说明停牌日,需用前值填充 df['Volume'].fillna(method='ffill', inplace=True)

注意:.history()默认返回的是美股格式,A股代码要加“.SS”(上交所)或“.SZ”(深交所),这是学生最容易填错的地方,填成“600519”会返回空DataFrame。

3.2 特征工程:超越“收盘价”本身,构建有经济意义的输入变量

毕设最常犯的错误,就是只用Close一列训练。这相当于让模型凭空猜谜。真正有效的输入,必须包含三类信息:
价格动量:过去5日、10日、20日的移动平均线(MA5/MA10/MA20)。计算时用df['Close'].rolling(5).mean(),注意min_periods=1参数,否则开头几行全是NaN。
波动强度:布林带宽度(Bollinger Band Width),即(上轨-下轨)/中轨。它反映市场恐慌程度,A股暴跌前常出现带宽急剧收缩。
量能验证:成交量与5日均量的比值。放量上涨比缩量上涨更具持续性,这是技术分析铁律。
我坚持让学生手动计算这些指标,而不是调用ta-lib库。因为ta-lib安装复杂(Windows需编译),且毕设文档里写“调用第三方库”会被质疑工作量不足。手动实现虽然多写20行,但每个公式都清晰可见,答辩时能解释“为什么MA20比MA60更适合捕捉中期趋势”。例如,计算MA20的代码必须包含:

# 确保索引是日期,否则rolling会出错 df = df.sort_index() df['MA20'] = df['Close'].rolling(window=20, min_periods=1).mean() # 处理首19行的NaN:用首日Close填充,而非drop,保证数据长度一致 df['MA20'].fillna(df['Close'].iloc[0], inplace=True)

3.3 归一化陷阱:为什么MinMaxScaler比StandardScaler更适合股价预测

几乎所有教程都用StandardScaler(Z-score标准化),但股价预测必须用MinMaxScaler。原因在于:StandardScaler将数据映射到均值为0、标准差为1的分布,而股价本身是正偏态分布(多数日子小幅波动,少数日子暴涨暴跌),标准化后,暴跌日的数值会变成-5、-6这样的极端负值,LSTM的tanh激活函数在该区间梯度接近零,导致模型“学不会”暴跌模式。MinMaxScaler则将所有值压缩到[0,1]区间,保留了原始分布的相对关系。更重要的是,毕设要求预测“绝对价格”,而非涨跌幅。如果用StandardScaler,预测输出需逆变换回原尺度,而逆变换公式x = x_scaled * std + mean中的std和mean必须是训练集的固定值——若在测试阶段重新计算,会导致结果失真。正确做法是:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) # 仅对训练集拟合,保存scaler对象 train_data = df[:int(len(df)*0.7)] scaler.fit(train_data[['Close','MA20','Volume']]) # 测试集用同一scaler转换 test_data = df[int(len(df)*0.7):] test_scaled = scaler.transform(test_data[['Close','MA20','Volume']])

这里有个隐藏细节:scaler.fit()必须传入DataFrame子集(如[['Close','MA20','Volume']]),而非Series,否则后续transform()会报错维度不匹配。这个错误我在三届毕设里都见过,学生调试两小时才发现少了一对方括号。

4. 模型构建与训练:从Keras到PyTorch,代码级避坑指南

4.1 输入序列构造:为什么滑动窗口大小=60是经过验证的黄金参数

LSTM的输入不是单日数据,而是连续N天的特征矩阵。窗口大小N的选择,本质是在“记忆长度”和“样本数量”间权衡。N太小(如10),模型看不到周线级别趋势;N太大(如120),5年数据只剩不到100个样本,训练必然过拟合。我让学生用网格搜索验证过:在贵州茅台、宁德时代、招商银行三只股票上,N=60时验证集RMSE最低。原因在于:A股有效交易日约240天/年,60天≈1/4年,恰好覆盖一个季度财报周期,也匹配主力资金的操作节奏。构造代码必须注意时序连续性:

def create_dataset(data, lookback=60): X, y = [], [] for i in range(lookback, len(data)): # X取前60天的所有特征(Close, MA20, Volume...) X.append(data[i-lookback:i, :]) # y取第60天的Close价格(预测目标) y.append(data[i, 0]) # 假设Close是第0列 return np.array(X), np.array(y) # 关键!reshape为LSTM要求的3D格式:(samples, timesteps, features) X_train, y_train = create_dataset(train_scaled) X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], X_train.shape[2]))

常见错误:reshape时把timestepsfeatures维度弄反,导致ValueError: Input 0 is incompatible with layer lstm_1。正确顺序永远是(batch_size, timesteps, features),其中timesteps=60features=3(Close+MA20+Volume)。

4.2 模型架构设计:两层LSTM+Dropout的工业级配置

毕设模型不必追求SOTA,但结构必须体现设计思考。我推荐的标准架构是:

  • 第一层LSTM:50个单元,return_sequences=True(为第二层提供序列输出)
  • Dropout层:0.2(防止过拟合,数值来自经验——大于0.3会欠拟合,小于0.1无效)
  • 第二层LSTM:30个单元,return_sequences=False(输出最终隐藏状态)
  • 全连接层:25个神经元,ReLU激活
  • 输出层:1个神经元,线性激活(预测连续值)

为什么是50+30?第一层负责捕捉细粒度波动(如早盘冲高回落),第二层整合为宏观趋势(如月度上涨通道)。单元数过多(如100+)会导致训练慢且易过拟合,过少(如20)则表达能力不足。代码实现(Keras):

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(50, return_sequences=True, input_shape=(X_train.shape[1], X_train.shape[2])), Dropout(0.2), LSTM(30, return_sequences=False), Dropout(0.2), Dense(25, activation='relu'), Dense(1) ]) model.compile(optimizer='adam', loss='mse')

注意:input_shape只指定后两个维度(timesteps, features),batch_size不在此处定义。Dropout必须放在LSTM层之后、Dense层之前,顺序错了会破坏时序信息。

4.3 训练过程监控:如何用回调函数避免“假收敛”

学生常犯的错误,是设置epochs=100然后干等。实际中,模型可能在第30轮就过拟合,后续训练只是浪费时间。必须用EarlyStoppingModelCheckpoint

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ # 当验证损失10轮不下降,停止训练 EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), # 保存最佳模型,避免训练中断丢失成果 ModelCheckpoint('best_model.h5', save_best_only=True) ] history = model.fit( X_train, y_train, batch_size=32, # 经验值:32在GPU显存和梯度稳定性间平衡 epochs=100, validation_data=(X_val, y_val), callbacks=callbacks, verbose=1 )

restore_best_weights=True是关键!否则EarlyStopping停在第40轮,但最佳权重在第25轮,模型性能会下降。verbose=1显示进度条,方便观察loss是否真在下降——如果val_loss持续高于train_loss,说明过拟合,需加大Dropout或减少LSTM单元数。

5. 结果评估与文档撰写:让毕设答辩不再“答非所问”

5.1 评估指标选择:为什么RMSE比Accuracy更科学

股票预测是回归任务,不是分类。用Accuracy(预测涨跌方向正确率)是学术自杀。因为股价微涨0.1%和暴涨10%都算“涨”,但前者对交易无意义。必须用连续值误差指标:

  • RMSE(均方根误差):对大误差敏感,能暴露模型在暴跌/暴涨日的失效。公式:√(Σ(y_i - ŷ_i)²/n)。毕设报告里必须写出具体数值,如“测试集RMSE=1.82元”。
  • MAE(平均绝对误差):鲁棒性强,反映平均偏差。公式:Σ|y_i - ŷ_i|/n。
  • MAPE(平均绝对百分比误差):消除量纲影响,便于跨股票比较。公式:(100%/n)Σ|y_i - ŷ_i|/y_i。

计算代码:

from sklearn.metrics import mean_squared_error, mean_absolute_error y_pred = model.predict(X_test) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) # MAPE需避免y_test为0(除零错误) mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100 print(f'RMSE: {rmse:.2f}, MAE: {mae:.2f}, MAPE: {mape:.2f}%')

注意:y_test是原始尺度(未归一化)的值,所以RMSE单位是“元”,这比说“loss=0.023”有意义得多。

5.2 可视化报告:三张图定胜负的答辩技巧

毕设答辩PPT里,图表比文字更有说服力。必须包含:

  1. 训练过程图:横轴Epoch,纵轴Loss,两条线(train_loss, val_loss)。重点标出EarlyStopping触发点,证明你控制了过拟合。
  2. 预测vs真实图:横轴日期,纵轴价格,两条线重叠。关键技巧:只画测试集最后100个交易日,避免整条线糊成一团。用plt.plot(test_dates[-100:], y_test[-100:], label='True')
  3. 残差散点图:横轴预测值,纵轴残差(真实-预测)。理想状态是点均匀分布在y=0附近。如果出现“喇叭形”(残差随预测值增大而扩大),说明模型对高价股预测偏差更大,需在文档中讨论。

绘图代码示例:

import matplotlib.pyplot as plt # 图1:训练曲线 plt.figure(figsize=(12,4)) plt.subplot(1,2,1) plt.plot(history.history['loss'], label='Train Loss') plt.plot(history.history['val_loss'], label='Val Loss') plt.title('Model Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() # 图2:预测效果 plt.subplot(1,2,2) plt.plot(y_test[-100:], label='True Price') plt.plot(y_pred[-100:], label='Predicted Price') plt.title('Prediction on Test Set (Last 100 Days)') plt.xlabel('Days') plt.ylabel('Price (CNY)') plt.legend() plt.tight_layout() plt.show()

5.3 LW文档核心章节:导师最关注的5个段落怎么写

毕设文档不是代码说明书,而是学术叙事。我要求学生在“系统设计”章节必须包含:

  • 数据预处理流程图:用文字描述步骤:原始数据→缺失值填充→特征构造→归一化→滑动窗口切分。图中要标注每个步骤的参数(如MA20的window=20)。
  • 模型结构表:列出每层名称、输入输出维度、参数量。例如:“LSTM Layer 1: Input(32,60,3) → Output(32,60,50), Params=12,200”。
  • 超参数选择依据:不能写“参考网上教程”,要写“经网格搜索,batch_size=32时GPU利用率85%,loss下降最稳;learning_rate=0.001时Adam优化器收敛最快”。
  • 评估结果对比表:横向对比不同模型(LSTM/GRU/MLP)的RMSE/MAE,证明LSTM最优。
  • 误差分析段落:指出模型在哪类行情下失效(如“2022年10月医药股集体涨停期间,预测偏差达±5%,因模型未纳入行业新闻特征”),这比吹嘘精度更有学术价值。

最后强调:所有图表必须有编号和标题,如“图3.1 训练损失曲线”,代码块必须有语言标识(```python),公式用LaTeX(如$RMSE=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2}$)。这些细节,是区分“能毕业”和“优秀毕设”的分水岭。

6. 常见问题排查与实战心得:那些没人告诉你的“潜规则”

6.1 “Loss不下降”问题的三级排查法

这是毕设最常遇到的崩溃时刻。我的排查流程是:一级:数据检查

  • 打印X_train.shapey_train.shape,确认维度匹配(如X_train=(1000,60,3),y_train=(1000,))。
  • np.isnan(X_train).any()检查输入是否有NaN,归一化前未处理缺失值是主因。

二级:模型检查

  • model.summary()后,加一行print(model.input_shape),确认输入维度正确。
  • 临时把LSTM单元数设为5,运行1轮,看loss是否变化。如果仍不降,说明数据或标签有问题。

三级:训练检查

  • batch_size改为1,epochs=1,观察单步loss。如果loss=nan,说明学习率太大(尝试0.0001)。
  • 关闭Dropout,看loss是否下降。如果关闭后下降,说明Dropout比例过高。

我见过最离谱的案例:学生把y_train写成df['Close'].values,但df是原始DataFrame,Close列含NaN,导致y_train里有nan,训练时loss直接爆为inf。解决方案:y_train = df['Close'].dropna().values

6.2 “预测结果全平”问题的根源与解法

模型输出一条直线,意味着它学会了“预测均值”。原因通常是:

  • 标签泄露:在构造y时,用了df['Close'].iloc[i],但i对应的是X的最后一天,导致yX的最后一行完全重合——模型只需记住最后一行价格即可。正确做法:y应取X之后一天的价格,即df['Close'].iloc[i]对应Xi-60i-1行。
  • 归一化不一致:训练时用scaler.fit_transform(),预测时用scaler.transform(),但如果scaler是用整个数据集拟合的,测试集归一化会引入未来信息。必须只用训练集拟合。
  • 激活函数错误:输出层用了'sigmoid',把价格压缩到[0,1],逆变换后失去量纲。必须用'linear'。

修复代码:

# 正确的y构造 y = df['Close'].iloc[lookback:].values # 从第60行开始取y # 确保scaler只在训练集上fit scaler.fit(train_data[['Close']]) y_train_scaled = scaler.transform(y_train.reshape(-1,1)).flatten()

6.3 毕设答辩高频问题应答模板

导师最爱问的三个问题,答案必须提前准备好:
Q1:“你的模型能实盘交易吗?”
A:“不能。本项目定位是学术验证,证明LSTM能学习股价的时间依赖模式。实盘需加入风控模块、订单执行延迟、手续费模拟,这已超出毕设范围。但模型输出的预测误差分布,可为仓位管理提供参考。”(展现边界意识)

Q2:“为什么不用Transformer?”
A:“我对比过,在相同数据量下,LSTM的RMSE比Transformer低12%。原因是Transformer需要大量数据学习位置编码,而单只股票5年数据仅2500条,不足以支撑其参数量。后续可尝试用LSTM提取特征,再接轻量Transformer,这是论文‘未来工作’部分的计划。”(用数据说话,不贬低技术)

Q3:“特征工程中为什么没加入新闻情感?”
A:“新闻文本处理需额外NLP模块,会大幅增加项目复杂度。本毕设聚焦于价格序列本身的建模能力。但我在附录中提供了接口设计:预留了news_sentiment字段,未来可接入SnowNLP库做情感打分,作为新特征输入。”(承认局限,给出扩展路径)

最后分享一个血泪教训:答辩前务必用另一台电脑重新clone代码库,从零安装环境、运行全流程。我带的学生里,有3人因本地环境依赖(如特定TensorFlow版本)导致答辩现场演示失败。真正的毕设,是代码、文档、答辩三位一体的可靠性验证,而不是某个深夜调通的“奇迹”。

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

汽车电子开发入门:工具链、AUTOSAR与量产思维三重门槛

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:31:13

星战VR与华为大赛同周上线,内容生态接棒设备竞争

《星球大战:飞行中队》宣布 10 月 2 日在 PC VR 和 PSVR 同步上线,同一周首届华为 VR 开发应用大赛总决赛暨 VR 产业峰会落幕。这两件事单看都是行业动态,合在一起就变成了一条明确的信号:VR 已经过了“靠设备攒热度”的阶段&…

作者头像 李华
网站建设 2026/9/15 23:30:49

ESP32红外实时监控系统:边缘采集+云闭环实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:24:29

吉他自学资料整理指南:教程、谱子与工具的高效搭配

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:22:42

CAN自定义协议设计:从ID规划到状态机的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华