news 2026/9/7 12:46:17

Python嵌入式开发实战:从MicroPython到ESP32节点跑通

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python嵌入式开发实战:从MicroPython到ESP32节点跑通

聊到 Python 和嵌入式,我最常被问到的问题就是:“我能不能把 Python 直接烧进单片机里跑?”每次我都要反问一句:你先告诉我,你说的嵌入式,是 8 位单片机裸机,还是跑 Linux 的板卡?如果是 STM32 裸机工程、只有几 KB RAM 的老单片机,那我的答案是别用 Python,至少别全用;但如果你说的是 ESP32、树莓派 Pico,或者是更往上的嵌入式 Linux 边缘设备,那 Python 在当下不仅能用于嵌入式开发,在某些环节甚至比 C 还顺手。这篇不写给那些天天在寄存器里打滚的老工程师,而是写给被“Python 到底能不能做嵌入式”这个问题卡住的动手派:搞懂边界、看清生态,然后跟着我的代码把一个真实节点跑通,就够了。

1. 先回答那个最关键的问题:Python 到底算不算“能做”

1.1 嵌入式开发的“能力边界”,取决于你在哪一层

嵌入式开发不是一个单一赛道,说白了是一条从“裸金属”到“应用系统”的完整金字塔。你问我 Python 能不能做嵌入式,本质上得先搞清楚你站在金字塔的哪一层:

  • 最底层是单片机裸机开发,资源以 KB 计算,代码直接操作寄存器,中断响应按微秒计,这一层 Python 基本没戏;
  • 往上一层是带 RTOS 的 MCU 开发,比如 FreeRTOS 上面的任务调度,Python 可以跑,但要靠 MicroPython 这种精简解释器,且要接受实时性打折;
  • 再往上是嵌入式 Linux 或者应用处理器,比如树莓派、各种 ARM Cortex-A 板卡、边缘 AI 盒子,这一层跑的是完整操作系统,Python 可以用得毫无心理负担;
  • 最顶层其实已经接近“边缘计算”了,Python 的全家桶:numpy、opencv、onnxruntime 全都能上,这时候它甚至比 C 还高效。

我之前一度用 C 写了很久的传感器驱动,后来接了一个物联网网关项目,拿到手才发现客户要的是“能快速对接各种协议、隔三差五改业务逻辑”的东西。用 C 改一版固件要重新编译烧录,用 Python 环境直接热更新,改完重启进程就完事。那种“被工具解放”的感觉,才是 Python 在嵌入式里真正的价值所在。

1.2 三级阶梯:Python 能杀入的三种嵌入式工况

为了不让“能做”和“不能做”停留在空对空,我按硬件资源丰俭,把 Python 在嵌入式中的可行场景分成三档,你可以直接对号入座:

场景档次代表硬件Python 方案适合做什么
入门级 MCURP2040、ESP32-C3、STM32F4MicroPython / CircuitPython原型验证、传感器采集、IoT 演示、教学
中端 MCUESP32-S3、STM32H7、nRF52MicroPython + 外设库网关节点的业务逻辑、协议转换、小规模 AI 识别
嵌入式 Linux / MPU树莓派、瑞芯微、全志、Jetson原生 Python + C 扩展边缘计算、视觉、AI 推理、网关服务

这三档里,Python 最难啃的是第一档,因为硬件资源太紧;但即使是第一档,MicroPython 也已经能跑。你只要搞清楚了“这一层跑 Python 是为了快速验证,不是为了量产最终方案”,心态就不会炸。很多项目真正的量产固件是 C,但原型阶段用 MicroPython 把逻辑梳理清楚,反而能让后面的 C 代码少踩一半坑。

2. 生态与硬件全景:MicroPython 之外还藏着哪些玩法

2.1 MicroPython 和 CircuitPython:最主流的两条微控路线

如果你决定在 MCU 上用 Python,首先会撞见两个名字:MicroPython 和 CircuitPython。很多人以为它俩是一个东西,实际上一开始的侧重点就不一样。

MicroPython 是基于 Python 3 语法做的一个超精简解释器,目标是在资源受限的 MCU 上跑 Python。它保留了 Python 的列表、字典、闭包、类这些核心特性,但底层又靠近硬件,比如machine.Pinmachine.I2C这些模块直接对应寄存器操作。它对 ESP32、STM32、RP2040 这些主流芯片的支持非常稳,社区庞大,很多工业原型也是拿它验证的。我个人的观点是:如果你要正经做嵌入式项目,或者想兼顾“学 Python”和“学硬件”,选 MicroPython 更合适。

CircuitPython 则是 Adafruit 从 MicroPython 分叉出来的分支,定位更偏向创客教育和 USB 外设。它最大的特点是“即插即用”——把板子插上电脑,它就会挂载成一个 U 盘,你直接往里拖.py文件就运行了。如果你想给小朋友做一门硬件课,或者快速搞一个带屏幕、带传感器的小玩意,CircuitPython 上手几乎零门槛。但它的实时性、底层可定制性都不如 MicroPython,工业项目里很少见到它。

还有一类更少人提的方案是 Zerynth 这类商业工具,它允许你用类似 Python 的语法写代码,然后编译成 C 再烧进 MCU,严格说这不是解释型 Python,而是“Python 语法的编译器”。这类方案在物联网平台时代火过一阵,现在热度已经明显下滑,我觉得没必要一上来就学它,反而容易学乱。

2.2 从 MCU 到 MPU:Python 的战场到底有多大

MicroPython 解决了 MCU 上的 Python 问题,但 Python 在嵌入式里真正的大本营,其实是嵌入式 Linux。先别急着把“嵌入式”等同于“单片机”,嵌入式系统里大量设备跑的是精简版 Linux:路由器、工业 HMI、边缘网关、视频采集盒,全是 Linux 内核。这些设备性能足够跑 CPython 官方解释器,也就是说你在电脑上怎么用 Python,在这些板卡上就能怎么用,几乎不用学第二套玩法。

这就带来一个特别实际的分工:底层硬件驱动、启动流程、中断响应这种硬骨头,交给 C/C++;而应用逻辑、协议对接、用户交互、算法调度,用 Python 来写。很多商业产品的软件栈就是这样分层设计的,我在实际项目里也验证过这种混合架构非常耐折腾。举个例子,一个带屏幕的工业采集器,C 负责读 ADC 和 PWM 输出,Python 负责跑 UI 逻辑、MQTT 通信、配置解析,两边通过系统共享内存或者 socket 接口对接。你想想,如果是纯 C 工程,改一次协议解析逻辑得编译多长时间,而 Python 侧改完直接重启进程,效率完全是两个维度。

从硬件支持上看,树莓派、瑞芯微 RK 系列、全志 H 系列、NXP i.MX 系列,这些主控都能跑标准 Linux,Python 支持自然不成问题。更别说 Jetson 这类带 GPU/NPU 的边缘 AI 平台,Python 生态里的 PyTorch、ONNX Runtime 就是它的主战场。所以别只盯着单片机问“能不能”,把眼光放到整个嵌入式硬件全景上,Python 能站稳的位置比想象中大得多。

2.3 选板指南:按项目预算和需求快速锁定开发板

不少动手派买第一块板子时容易冲动,看哪个评测视频火就买哪个,结果吃灰。我给个简化选板逻辑,按你的真实需求来挑:

你的目标推荐板卡理由与注意点
只想低成本试试 MicroPythonESP32-C3 SuperMini / 合宙 ESP32-C3十几块钱左右,WiFi+蓝牙都有,性能跑 MicroPython 足够
想入门嵌入式又希望引脚丰富Raspberry Pi Pico / Pico WRP2040 双核,引脚多,资料多,MicroPython 官方支持很完善
想做一个 IoT 节点并兼顾性能ESP32-S3 DevKitC支持 WiFi+BLE,内存更大,能跑小型模型,也支持 OTA
想做 AI 视觉类项目Sipeed Maix 系列(K210)自带 KPU 可以跑视觉 AI,MicroPython 生态支持得早
想认真走嵌入式 Linux 路线树莓派 4B 或 RK3588 板能跑完整 Linux,Python 是“一等公民”

我的建议是:如果你纯新手,别一上来买顶级板卡。ESP32-C3 或者 Pico 足够你折腾一个月。把 MicroPython 跑通、把传感器读上来、把数据发到云端,这套流程走一遍之后,你自然知道自己下一步是补硬件还是补软件。

3. 工具链实操:VSCode + mpremote 把第一行代码跑起来

3.1 环境搭建与固件烧录

很多人觉得嵌入式开发就得装 Keil、装 IAR,其实现在 Python 玩硬件,一套 VSCode + mpremote 就够了。先做环境自检,Windows 上最容易踩的坑是python --version没输出——多半是安装时没勾选“Add Python to PATH”,或者是被微软商店的 Python 别名占了。我用过最稳的做法:去 python.org 下载安装包,注意勾选 Add to PATH,装完后重启终端再验证一下。

验证命令:

python --version pip --version

通过之后装两个工具,esptool 用于烧写乐鑫芯片固件,mpremote 是 MicroPython 官方的板卡交互工具,脚本上传、运行、复位都靠它:

pip install esptool mpremote

以 ESP32-S3 为例,先去 MicroPython 官网下载对应固件,文件一般是.bin格式。擦除旧固件和烧录:

# 把板子用 USB 连电脑,查到串口号 esptool.py --chip esp32s3 --port COM3 erase_flash # 烧录 MicroPython 固件,注意地址从 0x0 开始(官方合并固件) esptool.py --chip esp32s3 --port COM3 write_flash -z 0x0 ESP32_GENERIC_S3-20240118-v1.22.2.bin

烧录完成后,直接用 mpremote 打开板子的 REPL:

mpremote connect COM3 repl

看到>>>提示符就说明 MicroPython 已经跑起来了,可以直接敲print("hello")试水。这一步做完,你实际上已经完成了“Python 在 MCU 上运行”的最小闭环。

3.2 项目组织与 REPL 调试习惯

我在本地跑 MicroPython 项目时,目录结构一般固定成这样:

my_project/ ├── boot.py # 上电初始化,配置 UART/网络 ├── main.py # 主程序入口 ├── lib/ # 第三方或自封装模块 │ ├── umqtt/ │ └── sensor_driver.py └── util/ # 本地辅助脚本

把板子当“小 Linux”来用,而不是把所有代码硬塞进一个文件。mpremote 的常用操作我贴在这里:

# 复位板子 mpremote connect COM3 reset # 上传单个文件到板子根目录 mpremote connect COM3 fs cp main.py : # 上传整个 lib 目录 mpremote connect COM3 fs cp -r lib : # 不烧入 flash,直接运行电脑上的脚本 mpremote connect COM3 run main.py # 进入 REPL mpremote connect COM3 repl

调试习惯上,我强烈建议你善用 REPL。比如程序跑崩了,别急着拔线重插,先按 Ctrl+C 回到 REPL,敲几行命令直接查看变量状态。很多 MicroPython 的问题在 REPL 里一调就明白了,比反复烧录快一个量级。

4. 一个完整示例:ESP32-S3 温湿度采集 + 定时上报

理论讲再多,不如跑一个完整节点。我做了一个 ESP32-S3 温湿度采集器,读取 DHT22,每 60 秒上报一次环境数据,支持通过 MQTT 发给本地服务器。你拿到手后改一下 WiFi 信息和 broker 地址,就能跑起来。

4.1 硬件连接与引脚规划

需要的东西就三样:ESP32-S3 开发板、DHT22 温湿度传感器模块、一根 USB 数据线。接线很简单:

DHT22 引脚接 ESP32-S3 引脚
VCC(或 +)3V3
GND(或 -)GND
DATA(或 S)GPIO4

DHT22 是单总线数字传感器,数据线最好接一个 4.7k 的上拉电阻到 VCC,不过很多现成模块已经集成上拉了,直接插杜邦线就能用。

4.2 main.py 实战代码拆解

整套代码的逻辑不复杂:开机先连 WiFi,再初始化和 MQTT 客户端,然后进入死循环,周期性读传感器并发布消息。我加了错误处理,避免传感器读取失败导致进程崩溃。

import time import network import machine from dht import DHT22 WIFI_SSID = "your_wifi" WIFI_PASSWORD = "your_password" MQTT_BROKER = "192.168.1.100" MQTT_TOPIC = b"dev/esp32s3/env" REPORT_INTERVAL = 60 # 上报间隔,单位秒 def connect_wifi(timeout=15): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print("connecting to wifi...") wlan.connect(WIFI_SSID, WIFI_PASSWORD) deadline = time.time() + timeout while not wlan.isconnected() and time.time() < deadline: time.sleep(0.5) if wlan.isconnected(): print("wifi ok, ip:", wlan.ifconfig()[0]) return wlan raise RuntimeError("wifi connect failed") def read_sensor(): dht = DHT22(machine.Pin(4)) dht.measure() return dht.temperature(), dht.humidity() def main(): wlan = connect_wifi() client = None try: from umqtt.simple import MQTTClient client = MQTTClient("esp32s3_env", MQTT_BROKER, keepalive=60) client.connect() print("mqtt connected:", MQTT_BROKER) except Exception as e: print("mqtt not available:", e) while True: try: temp, humi = read_sensor() msg = '{"temp": %s, "humidity": %s}' % (temp, humi) print("publish:", msg) if client: client.publish(MQTT_TOPIC, msg.encode()) except Exception as e: print("sensor read error:", e) time.sleep(REPORT_INTERVAL) if __name__ == "__main__": main()

如果你不想用 MQTT,想用 HTTP POST 到普通服务器,也可以只改上报那一段。关键是把“采集 + 上传”这个整体流程跑通,协议随便换。

4.3 把代码部署到板子并验证

保存成main.py,然后用 mpremote 传到板子根目录:

mpremote connect COM3 fs cp main.py : mpremote connect COM3 reset

重启后打开 REPL,你会看到类似输出:

connecting to wifi... wifi ok, ip: 192.168.1.32 mqtt connected: 192.168.1.100 publish: {"temp": 25.6, "humidity": 58.2}

看到这条日志,你的第一个 Python 嵌入式节点就跑通了。如果连不上 WiFi,先检查 SSID 和密码是不是写错,再看开发板天线附近有没有金属遮挡。如果 MQTT 连不上,先确认 broker 地址在电脑上能 ping 通。

5. 实战中的七个高频坑和对应解法

5.1 内存、性能与实时性:最容易被吐槽的三座大山

用 Python 做嵌入式,行业里最常见的吐槽就是“慢”和“吃内存”。这不是无理批评,MicroPython 在 ESP32-S3 上跑,可用的堆内存大概就一百多 KB,你要是把大量日志拼成超长字符串,很快会撞到MemoryError。我解决内存问题有三板斧:减少全局大对象、用gc.collect()在关键节点手动回收、尽量不往文件系统里高频写日志。尤其是最后一个,MicroPython 的 flash 是有擦写寿命的,我把日志改成内存环形缓冲区,只在异常时才落盘,实测板子稳定性明显提升。

性能问题的本质是 MicroPython 的解释执行 + 字节码开销。遇到对时序敏感的任务,比如 PWM 精细控制、高速 ADC 采样,别硬用 Python 写,要么换用硬件外设的底层模块,要么写 C 扩展,要么干脆把这类任务交给 C 固件处理。我的习惯是:10 微秒级以上的控制逻辑用 Python 写没问题,亚微秒级的时序想都别想。

实时性方面,MicroPython 的垃圾回收会导致不确定的暂停。如果你有强实时需求,我的建议是别把 Python 用在中断服务函数里。MicroPython 的中断回调不能长时间阻塞,否则系统直接重启。正确的姿势是中断里只置标志位,主循环里响应。这是我踩过最深的一个坑:在一个光电传感器的边缘触发中断里调用了休眠函数,结果一触发就宕机,最后查了两天才意识到是中断上下文的问题。

5.2 文件系统和硬件交互的隐性坑

除了内存和性能,实际硬件交互里还有几个容易忽略的点,我整理了一张问题速查表:

问题现象可能原因解决方案
python --version没输出Python 未加入 PATH / 商店别名冲突重装 Python,勾选 Add to PATH,或用py -V
串口连不上板子USB 驱动问题 / 端口选择错误设备管理器确认 COM 口,安装对应串口驱动
DHT22 读取偶尔失败传感器上拉电阻缺失 / 时序竞争检查模块是否集成上拉,换 GPIO,降低读取频率
MQTT 频繁断线网络不稳定 / keepalive 设置不当调大 MQTT 的 keepalive,并在主循环里检查 WiFi 状态
程序跑一段时间变慢内存碎片 / GC 触发频繁减少临时对象创建,动态模块改为静态分配
文件系统写入后数据丢失未正确卸载/断电写入关键配置写入后os.sync()确保落盘
上传文件后板子不执行文件名错误 / 入口不是 main.py确认根目录存在 main.py,且没有语法错误

有一条经验我觉得特别值得写出来:MicroPython 的引脚编号、ADC 通道编号,不同板子的映射不一样,不能想当然套用。同一个machine.Pin(4),在 ESP32 上是某个引脚,到了 ESP32-S3 上可能就不是预期那一路了。最好的办法是先查对应开发板的引脚图,然后连接板子后直接敲几个命令实测。REPL 在这种场景下比任何文档都好用。

6. 嵌入式工程师要不要因此重新评估 Rust 或其他方案

6.1 Python、C、Rust 在嵌入式里到底怎么共存

近两年 Rust 在嵌入式的声量很大,“用 Rust 重写系统软件”几乎是财富密码。但我觉得对动手派来说,工具选型要回到问题本身,而不是追热点。C 的优势是和硬件贴得最近,编译器成熟,芯片厂商的 SDK 几乎全是 C 的,短期内不可替代。Rust 的优势是内存安全和现代工具链,代价是学习曲线陡,很多芯片的底层外设库还不全。Python 的优势是开发效率和生态,代价是实时性和资源消耗。

三个语言不是非此即彼,我更愿意把它们看成同一项目的不同层次:硬件底层和驱动用 C,追求安全且性能要求高的中间层用 Rust,业务逻辑和工具链用 Python。我一个朋友的量产产品就是这套组合,MCU 里跑 C,主机侧跑 Python,中间用串口协议通信,两边开发节奏互不拖累。如果你觉得精力够,把 Rust 当第二语言是没有问题的;但若是刚入门,先把 Python 玩明白,再去碰 C 和 Rust,心态会从容很多。

6.2 AI 时代嵌入式开发方式的变化趋势

这两年明显感受到一个变化:AI 辅助开发已经深入嵌入式流程了。很多人开始用大模型辅助生成 MicroPython 脚本、解释串口日志、把 C 代码翻译成 Python 模块。我自己也试过让 AI 帮我从数据手册里提取寄存器配置,再对照生成 C 初始化的骨架,效率确实高不少,但前提是你得自己能看懂它改了什么。AI 时代,判断力和硬件 sense 反而更值钱,因为你判断不了 AI 生成的代码能不能上板子,那就只能当一个工具人。

另一个变化是端侧 AI 推理下沉。以前推理都是把数据传到服务器再做,现在很多板卡直接支持 Python 调用 NPU 跑模型。比如带 NPU 的瑞芯微芯片,在系统里直接跑 Python 的 RKNN 工具链,摄像头取流、模型推理、结果上报,全链路都能用 Python 串起来。这种趋势下,Python 在嵌入式生态里的位置不是变弱,而是更扎实了。

7. 写给动手派的学习路线:从点亮一颗灯到驾驭一个系统

7.1 第一个月:买什么、做什么、完成什么标准

如果你完全零基础,我的建议是别贪多。一块 ESP32-C3 加一个 DHT22 模块,总成本几十块钱,就够第一个月折腾了。给自己定三个里程碑:

第一周,把 MicroPython 固件烧进去,写一个点亮板载 LED 的小程序,理解Pin对象和time.sleep。这个阶段的目标不是写出多复杂的代码,而是把“代码 → 上传 → 运行”这条链路跑顺。第二周,接上 DHT22,学会从传感器读数据,理解时序、上拉电阻和引脚映射,解决读取失败的问题。第三周,接入 WiFi,把采集到的数据通过 MQTT 发到本地 broker,完成一个最简物联网节点。第四周,把前三周的东西整合成一个自动上报的小系统,并加上异常处理和手动重启逻辑。

一个月后你回头看,这个“小系统”其实已经覆盖了嵌入式开发一半以上的核心概念:GPIO、传感器时序、网络通信、协议解析、设备稳定性。这些东西学会之后,底层换成 C 也只是换工具而已,思路是相通的。

7.2 三个月后的进阶方向

跑通一个节点之后,很多人会陷在“Python 真方便”的舒适区里,我建议你及时跳出来。接下来按这个顺序进阶:

先补 C 语言和单片机基础,不用精通,但要能看懂芯片手册里的寄存器操作,理解 MicroPython 背后的machine模块封装了什么。然后学嵌入式 Linux,把树莓派或者瑞芯微板卡跑起来,在系统里装 Python、写 systemd 服务、设置开机自启,这是 Python 在嵌入式里的主场。再往后,挑一个你熟悉的传感器或者外设,尝试为 MicroPython 写一个 C 扩展模块,把核心计算放到底层执行,这时候你对“Python 调用底层”的理解就完整了。

这条路走下来,你回头看“Python 能做嵌入式开发吗”这个问题,大概率会换个问法:“我该用 Python 做哪部分嵌入式开发?”到那个时候,你已经在用工具,而不是被工具定义了。

补一个我最想说的个人心得:如果你是写应用出身的,学嵌入式别一上来就抱着芯片手册啃。先拿 Python 把整个链路跑通,从传感器到云端,这个过程能治好你 80% 的“底层恐惧”。等你对硬件真的有手感了,再决定要不要往下拆。这种方法适合大多数动手派,我自己就是靠这条路径,从脚本工程师一路做到嵌入式产品开发。祝你们顺利点亮第一颗灯,然后点亮的下一片灯海。

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

40个生产级行业数据模型落地指南:从表结构到数据质量

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

作者头像 李华
网站建设 2026/9/7 12:43:46

生产级行业数据模型落地评估:从建模到生产环境的完整指南

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

作者头像 李华
网站建设 2026/9/7 12:40:59

Hermes Agent Windows 本地部署全指南:从环境检查到模型接入与排错

在实际使用 AI Agent 时&#xff0c;很多人第一步遇到的往往不是提示词写不好&#xff0c;而是工具装不起来、模型连不上、Agent 跑不起来。Hermes Agent 是一个支持本地部署的 AI 智能体运行工具&#xff0c;围绕它的安装、配置和对接问题&#xff0c;社区里已经积累了大量讨论…

作者头像 李华
网站建设 2026/9/7 12:39:37

SNETCracker实战:Windows弱口令审计与多线程猜解全解析

简介&#xff1a;SNETCracker超级弱口令检查工具完整源码包面向Windows平台安全测试、运维审计及内网自查场景&#xff0c;是一款基于C#开发、需.NET Framework 4.0支持的弱口令审计工具。内置SSH、RDP、SMB、MySQL、SQLServer、Oracle、FTP、MongoDB、Memcached、PostgreSQL、…

作者头像 李华
网站建设 2026/9/7 12:38:59

FFmpeg音视频兼容性处理:从检测到转码的完整解决方案

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

作者头像 李华
网站建设 2026/9/7 12:37:45

蛋小黄拍灯闹钟评测:拍打感应交互与智能夜灯功能详解

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

作者头像 李华