news 2026/10/2 1:56:16

LSTM电力负荷预测实践:基于PyTorch的源码拆解与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSTM电力负荷预测实践:基于PyTorch的源码拆解与避坑指南

简介:基于PyTorch实现的LSTM电力负荷预测完整工程,面向具备一定Python基础、希望入门时序预测或深度学习建模的开发者。项目覆盖数据预处理、LSTM网络搭建、训练、测试与结果可视化全流程,可帮助读者理解门控机制如何缓解传统RNN的梯度消失问题,并掌握从数据清洗、归一化到模型评估的实操方法。压缩包共14个文件,包含5个Python脚本(分别对应数据处理、模型定义、训练、测试等模块)、5个CSV格式的电力负荷数据集、1个已训练好的模型权重文件、1个Markdown项目说明文档及运行生成的缓存文件,整体大小仅2.1MB,目录结构清晰、便于对照学习。目前已有156人学习下载,说明该示例具备一定参考热度。项目说明文档对代码实现和参数设置做了细致解读,可直接复现或在此基础上调整超参数、优化网络结构,是理解LSTM原理与PyTorch应用相结合的实用入门资料。

1. 从调度台到Jupyter:这份LSTM电力负荷预测源码解决了什么

做电力负荷预测的人多少都经历过这种时刻:眼看明天午高峰的负荷曲线就要往上窜,传统ARIMA或者简单回归模型的预测值却还在低位平躺,偏差大得让人没法跟调度解释。LSTM之所以在这一场景里被反复拿出来用,是因为它靠门控机制把长距离的时序依赖记住了——前一天的峰值、一周前的同类型日、节假日前后那段异常波动,都能被编码进隐状态里。这份基于PyTorch实现的LSTM电力负荷预测源码,正是把「读历史负荷序列 → 训练循环神经网络 → 输出未来一段时间的负荷值」整条链路打包好的可运行项目。

它解决的问题很具体:你有一份电力负荷历史数据(通常是按小时或15分钟一个采样点),想预测未来24小时或更长的负荷走势,但又不想从零去拼数据处理、模型搭建和评估代码。这份资源适合三类人:刚入门PyTorch、想找个完整时间序列项目练手的学生;需要快速搭建负荷预测基线的算法工程师;以及在做能源管理系统、微电网调度时,需要一个可解释、可复现预测模块的从业者。项目本身不依赖分布式集群,单张GPU甚至CPU都能跑完。接下来我会按数据准备、模型搭建、训练评估、避坑、落地这条线,把这套源码的每个关键节点拆开讲清楚。

2. 先让数据变成LSTM能吃的样子:滑窗、归一化与DataLoader的时序逻辑

2.1 负荷序列的滑窗构造:look_back怎么定才不玄学

LSTM不接收一条孤零零的数据点,它接收的是一个有先后顺序的窗口序列。电力负荷预测里最常用的做法是滑窗(sliding window):用过去N个时刻的负荷值预测未来M个时刻的负荷值。这个N就是look_back,M一般叫predict_step。

import numpy as np from sklearn.preprocessing import MinMaxScaler def create_sequences(data, look_back=24, predict_step=1): X, y = [], [] for i in range(len(data) - look_back - predict_step + 1): X.append(data[i : i + look_back]) y.append(data[i + look_back : i + look_back + predict_step]) return np.array(X), np.array(y) # 原始负荷数据,假设已经是按小时采样的一维数组 raw_data = np.loadtxt("load_data.csv", delimiter=",") scaler = MinMaxScaler(feature_range=(0, 1)) scaled_data = scaler.fit_transform(raw_data.reshape(-1, 1)).flatten() look_back = 24 # 用过去24小时预测 predict_step = 1 # 预测下一个时刻 X, y = create_sequences(scaled_data, look_back, predict_step) train_size = int(len(X) * 0.8) X_train, X_test = X[:train_size], X[train_size:] y_train, y_test = y[:train_size], y[train_size:]

这里look_back=24的含义是:用过去24个小时的负荷来预测下一个小时的负荷。如果数据是15分钟一个采样点,24就代表6小时,得按实际采样频率换算。predict_step=1代表单步预测,这也是这套源码默认的简单场景;想要一次预测未来24小时,就把predict_step改成24,但要注意改成多步之后损失函数和评估逻辑都要跟着调整。滑窗有个容易被忽略的细节:X的形状是(samples, look_back, 1),第三维是特征维度,LSTM要求输入必须是三维,这一点在下一步进DataLoader时要保持住。

2.2 归一化里的隐藏门槛:scaler只能fit训练集

电力负荷数值动辄几百上千MW,直接塞给LSTM会让损失函数震荡得厉害,激活函数也很容易饱和。所以归一化是必做的前置步骤。但这个环节藏着一个新手几乎必踩的坑:如果在全量数据上fit了MinMaxScaler,再用它去transform训练集和测试集,测试集的信息就已经通过scaler泄漏进了训练过程。

# 正确做法:先切分,再fit训练集 train_raw, test_raw = raw_data[:train_size + look_back], raw_data[train_size + look_back:] scaler_train = MinMaxScaler(feature_range=(0, 1)) scaler_train.fit(train_raw.reshape(-1, 1)) train_scaled = scaler_train.transform(train_raw.reshape(-1, 1)).flatten() test_scaled = scaler_train.transform(test_raw.reshape(-1, 1)).flatten()

注意这里切分原始数据时要把look_back部分预留出来,保证滑窗在边界上不会把训练集末尾的标签丢掉。测试集的归一化必须复用scaler_train,而不是重新fit一个scaler。否则测试集被单独缩放后,模型看到的数值分布和训练时不一致,预测结果在反归一化之后往往会出现整体偏移。反归一化时用同一个scaler把预测值换回真实负荷单位,这步做错了,后面所有评估指标都会失真。

2.3 DataLoader的batch顺序:别再被(batch, seq_len, input_size)绕晕

PyTorch的DataLoader会对一个batch内的序列自动在第0维堆叠。如果X的形状是(samples, look_back, 1),那么batch出来之后是(batch_size, look_back, 1),这正好符合nn.LSTM要求的输入形状。但不少人在这里翻车的原因是:先手动把X reshape成了(samples, 1, look_back),想当然地以为时间维要放中间,结果batch之后变成(batch_size, 1, look_back),LSTM把每一行当成一个时间步长度为1的序列,模型完全学不到时序依赖。

import torch from torch.utils.data import TensorDataset, DataLoader X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) # 关键:X_train已经是(samples, look_back, 1),这里不要再reshape train_dataset = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) for batch_X, batch_y in train_loader: print(batch_X.shape) # 输出: torch.Size([64, 24, 1]) # 64是batch_size,24是look_back,1是特征数 break

shuffle=True在时间序列里是个争议点。对于负荷预测这种强周期数据,shuffle之后每个batch里混着不同季节、不同时段的样本,模型更容易学到的是「给定任意历史窗口,下一时刻大概是什么量级」,而不是死记硬背书序。这套源码默认开启shuffle,我建议保留,因为负荷数据本身周期性强,不shuffle时连续batch的高度相关会让梯度更新方向偏执。但如果你要做严格的时序回测(比如用t时刻之前的所有数据训练,预测t之后),就应该把shuffle关掉,改成按时间顺序切分验证集。

3. 把LSTM模型立起来:PyTorch的nn.LSTM参数与维度对齐

3.1 nn.LSTM输入输出形状:h_n和c_n到底什么时候用

PyTorch的nn.LSTM接口非常简洁,但简洁背后是两组容易混淆的输出。一个LSTM层接收的输入形状是(seq_len, batch_size, input_size),如果batch_first=True,则是(batch_size, seq_len, input_size)。输出有两个:output和(h_n, c_n)。output是所有时间步的隐状态序列,形状是(batch_size, seq_len, hidden_size);h_n是最后一个时间步的隐状态,形状是(num_layers, batch_size, hidden_size)。

import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1, dropout=0.2): super(LSTMPredictor, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch_size, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # out: (batch_size, seq_len, hidden_size) # 取最后一个时间步的隐状态 last_hidden = out[:, -1, :] # (batch_size, hidden_size) pred = self.fc(last_hidden) # (batch_size, output_size) return pred

在预测任务里,我们通常只关心最后一个时间步的输出,因此用out[:, -1, :]把最后一个时间步的hidden_size维向量取出来,再接一个全连接层映射到输出维度。h_n和c_n在这种单步预测场景里可以不直接用,但如果你做的是序列生成(比如解码阶段逐步喂预测值),就需要把h_n、c_n作为下一时间步的初始状态传回去。dropout在num_layers=1时传入会报警告,因为单层LSTM内部没有可dropout的层间连接,这个参数只有层数大于1才生效。

3.2 hidden_size和num_layers怎么定:先看数据量再谈调参

hidden_size是LSTM隐状态向量的维度,它决定了模型记忆容量的大小。对电力负荷预测这种单特征序列,hidden_size取32到128一般就够用。数据量只有几千个点时,hidden_size=128的模型很容易过拟合,训练loss降得很漂亮,验证集MAPE却越跑越高。num_layers决定堆叠几层LSTM,层数加深能捕捉更抽象的时序特征,但训练难度同步上升,梯度在跨层反向传播时更容易衰减。

model = LSTMPredictor( input_size=1, hidden_size=64, # 隐状态维度,中等数据量从64起步 num_layers=2, # 两层LSTM,兼顾表达能力和训练稳定 output_size=1, dropout=0.2 # 两层之间加dropout防过拟合 ) for name, param in model.named_parameters(): if 'weight_ih' in name: nn.init.xavier_uniform_(param) elif 'weight_hh' in name: nn.init.orthogonal_(param)

权重初始化在LSTM里值得单独说。PyTorch默认的初始化方式对LSTM通常能正常工作,但手动改成xavier_uniform_初始化输入权重、orthogonal_初始化循环权重,是训练更稳定的常见做法。原因在于:LSTM的循环权重矩阵如果初始特征值过大,很容易引发梯度爆炸;orthogonal初始化能保证矩阵的条件数接近1,让梯度在时间步之间传递时不至于快速膨胀或消失。这套源码里用了这个初始化方案,如果你自己写模型,建议保留。

3.3 LSTM vs 其它时序模型:为什么负荷预测场景选它不选Transformer

做负荷预测时经常被问到:为什么不用Transformer?Attention不是更强吗?关键在于数据规模和训练成本。电力负荷预测通常是单变量或少量变量的时间序列,样本量有限,Transformer在这种小规模序列上容易过拟合,而且它的位置编码和时间步之间的注意力矩阵,对周期性的捕捉并不比LSTM的门控机制更高效。LSTM的归纳偏置——逐步读取、选择性遗忘——天然贴合负荷数据的平滑连续特性。

对比项LSTMTransformer
序列长度适应性适合几百步内的中等长度擅长超长序列,但短序列优势不明显
参数量hidden_size=64时约1.7万参数同表达力下参数量大一个量级
训练稳定性配合梯度裁剪容易收敛学习率敏感,需要warmup
负荷预测场景单特征、中等样本量表现稳定多变量、长上下文时才有明显优势

这套源码用的就是单层或两层LSTM加一个全连接输出层的轻量结构,CPU上训练几分钟就能看到收敛趋势。如果你的数据特征不止负荷本身,还包含温度、湿度、星期几等外部变量,可以把input_size从1改成特征总数,在滑窗构造时把多列数据一起拼进X。

4. 训练循环与评估:从MSE到MAPE的完整验收流程

4.1 训练循环:Adam优化器、StepLR与梯度裁剪

LSTM训练里最常见的两个不稳定因素:学习率设太大导致loss震荡,以及时间步展开后梯度累积导致的爆炸。Adam优化器本身对学习率有一定的自适应能力,但初始学习率仍然敏感。这套源码默认用lr=0.001,配合StepLR每30个epoch衰减为原来的0.1倍,再加上梯度裁剪max_norm=1.0,基本能保证训练过程不翻车。

import torch.optim as optim from torch.optim.lr_scheduler import StepLR optimizer = optim.Adam(model.parameters(), lr=0.001) scheduler = StepLR(optimizer, step_size=30, gamma=0.1) criterion = nn.MSELoss() epochs = 80 for epoch in range(epochs): model.train() epoch_loss = 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_X) loss = criterion(pred, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() epoch_loss += loss.item() * batch_X.size(0) scheduler.step() if (epoch + 1) % 10 == 0: print(f"Epoch {epoch + 1}/{epochs}, Loss: {epoch_loss / len(train_dataset):.6f}")

梯度裁剪这行代码容易被新手删掉,但它对LSTM几乎属于必需品。训练早期如果出现loss突然跳到nan,多半是梯度范数过大,裁剪能直接压制这个问题。StepLR的step_size要根据epoch总数调整,如果训练100个epoch,step_size=30意味着在30、60、90轮时各衰减一次;如果只跑50轮,step_size=30就只有一次衰减。这套源码默认80轮,step_size=30的配置合理。

4.2 反归一化计算MAPE:指标比loss更能说明问题

MSE是训练用的损失函数,但它给出的是归一化空间里的误差,业务上不好解释。电力负荷预测行业里最常用的验收指标是MAPE(平均绝对百分比误差)和RMSE(均方根误差)。关键在于计算这些指标前,必须把预测值和真实值都反归一化回原始负荷单位。

from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error model.eval() with torch.no_grad(): test_X_t = torch.tensor(X_test, dtype=torch.float32) test_y_t = torch.tensor(y_test, dtype=torch.float32) test_pred = model(test_X_t).numpy() # 反归一化:用训练阶段同一个scaler y_test_inv = scaler_train.inverse_transform(test_y_t.numpy().reshape(-1, 1)).flatten() pred_inv = scaler_train.inverse_transform(test_pred.reshape(-1, 1)).flatten() rmse = np.sqrt(mean_squared_error(y_test_inv, pred_inv)) mape = mean_absolute_percentage_error(y_test_inv, pred_inv) * 100 print(f"RMSE: {rmse:.2f} MW") print(f"MAPE: {mape:.2f}%")

注意test_y_t和test_pred的形状需要对齐。y_test在滑窗构造时是(samples, predict_step),predict_step=1时是(samples, 1),reshape(-1, 1)后inverse_transform的维度才匹配。MAPE在真实负荷接近0的时刻会异常放大,比如凌晨低谷期负荷几百千瓦时,一个小的绝对误差就会产生很大的百分比误差。所以看MAPE时建议把负荷低于某个阈值(比如峰值的10%)的样本剔除后再算,否则你会被个别凌晨时段的误差拉低整体评价。

4.3 把预测曲线画出来:肉眼比指标更能发现滞后问题

数值指标不是验收的全部。把测试集的真实负荷和预测负荷画在同一张图上,你会直观看到模型的拟合质量和滞后情况。这一步我会单独说,因为曲线形态能暴露指标掩盖的问题——比如预测曲线整体向右平移了一个时间步,这种情况的MAPE可能只有5%,但实际用途上已经完全不可用。

import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) # 只展示测试集最后168个点(一周) n_show = 168 plt.plot(y_test_inv[-n_show:], label="True Load", linewidth=1.5) plt.plot(pred_inv[-n_show:], label="LSTM Prediction", linewidth=1.5, linestyle="--") plt.xlabel("Hour") plt.ylabel("Load (MW)") plt.legend() plt.grid(alpha=0.3) plt.savefig("prediction_curve.png", dpi=150)

如果预测曲线比真实曲线滞后一个时间步,你看到的现象是预测值的拐点永远比真实拐点晚到。原因通常是模型学到了「拿当前值近似下一时刻」的捷径——当负荷变化平缓时,t时刻的负荷确实接近t+1时刻,模型发现这样预测loss也很低,就不愿意去学真正的变化趋势了。减轻滞后可以从三方面入手:增加look_back到48或72,让模型有更长的上下文;把输入里加入时刻特征(比如hour_of_day的sin/cos编码);或者对loss函数做修改,不只算MSE,而是叠加一阶差分误差项,惩罚预测曲线的形态偏差。

5. 电力负荷预测避坑指南:五个我反复踩过的坑

5.1 归一化泄漏导致测试集指标虚高

现象:测试集MAPE只有1.5%,但部署到线上之后误差直接翻倍。原因:在全量数据上fit了MinMaxScaler,测试集的数值范围被scaler提前“看过”,模型在评估时相当于开卷考试。解决:严格按「先切分,只用训练集fit scaler,再transform测试集」的顺序执行,上一章代码里的scaler_train就是为这个准备的。从那以后我每写一个时间序列项目,都会先在代码里搜一遍fit_transform的位置,确认它没有出现在切分之前。

5.2 训练loss正常但验证loss完全不平滑

现象:训练epoch从第1轮到第10轮loss持续下降,但验证集loss忽高忽低,甚至某些epoch突然变成nan。原因:学习率过大加上LSTM时间维梯度累积到爆炸,反向传播时某些参数的梯度范数超过浮点数表达上限。解决:在optimizer.step()之前加一行clip_grad_norm_(model.parameters(), max_norm=1.0);如果已经出现nan,把学习率从0.001降到0.0005重新训练,同时检查batch输入里有没有包含nan值——负荷数据CSV里偶尔会有空行或异常字符,pd.read_csv读进来后要加一步dropna。

5.3 预测曲线整体滞后一拍

现象:预测值和真实值拟合得很好,MAPE也在可接受范围,但放大看每个峰值都晚了一个采样点才出现。原因:负荷数据相邻时刻高度相关,模型发现直接复制上一时刻的数值就能把loss降到很低,于是放弃学习真实的动态变化规律。解决:把look_back从24增大到48或72;在输入特征里拼上时间编码(小时、星期几的sin/cos变换),打破模型对“当前值即未来值”的惰性依赖;对loss加一阶差分惩罚项,用label_smooth策略强制模型关注变化趋势而不是绝对数值。

5.4 训练和测试的batch维度假设崩塌

现象:单步预测时一切正常,改成predict_step=24之后,训练代码报错说维度和预期不符。原因:y的形状从(samples, 1)变成了(samples, 24),而模型的output_size还是1,全连接层输出维度对不上。解决:把output_size同步改成predict_step,并且在评估时把pred用reshape(-1, predict_step)和真实y对齐。多步预测如果是用递归方式(把上一步预测值拼回输入),更要注意每一步生成的张量形状必须保持(batch_size, 1, input_size)的三维结构,少了中间维度就会在cat时报错,这个坑我在自己写代码时至少踩过三次。

5.5 设备不匹配直接报错或CPU训练慢到怀疑人生

现象:程序在本机能跑,换到服务器上之后爆出RuntimeError: Expected all tensors to be on the same device。原因:模型放在了GPU上,但输入数据没有调用.to(device),或者反过来。解决:固定写法是把device定义放在数据加载之后,统一执行model.to(device)和batch_X.to(device)。另外,PyTorch安装时要注意CPU版本和CUDA版本的区别,纯CPU版本的torch在GPU服务器上也能跑但完全用不上显卡,训练速度能差几十倍。检查方式很简单:print(torch.cuda.is_available()),输出False就得去重装对应CUDA版本的PyTorch。

6. 让模型不止会跑:多步预测、模型导出与增量学习的三个落地技巧

多步预测是负荷预测从演示走向实用的第一道坎。单步预测虽然指标好看,但调度需要的是未来24小时的曲线,而不是只告诉你下一个小时是多少。常见的做法有两种:递归多步预测和直接多步预测。递归方式把上一时间步的预测值拼进输入窗口,逐步滚动生成整个未来序列:先预测t+1,把它加进窗口末尾并去掉最旧的时间步,再预测t+2,依次类推。这种方式实现简单,但有误差累积问题——第一步预测的误差会进入第二步的输入,越往后误差越大。直接多步预测则是一次性输出未来24个时刻的负荷值,把output_size从1改成24,代价是模型结构变胖、训练时间变长。

我自己的工程习惯是两者折中:用直接多步预测输出整体趋势,再用残差修正处理误差累积。具体做法是让模型输出predict_step=24的预测序列,训练时loss在MSE基础上叠加一阶差分误差,这样模型必须学会生成形状合理的曲线而不只是逼近单点数值。代码上只需要改model的output_size为24,loss里多算一项torch.mean(torch.abs(torch.diff(pred, dim=1) - torch.diff(batch_y, dim=1)))。这个差分项的权重设为0.2左右,太小没效果,太大会让模型过度关注曲线形状而忽略绝对精度。

模型保存方面,我推荐把训练好的权重连同scaler一起打包持久化。很多项目只保存了model.state_dict(),换到另一台机器上做推理时发现还要重新fit一遍scaler才能反归一化,非常被动。正确的做法是用torch.save同时存模型状态和scaler对象:

torch.save({ 'model_state_dict': model.state_dict(), 'scaler': scaler_train, 'look_back': look_back, 'predict_step': predict_step }, 'lstm_load_forecast.pt') # 推理时恢复 checkpoint = torch.load('lstm_load_forecast.pt', map_location='cpu') model.load_state_dict(checkpoint['model_state_dict']) scaler_loaded = checkpoint['scaler']

最后说增量更新。电力负荷的模式会随季节更替缓慢漂移,一套权重跑半年之后MAPE往往从2%涨到4%。我的习惯是每周做一次轻量微调:用最近一周的新数据跑20个epoch,学习率设成初始值的十分之一,只更新模型参数不重建scaler和滑窗结构。这样既保留了历史学到的负荷模式,又能跟上最近的趋势变化。从那以后我每次上线负荷预测模型,都会把「模型保存是否带scaler」和「是否配了增量更新脚本」写进交付清单里强制检查一遍,这两个细节决定了一个模型是只能跑demo还是能扛住半年的真实业务。希望这篇拆解能帮你把这份源码跑通、跑稳,在电力负荷这条路上少绕几个弯。

本文还有配套的精品资源,点击获取

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

C语言子集编译器源码解析:从词法分析到MIPS汇编全链路

简介:这份资源是面向编译原理学习者与课程实践者的词法分析阶段完整实现包,聚焦C语言文法到MIPS汇编的编译流程,适合正在做编译器课程设计或想深入理解前端构造的读者。压缩包共19个文件,以10个h头文件与7个cpp源文件为主&#xf…

作者头像 李华
网站建设 2026/10/2 1:55:32

ArcGIS入门必练:十分钟快速出图的核心流程与坐标系避坑

先说出我的结论:ArcGIS 入门最该练的,不是背按钮,而是先把“数据、坐标、出图”这条主线想通。很多人装上 ArcGIS 10.2 或者 ArcGIS Pro 之后,第一反应是“我该点什么”,结果十分钟过去,还在“添加数据”那…

作者头像 李华
网站建设 2026/10/2 1:55:27

yum安装Redis全流程:源配置、systemd托管与缓存治理避坑指南

说个真实经历。早些年我维护的一台 CentOS 7 机器,生产环境突然要上 Redis,当时图省事直接源码编译,结果 gcc 版本太低,make 报错搞了一下午,最后还因为编译参数漏了 jemalloc,上线半个月就开始出现延迟抖动…

作者头像 李华
网站建设 2026/10/2 1:55:18

Open WebUI 工具调用从零搭建:20 分钟让模型自己会办事

Open WebUI 工具调用从零搭建:20 分钟让模型自己会办事 【免费下载链接】open-webui User-friendly AI Interface (Supports Ollama, OpenAI API, ...) 项目地址: https://gitcode.com/GitHub_Trending/op/open-webui 你问模型"查一下最近三天 AI 圈有什…

作者头像 李华
网站建设 2026/10/2 1:54:27

双RTX 3090 + vLLM 部署 Qwen2.5-14B 低成本私有化推理实践

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

作者头像 李华