9000样本天气分类实战:从数据划分到模型微调全流程
简介这份天气分类数据集面向计算机视觉入门与进阶学习者以及需要开展图像分类实验的学生和开发者可用于CNN模型训练、迁移学习对比与数据增强等场景。资源共包含2000个文件以7987张jpg图像为主体另附1个py脚本与1个png图片压缩包约676.81MB训练集7180张、测试集808张覆盖cloudy、dew、fogsmog、frost、glaze、hail、lightning、rain、rainbow、rime、sandstorm、shine、snow、sunrise共14个类别目录按train_data与test_data划分便于直接加载训练与评估。已有1130人学习下载作者使用CNN模型取得96.3%的最好结果说明数据质量与类别区分度较好。读者可基于该数据集复现分类流程、调整网络结构与超参数并借助脚本快速搭建训练入口适合作为课程作业、竞赛练习或论文实验的基准数据。1. 天气分类数据集共9000个样本从零训练一个能用的天气识别模型手上有个天气分类数据集共9000个样本四个类别——多云、雨天、晴天、日出。这个量级不大不小刚好卡在一个尴尬的位置从头训练容易过拟合直接套用预训练模型又觉得杀鸡用牛刀。但实际工程里这恰恰是最常见的场景——你不可能每次都拿到百万级数据更多时候就是几千到一万张图要求你在有限资源下把准确率做到能上线的水平。这篇文章要解决的问题很具体拿到这个9000样本的天气分类数据集后怎么划分、怎么增强、选什么模型、参数怎么调、训练过程中怎么判断模型是不是在假学。适合有基本PyTorch基础、想快速跑通一个完整图像分类流程的读者。不涉及任何复杂部署目标就是让你在单卡上把验证集准确率做到90%以上并且知道每一步为什么这么做。2. 数据划分与增强9000样本怎么切才不翻车2.1 按类别分层划分别用随机切分9000个样本如果按7:2:1划分训练集6300、验证集1800、测试集900。听起来合理但如果你直接random.shuffle然后切片很可能出现某个类别在验证集里只有几十张的情况。天气分类四个类别理论上应该均衡但实际数据集中晴天和多云往往偏多日出可能偏少。正确做法是按类别分层抽样。用sklearn.model_selection.train_test_split的stratify参数保证每个子集的类别比例和原始一致。import os import shutil from sklearn.model_selection import train_test_split # 假设数据结构data/多云/*.jpg, data/雨天/*.jpg, ... data_dir data classes [cloudy, rainy, sunny, sunrise] all_files, all_labels [], [] for idx, cls in enumerate(classes): cls_dir os.path.join(data_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .png, .jpeg)): all_files.append(os.path.join(cls_dir, fname)) all_labels.append(idx) # 先切出测试集再从剩余切验证集 X_train, X_test, y_train, y_test train_test_split( all_files, all_labels, test_size0.1, stratifyall_labels, random_state42 ) X_train, X_val, y_train, y_val train_test_split( X_train, y_train, test_size0.2, stratifyy_train, random_state42 ) print(f训练集: {len(X_train)}, 验证集: {len(X_val)}, 测试集: {len(X_test)})这段代码的关键在stratifyall_labels它确保每个子集中四个类别的比例与原始数据集一致。random_state42是为了可复现换个数结果会不同但分布规律一致。注意先切测试集再切验证集避免测试集信息泄露到验证集。2.2 增强策略要匹配天气场景天气分类的图像增强不能照搬ImageNet那套。翻转、裁剪、颜色抖动都可以用但有几个细节水平翻转可以天气场景没有方向性垂直翻转不建议天空不会跑到下面颜色抖动慎用天气分类高度依赖颜色和亮度特征过度抖动会让晴天和多云混淆RandomResizedCrop可以用但scale范围别设太激进0.7到1.0比较稳from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])ColorJitter里hue只给了0.05因为色相变化太大会把日出的暖色调变成冷色调。Normalize用的是ImageNet统计量如果你用预训练模型就必须保持一致从头训练的话可以用数据集自身的均值和标准差但差别不大。提示增强只作用于训练集验证集和测试集只用ResizeCenterCropNormalize。这个坑每年都有人踩。3. 模型选型9000样本该用ResNet还是EfficientNet3.1 预训练权重几乎总是更好的起点9000样本从头训练一个ResNet-50训练集6300张每个epoch只能看到6300次样本。ResNet-50有2500万参数这个数据量根本喂不饱训练loss能降下去但验证loss很快反弹——典型的过拟合。用预训练权重是标准做法。ImageNet上预训练的模型已经学会了边缘、纹理、颜色分布等底层特征你只需要微调高层语义部分。实际测试中同样9000样本预训练ResNet-18比从头训练的ResNet-50验证准确率高8到12个百分点。import torch import torch.nn as nn from torchvision import models def build_model(num_classes4, model_nameresnet18, pretrainedTrue): if model_name resnet18: model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) elif model_name efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.DEFAULT if pretrained else None) in_features model.classifier[1].in_features model.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model替换分类头时加了Dropout(0.3)这是小数据集上的常规操作。ResNet18_Weights.DEFAULT是新版torchvision的写法旧版用pretrainedTrue效果一样但新写法更明确。3.2 冻结策略先冻后解分阶段微调直接全量微调不是不行但小数据集上容易把预训练学到的特征冲掉。更稳的做法是分两阶段第一阶段冻结backbone只训练分类头。学习率设1e-3跑5个epoch。这一步让随机初始化的分类头先收敛到一个合理位置不会产生大梯度回传破坏预训练特征。第二阶段解冻全部参数用更小的学习率1e-4微调。跑15到20个epoch配合余弦退火调度。def set_backbone_grad(model, requires_grad): for name, param in model.named_parameters(): if fc not in name and classifier not in name: param.requires_grad requires_grad # 阶段一 model build_model() set_backbone_grad(model, False) optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) # 阶段二训练5个epoch后 set_backbone_grad(model, True) optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20)filter(lambda p: p.requires_grad, ...)这行很重要否则优化器会把冻结参数也加进去虽然不更新但浪费显存。阶段切换时重新构建优化器因为参数组的requires_grad状态变了。3.3 批次大小与学习率的搭配9000样本batch_size设32或64都行。32的话一个epoch约197步64约98步。小数据集建议用32梯度更新更频繁收敛更稳。学习率和batch_size的关系batch_size翻倍学习率可以适当放大1.5到2倍但微调场景下不建议超过1e-3。我一般用1e-4起步如果loss下降太慢再调到3e-4。4. 训练循环与验证怎么判断模型是不是在假学4.1 训练循环里必须记录的东西很多人训练时只看loss这是不够的。至少记录训练loss、训练准确率、验证loss、验证准确率、当前学习率。这四个指标能告诉你模型是在真学还是假学。import torch from torch.utils.data import DataLoader, Dataset from PIL import Image class WeatherDataset(Dataset): def __init__(self, file_list, label_list, transformNone): self.file_list file_list self.label_list label_list self.transform transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): img Image.open(self.file_list[idx]).convert(RGB) label self.label_list[idx] if self.transform: img self.transform(img) return img, label def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / totalmodel.train()和model.eval()必须成对出现前者启用Dropout和BatchNorm的更新后者固定它们。torch.no_grad()在验证时关闭梯度计算省显存也加速。4.2 过拟合与欠拟合的判断信号训练loss降、验证loss也降正常学习训练loss降、验证loss先降后升过拟合需要加正则或早停训练loss不降、验证loss不降欠拟合学习率太小或模型容量不够训练准确率很高、验证准确率很低且差距持续扩大严重过拟合9000样本用预训练ResNet-18正常情况下第3到5个epoch验证准确率就能到85%以上10个epoch内到90%。如果20个epoch还在80%徘徊检查数据标签有没有错、增强是不是太狠、学习率是不是设错了。注意验证集准确率波动2%以内是正常的别看到一次下降就调参。看趋势不看单点。4.3 早停与模型保存best_acc 0.0 patience 7 counter 0 for epoch in range(25): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1}: train_loss{train_loss:.4f} train_acc{train_acc:.4f} fval_loss{val_loss:.4f} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(fEarly stopping at epoch {epoch1}) breakpatience7意味着验证准确率连续7个epoch没提升就停。这个值别设太小余弦退火过程中准确率可能先平后升。保存的是state_dict()而不是整个模型加载时先构建模型结构再load_state_dict。5. 避坑与排查9000样本训练中最容易翻车的5个地方5.1 验证准确率远高于测试准确率现象验证集92%测试集只有78%。原因验证集和测试集划分时没有分层或者验证集被重复用于调参导致信息泄露。更隐蔽的原因是数据集中存在同一场景的连拍图片随机划分时相似图片同时进入训练和验证集。解决按类别分层划分并且检查是否有重复或高度相似的图片。可以用感知哈希去重或者按拍摄时间/地点分组划分。如果数据集里同一场景有多张图用GroupShuffleSplit而不是普通train_test_split。5.2 训练loss正常但验证loss是NaN现象前几个epoch正常突然验证loss变成nan。原因验证集中有损坏图片或者某张图片尺寸异常导致预处理出错。也可能是学习率太大导致权重爆炸。解决在Dataset的__getitem__里加try-except跳过无法读取的图片。同时用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)做梯度裁剪。检查学习率是否超过1e-3。5.3 某个类别准确率特别低现象多云、雨天、晴天都在90%以上日出只有60%。原因日出样本量太少或者日出和多云在视觉上确实难分。也可能是标注错误把日出标成了多云。解决先看混淆矩阵确认是跟哪个类别混淆。如果是样本量问题对少数类做过采样或加类别权重。如果是标注问题人工抽查几十张。nn.CrossEntropyLoss(weightclass_weights)可以给少数类更高权重。5.4 模型在验证集上表现好但实际用起来很差现象验证集90%但拿手机拍的照片测试准确率掉到60%。原因训练数据都是专业相机拍摄的高质量图片手机照片的色调、分辨率、噪点分布完全不同。这是域偏移问题。解决在增强里加入模拟手机拍摄的变换——降低分辨率、加高斯噪声、调整白平衡。或者收集少量手机拍摄的图片做微调。没有这个条件的话至少把Resize的插值方式从默认的bilinear改成bicubic对低质量图片更友好。5.5 训练速度慢得离谱现象一个epoch要跑十几分钟。原因num_workers设成了0数据加载是单线程的。或者图片分辨率太大224的输入实际读的是4000x3000的原图。解决DataLoader设num_workers4或8pin_memoryTrue。在Dataset里先做一次离线Resize把原图缩到256x256存下来训练时直接读小图。这个操作能把训练速度提升3到5倍。train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue )drop_lastTrue在训练时丢弃最后一个不满batch的数据避免BatchNorm在只有一两张图时统计量不准。6. 进阶技巧用混淆矩阵和Grad-CAM定位模型到底在看哪里训练完模型准确率90%以上但你真的知道模型在学什么吗我习惯做两件事画混淆矩阵跑Grad-CAM。混淆矩阵能告诉你类别之间的混淆模式。比如多云和雨天互相混淆多说明模型对云层厚度和亮度的区分不够敏感。这时候可以针对性加强这两类的数据增强或者引入注意力机制。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) outputs model(imgs) all_preds.extend(outputs.argmax(1).cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclasses, yticklabelsclasses) plt.savefig(confusion_matrix.png) print(classification_report(all_labels, all_preds, target_namesclasses))Grad-CAM更直观它把模型最后卷积层的梯度回传到特征图上生成热力图叠加在原图上。如果模型判断晴天时关注的是天空区域说明学对了如果关注的是图片角落的水印那就翻车了。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layers [model.layer4[-1]] # ResNet18的最后一个卷积块 cam GradCAM(modelmodel, target_layerstarget_layers) grayscale_cam cam(input_tensorimg_tensor.unsqueeze(0)) visualization show_cam_on_image(img_np, grayscale_cam[0], use_rgbTrue)跑完Grad-CAM后我发现一个血泪教训模型有时候会依赖图片的亮度均值来判断类别而不是真正的天气特征。这意味着如果测试图片整体偏暗模型会倾向预测雨天。解决办法是在增强里加入亮度归一化或者用更激进的ColorJitter让模型学会忽略全局亮度。最后一个习惯每次训练完把验证集里预测错误的图片单独存一个文件夹肉眼过一遍。十次里有八次能发现标注错误或者数据质量问题。这个动作花不了几分钟但比调参管用得多。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

GitHub热榜背后的动态技术趋势分析方法

GitHub热榜背后的动态技术趋势分析方法

1. 这份“GitHub周榜”到底在追踪什么,又为什么值得你每天点开?很多人第一次看到“GitHub 热榜项目:周榜(2026-10-04)”这个标题,下意识会以为是某个具体开源项目的名称——比如某款新发布的AI工具、一个前…

2026/10/11 5:22:40 阅读更多 →
Uni-app项目架构进阶:从分层设计到性能优化的实战指南

Uni-app项目架构进阶:从分层设计到性能优化的实战指南

1. 为什么大多数Uni-app项目会越改越乱:先打破"能跑就行"的思维定式先说个我自己的观察。这几年看过不少Uni-app项目,也接手过几个半路烂尾的。开发头两个月,大家普遍觉得真香——一套代码能跑小程序、App、H5,团队不用…

2026/10/11 5:22:40 阅读更多 →
390亿美元,全球具身智能顶流Figure的三重泡沫

390亿美元,全球具身智能顶流Figure的三重泡沫

文 | 周易编 | 沈校这家加州总部占地30万平方英尺的全球机器人明星,10月份被福布斯一篇《Figure’s Brett Adcock Sold Investors A Humanoid Future. Now He Has To Build It》,揭开了这家企业的估值泡沫。大致可以总结为三个层面,第一层是营…

2026/10/11 5:21:40 阅读更多 →

最新新闻

Ionic Icon 图标完全指南:从基础用法到实战技巧

Ionic Icon 图标完全指南:从基础用法到实战技巧

1. 引言在移动应用和 Web 应用开发中,图标是界面设计中不可或缺的元素。Ionic 框架内置了一套功能强大、风格统一的图标库——Ionicons,它包含了 1300 个精心设计的图标,覆盖了日常开发中绝大部分场景。本文将带你全面了解 Ionic Icon 的使用…

2026/10/11 7:03:35 阅读更多 →
AnyPS5:面向PS5原生ELF的Linux系统调用语义桥接技术

AnyPS5:面向PS5原生ELF的Linux系统调用语义桥接技术

1. “AnyPS5”不是产品代号,而是开发者社区里一个隐秘的共识性称呼最近在几个硬核技术论坛和跨平台开发群组里,“AnyPS5”这个词频繁出现在讨论帖标题和代码注释中。它既不是索尼官方发布的SDK名称,也不是某款第三方工具的注册商标&#xff0…

2026/10/11 7:03:35 阅读更多 →
快鹭办公与蓝信、360织语、Worktile对比:支持移动端多端协同的一站式AI办公平台怎么选

快鹭办公与蓝信、360织语、Worktile对比:支持移动端多端协同的一站式AI办公平台怎么选

当企业要在支持移动端和多端协同的一站式 AI 智能办公平台之间做选择时,同一口径下值得对比的是快鹭办公、蓝信、360织语、Worktile、信源密信与 360纳米Work 六类平台。差异集中在协同基座与安全等级、审批与应用搭建、系统集成、AI 能力定位,以及部署方…

2026/10/11 7:03:35 阅读更多 →
各行业优化技术实力强口碑佳订单提升快源头厂家清单--一网推

各行业优化技术实力强口碑佳订单提升快源头厂家清单--一网推

当前苏州全域(含昆山、张家港、常熟、太仓)及无锡、嘉兴、湖州等周边城市的企业数字优化需求持续上涨,不少企业因对GEO、SEO、AEO概念认知模糊、选错服务商导致投入产出比偏低,行业普遍存在报价不透明、交付无标准、效果难溯源三大…

2026/10/11 7:03:35 阅读更多 →
GitHub热榜刷榜方法论:从看到读,洞悉技术风向

GitHub热榜刷榜方法论:从看到读,洞悉技术风向

1. 为什么每天刷热榜,大多数人都白刷了GitHub 热榜日榜这个东西,我刷了整整五年多。每天打开排行榜扫一眼,看到眼熟的项目点进去看看 Star 涨了多少,偶尔收藏几个"看起来有用"的仓库,然后关掉页面——这是绝…

2026/10/11 7:03:35 阅读更多 →
基于YOLO的焊缝缺陷检测毕设方案:数据集、训练与推理全流程拆解

基于YOLO的焊缝缺陷检测毕设方案:数据集、训练与推理全流程拆解

简介:这份资源面向深度学习与计算机视觉方向的毕业设计、课程设计及期末大作业需求者,聚焦工业焊缝缺陷的自动识别与定位问题。方案以YOLO算法为核心,将目标检测转化为回归任务,实现对裂纹、气孔、未熔合、未焊透等缺陷的快速预测…

2026/10/11 7:02:35 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/10 10:38:42 阅读更多 →