news 2026/9/20 9:56:10

零基础学ESP32:SD卡读写与数据存储完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零基础学ESP32:SD卡读写与数据存储完全指南

零基础学ESP32,很多教程讲到点灯、连WiFi、读传感器就戛然而止。等你真拿它做个正经东西——比如做个温湿度记录仪、搞个小气象站、做个离线Web服务器,或者单纯想把传感器数据存下来回头分析,就会发现一个绕不开的问题:板子自带的Flash存储空间太紧张了。ESP32常见型号内置Flash一般是4MB,能放固件、能放几个网页文件,但你要持续记录数据、存日志、放音频资源,根本不够用。这时候外挂一张Micro SD卡,是最简单、最省心、性价比最高的扩容方案。一张几块钱的TF卡配上十几块的SD卡模块,就能让ESP32拥有从几十MB到几十GB的存储空间,读取写入全在代码层面搞定。

这篇文章就围绕“零基础学ESP32:读写SD卡”这个项目,从硬件选型、电路连接、开发环境配置,到用Arduino框架实际写代码,把创建文件、写入数据、读取内容、追加日志这些操作完整走一遍。还会把我踩过的坑——比如SD卡初始化失败、文件打不开、“SD卡显示没有文件”这类问题,整理成排查清单。适合刚接触ESP32的入门用户,也适合做过一些小项目但没碰过文件存储的开发者参考。

1. 整体设计与方案选型:为什么是SD卡,怎么选最合理

1.1 ESP32的存储痛点与SD卡的定位

先明确一个概念:ESP32的Flash和普通电脑硬盘不是一个东西。ESP32的4MB Flash主要用来存固件程序,虽然可以通过SPIFFS或LittleFS把剩余空间划分出来当文件系统用,但可用空间撑死也就1.5MB到2.5MB。存放几个HTML网页、一张图标图片还行,真正跑数据记录就捉襟见肘了。

举个例子,假如你要做一个温湿度记录仪,每隔5秒读一次DHT11,把时间、温度、湿度存成一行文本,一行大约30字节左右,一天下来就是50多KB,一个月接近1.6MB。用内置Flash能撑一个月,但数据一多还得考虑擦写寿命——Flash是有写入次数限制的,频繁擦写会缩短寿命,万一写坏了Bootloader,板子直接变砖。SD卡就完全没这个顾虑,Micro SD卡本来就是消费级存储设备,就算普通Class10卡,写入寿命和容量都远胜板载Flash。插一张16GB卡,按同样的频率记录,几年都写不满。

所以SD卡在ESP32生态里的定位很清晰:大容量、可插拔、离线存储。适合四类典型场景:传感器数据长期记录、离线Web服务器资源存放、音频文件的存储与播放、程序运行日志落盘。

1.2 SD卡通信方案对比:SPI模式还是SDMMC模式

ESP32和SD卡通信有两种方式,这也是很多新手第一次搞不清的地方。

第一种是SDMMC模式,又叫SDIO模式,走SD卡原生总线协议。速度最快,4-bit模式理论能跑到几十MB/s,但需要占据固定的引脚(CMD、CLK、D0到D3,对应GPIO15、14、2、4、12、13这组)。在Arduino框架下操作稍微偏底层,对新手不够友好,而且这些引脚通常还和Flash、PSRAM等功能有冲突,使用限制比较多。

第二种是SPI模式,把SD卡挂到SPI总线上,用标准SPI协议通信。速度虽然比SDMMC慢,但实际读文件跑几百KB/s依然很轻松,对于记录数据、存读文本配置文件来说完全够用。SPI模式的引脚可以自由映射,接线的灵活度大得多,Arduino环境里的SD库对SPI模式支持得最完善,资料也多,出问题容易查。

零基础用户直接选SPI模式,别犹豫。

1.3 开发框架选型:Arduino还是ESP-IDF

目前主流的ESP32开发方式有两种:Arduino框架和ESP-IDF官方框架。

ESP-IDF是乐鑫官方的底层开发框架,功能最强、性能最好,但学习曲线陡峭,需要自己处理线程同步、事件循环、内存管理,连编译工程结构都需要时间适应。如果你是从零开始学,只看这一篇文章,我不建议直接用IDF,容易劝退。

Arduino框架是封装之后的高级语言环境,底层API被包装得极其简单。几十行代码就能完成文件读写,库覆盖率高,网上例程一搜一堆。这个项目选择Arduino框架,核心原因就是低门槛、快验证,把SD卡模块跑通之后再决定要不要深入底层,路线更合理。

2. 硬件连接与准备工作:一块模块、四根杜邦线

2.1 硬件清单与模块选购要点

要做这个实验,先准备以下几样东西:

  • ESP32开发板一块,常见的是ESP32 DevKitC或者NodeMCU-32S。
  • Micro SD卡模块一个,淘宝上搜“SD卡模块”基本都长一个样,带卡座和板载电平转换电路。
  • Micro SD卡一张,推荐Class10、容量16GB以下,优先选大品牌,兼容性更好。
  • 杜邦线若干,公对母的最实用。
  • 面包板一块,方便接线和临时调整。

关于模块选购,网上几块钱的模块和十几块钱的模块差异主要在元件用料和做工。最核心的是模块是否带电平转换电路。SD卡本身是3.3V设备,而很多ESP32开发板的引脚输出逻辑电平是3.3V,按理说不需要额外转换,但如果你用的是5V供电的Arduino Uno或者某些逻辑电平偏高的模块,就需要带转换电路。ESP32用模块时,认准3.3V逻辑兼容的就行,带不带透明转换芯片其实影响不大,但大牌的模块走线更规范,供电纹波控制得更好,SD卡长时间高负荷写入时稳定性差异就能体现出来。

2.2 ESP32与SD卡模块接线对照表

SPI模式下,SD卡模块有6个引脚:VCC、GND、CS、MOSI、MISO、SCK。ESP32的SPI总线上有默认的VSPI引脚,这套默认映射在Arduino的SD库下是直接能用的:

SD卡模块引脚ESP32引脚说明
VCC3V3模块供电,接3.3V,不要接5V
GNDGND共地
CSGPIO5片选信号,可换其他GPIO,需在代码中指定
MOSIGPIO23主设备输出,从设备输入
MISOGPIO19主设备输入,从设备输出
SCKGPIO18SPI时钟

接线逻辑不复杂:供电、共地、三根SPI数据线、一根片选线。片选的作用是告诉SD卡“我要跟你说话”,一个SPI总线上可以挂多个设备,靠CS来区分,所以CS引脚是可以自由选的,只要不和其他外设冲突就行。我习惯用GPIO5,因为默认SPI SS引脚就是它,代码里写起来也顺。

接线时注意顺序:先把ESP32断电,再接好杜邦线,最后上电。别带电插拔杜邦线,容易损坏引脚。

2.3 SD卡格式化:为什么一定要用FAT32

SD卡不是插上就能用的,ESP32的SD库默认只认FAT32文件系统。很多人的卡是出厂格式化成exFAT或者NTFS,直接插上去初始化会失败,报错“Card Mount Failed”。

格式化操作很简单:电脑上把SD卡插入读卡器,右键格式化,文件系统选FAT32,分配单元大小选默认即可。Windows 10/11系统能直接格式化32GB以内的卡为FAT32,超过32GB的卡在资源管理器里没有FAT32选项,可以用第三方工具如Rufus、DiskGenius来处理,或者直接用命令行工具format在管理员模式下执行:format E: /FS:FAT32(E换成你实际的盘符)。

还有一个建议:新卡到手,先放进手机或者相机里拍一张照片再拿出来接ESP32,确认卡本身读写正常。这样可以排除“卡是坏的”这个最尴尬的原因。

3. 开发环境搭建:Arduino IDE添加ESP32支持

3.1 安装ESP32开发板支持包

Arduino IDE可以从官网下载,或者用Arduino IDE 2.x版本,界面更现代,编译快。打开IDE后,需要添加ESP32的开发板支持包。这一步是很多人容易卡住的地方。

打开“文件→首选项”,在“附加开发板管理器网址”一栏填入官方地址:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

如果这个地址访问缓慢或者失败,可以换成国内镜像源,或者手动下载esp32的压缩包放进指定目录。之后在“工具→开发板→开发板管理器”里搜索“esp32”,找到Espressif Systems的官方包,点击安装。安装包比较大,整个下载可能需要十几分钟,取决于网络状况。

安装完成后,在“工具→开发板”菜单里能找到ESP32 Dev Module或ESP32 Wrover Module等选项,选择你手上板子对应的型号即可。串口选择方面,Windows下一般会识别为COM3之类的端口,设备管理器里能看到“USB-Enhanced-SERIAL CH340”之类的设备名。不同芯片的USB转串口芯片显示名称不同,但只要能看到新插入的COM口,选它就行。

3.2 验证环境:先跑一个点灯程序

环境配好后先别急着接SD卡,跑一个最简单的Blink程序验证工具链和驱动是否正常工作。选好板子、选好串口,把示例里的Blink程序上传,如果板载LED能闪烁,说明Arduino IDE、ESP32支持包、USB驱动全套通了。

这一步很容易被省略,但强烈建议不要跳。很多SD卡的问题排查到最后发现其实是编译环境有问题,先跑点灯相当于把环境的变量先控制住,后面再出问题就是硬件或代码的问题,定位起来快很多。

3.3 SD库的引入方式

Arduino环境下读写SD卡,需要用到内置的SD库。在IDE的“库管理器”里搜索“SD”,可以看到Arduino官方库。默认情况它可能已经随ESP32支持包一起装了,但建议手动检查一下版本,确保不为空。

这里有个隐藏细节:ESP32的SD库底层不是Arduino官方那套,而是基于乐鑫的ESP-IDF封装出来的,所以即使你在库管理器里看到的是标准SD库,它实际调用的接口和标准Arduino略有差异。比如SD.begin()接受一个片选引脚参数,而标准Arduino库对你指定的引脚要求不那么严格。这些差异在后面代码里我会逐个标注,照着写就没错。

4. 核心代码实现:从创建文件到读写进阶操作

4.1 SD库核心API通俗解析

在写完整代码之前,先把SD库最常用的几个API过一遍,理解它们的用途,后面看代码就不费劲了。

  • SD.begin(csPin):初始化SD卡和文件系统。csPin传入片选引脚号。返回true表示初始化成功,false表示失败。
  • SD.open(path, mode):打开或创建文件。path是文件路径,例如"/test.txt"。mode是打开模式,常见的是FILE_READ(只读)和FILE_WRITE(写,从头覆盖写入),还有FILE_APPEND(追加写入)。
  • file.print(data)/file.println(data):写入数据。println会在末尾自动加上换行符,适合记录日志。
  • file.write(buf, len):写入缓冲区数据,常用于写二进制数据或大块数据。
  • file.read()/file.available():读取文件。available()返回还有多少字节可以读,read()一次读一个字节并返回,读到末尾返回-1。
  • file.close():关闭文件,把缓冲区数据真正写入SD卡。这是我最强调的一步,忘了close,轻则数据丢失,重则损坏文件系统。
  • SD.exists(path):判断文件是否存在。
  • SD.mkdir(path):创建目录。
  • SD.remove(path):删除文件。
  • SD.rmdir(path):删除目录。

这些API记熟之后,绝大多数SD卡应用场景都能覆盖。

4.2 写文件完整示例:记录一条温湿度日志

我写一个最贴近实际应用的例子:模拟从传感器读数据,然后写入SD卡文件。这里用随机数代替真实传感器值,方便你直接跑通。

#include "FS.h" #include "SD.h" #include "SPI.h" // 定义SPI引脚的CS引脚 #define SD_CS 5 void setup() { Serial.begin(115200); delay(500); // 初始化SD卡 if (!SD.begin(SD_CS)) { Serial.println("SD Card Mount Failed"); return; } // 检查SD卡类型和容量(可选) uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { Serial.println("No SD card attached"); return; } Serial.println("SD Card initialized."); Serial.printf("Card Size: %llu MB\n", SD.cardSize() / (1024 * 1024)); // 生成模拟的温湿度数据 float temp = random(200, 350) / 10.0; // 20.0 ~ 35.0 摄氏度 float humi = random(400, 800) / 10.0; // 40.0 ~ 80.0 百分比 // 打开文件,FILE_WRITE 模式会从文件开头写入,覆盖原内容 File file = SD.open("/dht_log.txt", FILE_WRITE); if (!file) { Serial.println("Failed to open file for writing"); return; } // 写入数据 file.print("Temperature:"); file.print(temp); file.print(" C, Humidity:"); file.print(humi); file.println(" %"); // 关闭文件,重点!不close数据不会真正落盘 file.close(); Serial.println("Data written to SD card."); } void loop() { // 只写一次,loop里不做任何事 }

运行这个程序,串口监视器如果显示初始化成功和数据写入完成,就可以把SD卡拔下来插到电脑上,看到根目录下多了一个DHT_LOG.TXT文件,打开里面有一行刚才写入的数据。

这里有两个细节值得展开。第一,SD.open的路径写法,"/dht_log.txt"最前面的斜杠代表根目录,如果漏了可能打不开文件。第二,FILE_WRITE模式每次打开都是从头覆盖写,如果你想连续记录数据,得用FILE_APPEND模式追加写入,这就是下面要讲的进阶操作。

4.3 读文件完整示例:把数据读出来打印到串口

写完文件之后,自然要验证一下能不能把它读出来。读取文件的核心逻辑是:打开文件、检查是否打开成功、循环读取每个字节直到结束、关闭文件。

#include "FS.h" #include "SD.h" #include "SPI.h" #define SD_CS 5 void setup() { Serial.begin(115200); delay(500); if (!SD.begin(SD_CS)) { Serial.println("SD Card Mount Failed"); return; } File file = SD.open("/dht_log.txt", FILE_READ); if (!file) { Serial.println("Failed to open file for reading"); return; } // 逐字节读取文件内容并发送到串口 while (file.available()) { Serial.write(file.read()); } file.close(); } void loop() { }

file.available()返回的是剩余可读字节数,文件读完变成0,循环自然退出。用Serial.write(file.read())可以直接把字节发送到串口监视器,不会做任何字符转换,适合读文本文件。

如果文件内容比较多,逐字节读在速度上稍慢,但胜在逻辑简单、不易出错。有些教程里会有String content = file.readString();这种整段读取的写法,对于大文件会占用大量内存,ESP32虽然内存大,但还是要小心,普通日志场景其实用循环读取就够了。

4.4 进阶操作:追加写入、创建目录、遍历文件

实际项目里,极少只用覆盖写或只读一遍,更多的场景是把SD卡当成一个微型的文件管理器来用。这里补充几个高频操作。

追加写入日志:

File file = SD.open("/dht_log.txt", FILE_APPEND); if (file) { file.println("Append line: new record"); file.close(); }

创建目录:

if (SD.mkdir("/logs")) { Serial.println("Directory created"); }

遍历根目录下所有文件和子目录:

File root = SD.open("/"); File entry = root.openNextFile(); while (entry) { if (entry.isDirectory()) { Serial.printf("DIR : %s\n", entry.name()); } else { Serial.printf("FILE : %s SIZE: %zu\n", entry.name(), entry.size()); } entry = root.openNextFile(); } root.close();

有一段现实中的坑想提醒:FAT32文件系统对中文文件名的支持是有条件的。Arduino的SD库底层虽然可以显示中文名,但前提是文件名要符合8.3短文件名的规则——最多8个英文字符或数字做主名,点号后跟最多3个字符的扩展名。在代码里写中文文件名很容易乱码或打不开,建议一律用英文文件名。数据写入文件时,比如日志正文里带中文,在电脑上打开能看到能正常显示吗?这和编辑器编码有关,但为了方便,文本内容建议用英文,或者用UTF-8编码处理后再写入。

5. 常见问题排查与避坑实录:我踩过的那些坑

5.1 初始化失败:“Card Mount Failed”的常规排查

这个报错几乎是每个用SD卡的ESP32玩家都会遇到的,我先说原因,再给排查顺序。

第一个原因是接线错误。检查一遍:VCC是否接了3.3V而不是5V,CS是否接到了GPIO5,MOSI/MISO/SCK是否和代码里预期的一致。由于ESP32的SPI引脚可以重映射,很多人习惯把CS换成GPIO4或者GPIO2,如果换了,代码里的SD.begin()传入的引脚也必须跟着换。第二个原因是供电不足。有些开发板USB供电本来就不稳,插入SD卡后瞬间电流需求上升,板载LDO电压跌落,SD卡初始化就失败。解决办法:换一个供电能力更强的USB口,或者给开发板用5V/2A适配器供电。第三,卡不兼容或没格式化。我之前拿了一张给行车记录仪用过的老卡,插上去死活初始化不成功,换成新卡一次就过。FAT32格式化、不要用exFAT、NTFS,这是基本要求。

排查顺序建议:先Sdcard的接线供电,再重新格式化,再换一张不同的卡交叉测试。每换一次,都重新上下电,不要只点复位按钮,因为热复位时SD卡状态可能没有完全复位,容易偶发失败。

5.2 文件打不开:“SD卡显示没有文件”的真相

用户热搜词里有“SD卡显示没有文件”,出现这种情况,是卡插在电脑或者读卡器里看,根目录是空的,什么都没有。结合ESP32的项目场景,最常见的原因是代码里写入的文件并没有真正落盘就直接断电或者拔卡了。

我之前写过一段测试代码,打开文件、写入数据、注释掉file.close(),然后把程序里的delay(100)放在关闭文件之前,结果串口打印“write ok”,但把卡拔下来插电脑,文件显示0字节。原因很简单:SD库有缓冲区,写入的数据先存在内存里,只有调用close()flush()才会真正写信到卡里。所以无论在什么情况下,写完文件必须file.close(),如果长时间连续写入,还可以在临界点主动调用file.flush()强制落盘,防止意外断电丢数据。

还有一种“没有文件”的情况是文件名大小写问题。FAT32文件系统在Windows下不区分大小写,但ESP32的SD库在查找文件时是区分大小写的。你在电脑上看到的是TEST.TXT,代码里写SD.open("/test.txt")就可能打开失败。稳妥做法是统一用小写文件名。

5.3 中文乱码与字符编码问题

很多人第一次读到SD卡里的数据,发现串口监视器显示乱码,第一反应是文件系统坏了,其实不然。

串口乱码绝大多数是串口监视器编码和ESP32发送编码不匹配。Arduino IDE的串口监视器默认用UTF-8解码,如果代码里的字符串是UTF-8编码,显示就正常。如果你看到的是类似“涓?╁害”这样的乱码,说明编码对不上,或者字符串是从别处复制过来的GBK编码内容。解决方式:统一源码编码为UTF-8,串口监视器选择UTF-8,部分IDE版本没有编码选项,就建议在代码里避免直接输出中文,改用英文。

文件里存的中文乱码,又是另一种情况。FAT32文件系统不关心内容编码,只负责存字节。你在代码里以UTF-8写的中文,用Windows记事本打开时,如果Windows默认用ANSI解码就显示乱码。这种不是写入错误,而是打开方式的问题,换个支持UTF-8的编辑器(VS Code、Notepad++)就能正常显示。我自己做日志数据时,干脆直接全部存英文和数字,省掉很多麻烦。

5.4 运行中卡死、写入耗时过长的问题

有网友遇到过程序跑着跑着SD卡操作就卡住了,整个系统无响应。这个一般和三个因素有关。

第一,SPI通信速率过高。ESP32的SPI默认时钟可能是10MHz甚至20MHz,部分速度等级低的SD卡或劣质模块扛不住这么高的频率,通信失败导致卡死。解决方法是降低SPI时钟频率,在SD.begin()之后可以用SD.setFrequency(4000000)把频率降到4MHz甚至更低。牺牲点速度换稳定性,对数据记录类应用完全值得。第二,供电不稳。写入SD卡时电流波动大,如果供电线又细又长,压降严重会导致卡逻辑出错。建议供电线尽量短、粗,或者给模块单独加一个10uF左右的钽电容做储能。第三,文件系统碎片化。长时间反复删除、创建文件,SD卡的文件分配表会变得不稳定,偶尔会出现写入错误。这种只能靠定期格式化解决,或者写代码时避免频繁创建和删除小文件,尽量用一个大文件做日志滚动写入。

5.5 只能用SPI引脚的特殊情况和ESP32多外设协同

有一个场景值得单独拿出来说:如果你的项目同时要用SD卡和WiFi/蓝牙,会不会冲突?答案是基本不会冲突,因为SD卡走的是外设SPI总线,WiFi和蓝牙走的是内部射频模块,两者在硬件上不冲突。但理论上是这样,实际布局上如果SD卡模块的走线和天线靠太近,高速SPI信号可能对WiFi接收产生一些干扰。我做过一个项目,SD卡写入大文件的同时开着WiFi接收数据,实测吞吐量没有明显下降,但偶尔会有WiFi重传率上升的情况。解决方法是把SD卡模块放在远离天线的一端,或者降低SPI频率。

另一个常见问题是:ESP32的某些引脚是输入-only或者有特殊功能。比如GPIO34到GPIO39是仅输入引脚,不能作为输出控制CS引脚,很多开发板的GPIO1和GPIO3被UART占用。所以在选择CS引脚时,除了注意数值冲突,还要避开这些特殊引脚。我用的GPIO5是常用的安全选择。

6. 一些个人的使用体会

最后分享一点我自己的习惯。项目里只要用了SD卡,我会在程序启动时做一次完整的状态检测——打印卡类型、容量、可用空间,再判断关键目录是否存在,不存在就创建。这样程序跑到后面突然报错时,能快速定位是不是存储介质的问题。

另外,如果是做长期记录型设备,不要直接对着根目录写文件,我习惯把所有数据放在/data目录下,按日期生成子目录,比如/data/2025/06/15.log。这样做的好处是文件数量可控,查找方便,也减少了根目录文件碎片化。

还有个小技巧:SD卡也是会损坏的,尤其是频繁断电、拔插、低质量卡更是如此。重要数据不要只存在一份,有条件的话让ESP32定时用WiFi把日志推送到局域网服务器备份,SD卡当成离线兜底存储。这样即使卡坏了,数据也不至于全丢。

这篇文章里所有代码和接线方法,都是可以拿回去直接复现的。先跑通写文件,再跑通读文件,然后按自己的需求改成追加模式、目录结构、循环记录。SD卡这个外设在ESP32项目里的地位,不亚于你的U盘在电脑旁的地位,早用早省心。

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

2026压测工具测评盘点:13款主流开源与商业工具选型指南

2026年了,性能测试这个活儿在测试工程师的日常里占的权重越来越高,但每次聊到压测工具,总有人问“到底用哪款好”。市面上的压测工具少说有几十款,能经得住生产环境检验、团队愿意长期用的其实就那么十来个。这篇盘点不打算把13款…

作者头像 李华
网站建设 2026/9/20 9:52:19

B站弹幕抓取与词云生成全链路解析:从cid获取到TF-IDF加权词云

简介:本资源是一套面向高校计算机专业学生的Python课程设计实践项目,聚焦B站视频弹幕与评论数据的采集、清洗及可视化分析,解决短视频平台用户行为文本挖掘的教学与实践需求。压缩包共12个文件,含4个核心Python脚本(如…

作者头像 李华
网站建设 2026/9/20 9:49:50

从零吃透多层感知机MLP:原理、PyTorch实战与训练避坑指南

1. 为什么我劝你别跳过MLP直接上手Transformer这两年聊神经网络有个很有趣的现象:新人入门第一件事是跑通一个Transformer,好像不聊注意力机制就不算"搞深度学习的"。上一次出现这种风气是CNN火的时候,大家觉得会调几个卷积层就很厉…

作者头像 李华
网站建设 2026/9/20 9:48:59

从免费CRM到私有化部署:DeskcommCRM落地实践与避坑指南

做销售管理这几年,我印象最深的一个词是“客户不在系统里,就在抽屉里”。我们团队从七八个人扩张到二十多人,客户信息却还停留在Excel、微信群和个人通讯录里。每周五大家交周报,我经常看到同一个客户被两个人跟进,报价…

作者头像 李华
网站建设 2026/9/20 9:48:12

游戏监控覆盖层FPS显示N/A怎么办?从原理到排查修复全指南

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

作者头像 李华
网站建设 2026/9/20 9:47:27

Windows安装Git完整教程:避开PATH、换行符、SSH三大坑

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

作者头像 李华