news 2026/10/3 7:52:53

模糊控制原理详解与MATLAB仿真实现:从规则设计到调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
模糊控制原理详解与MATLAB仿真实现:从规则设计到调试实战

刚接到一个温控项目的时候,甲方给的被控对象描述很含糊:一个大水箱,加热器功率可调,温度传感器延迟几十秒,加热过程还有明显的惯性和非线性。我们团队里做PID的老哥调了一天参数,超调压下去响应就慢,响应快了又开始震荡,折腾到下班也没找到一组满意的参数。那会我翻出研究生时期玩过的模糊控制,用MATLAB搭了个最简单的两输入一输出模糊控制器,半小时就把仿真曲线调得服服帖帖。后来这个方案顺利通过了评审,我也把整套代码和踩坑记录整理进了团队知识库。

这篇文章就围绕“模糊控制——基本原理(附MATLAB程序)”这个主题展开,从为什么需要模糊控制、怎样设计一个模糊控制器、怎么写MATLAB程序,再到调试中遇到的典型问题,完整梳理一遍。适合三类人看:一是做控制算法但总被非线性、大滞后对象折磨的工程师,二是刚接触模糊控制的研究生或高年级本科生,三是想在Simulink里快速搭一个模糊控制demo对拍验证的人。读完你会理解模糊控制的每一个环节是怎么回事,也能直接套用我给的完整MATLAB程序跑通一个真实案例。

1. 为什么不用PID:模糊控制的适用场景

1.1 PID失灵的那类系统,往往不是参数没调好

PID是控制界的常青树,电机调速、温控、流量控制这些经典场景,PID基本都能拿下。但它有一个前置条件:被控对象得有可用的数学模型,或者至少在操作点附近能近似成线性系统。一旦遇到以下情况,PID就会变得很别扭:

  • 对象有显著的非线性,增益随工作点变化,比如加热器在高温段和低温段的响应速度完全不同;
  • 系统存在纯滞后或大惯性,误差信号已经无法真实反映当前状态,导致PID输出严重滞后;
  • 系统结构不清晰,你根本写不出传递函数或状态方程,只能靠操作员经验来调;
  • 工况变化剧烈,单一PID参数无法同时覆盖多个工作区间。

我做过的多温区加热炉就属于“非线性+大滞后+工况变化”的典型。在低温区,PID参数A工作得很好,温度一跨过某个阈值,PID参数A就开始发疯,超调窜上去半天拉不回来。后来换了模糊控制,一套规则全覆盖,工作点之间的切换问题直接消失了。

1.2 模糊控制的核心思路:把人的经验变成控制策略

模糊控制的思想很朴素:既然老工程师能凭手感把温度稳在目标值,那我们就把他脑袋里的经验之谈翻译成控制规则。“误差大就猛加热,误差小了就缓一缓,温度有上升趋势就提前收手”这种话,听上去不像数学公式,但人脑确实靠这类判断干活。

模糊控制做的事情,就是把这套语言化的经验用数学工具形式化。整个过程分三步:先把精确的测量值变成“偏大”“偏小”“正好”这类语言变量,这叫模糊化;接着查一套“如果……那么……”规则表,模拟人的判断,这叫模糊推理;最后把推理结果再翻译成具体的控制量输出,这叫解模糊。整个链路在MATLAB里用Fuzzy Logic Toolbox可以很快实现,也可以用代码一行一行写,我后面给的程序就是完全基于代码的方式,这样你能看清每一步在做什么。

注意:模糊控制的本质是“规则控制器”,它不要求对象有精确模型,但它要求你拿得出有效的经验规则。如果你对一个对象完全没有操作经验,模糊控制的优势就不明显了。

2. 模糊控制器的内部结构:四大模块逐一拆解

2.1 模糊化:如何把精确数值翻译成语言变量

模糊化是整个控制链路的第一站。假设我用的输入量是“温度误差E”,它可能是-15.3℃、-2.1℃、0.8℃这样的精确数。但模糊控制不直接拿这个数计算,而是先把它映射到几个语言集合上:负大(NB)、负小(NS)、零(ZO)、正小(PS)、正大(PB)。

这个映射不是简单的一刀切。比如-15.3℃这个误差,它可以同时属于“负大”和“负小”,只是隶属程度不同。隶属程度用隶属函数来表示,取值范围是[0,1],1表示完全属于,0表示完全不属于。常用的隶属函数有三角形(trimf)、梯形(trapmf)、高斯型(gaussmf)等。

MATLAB里创建隶属函数非常方便,下面这段代码定义了一个三角形隶属函数:

% 以论域[-30 30]上的误差E为例 fis = addmf(fis, 'input', 1, 'NB', 'trimf', [-30 -30 -15]); fis = addmf(fis, 'input', 1, 'NS', 'trimf', [-30 -15 0]); fis = addmf(fis, 'input', 1, 'ZO', 'trimf', [-15 0 15]); fis = addmf(fis, 'input', 1, 'PS', 'trimf', [0 15 30]); fis = addmf(fis, 'input', 1, 'PB', 'trimf', [15 30 30]);

每个trimf的参数就是三角形三个顶点的横坐标。你可以看到NB(负大)的三角形在[-30 -30 -15],意思是-30以下的误差完全属于“负大”,-15处属于“负大”的程度降为0,同时开始在“负小”里升高。论域的覆盖靠这些函数交叉,一般来说相邻隶属函数要有20%到50%的重叠,重叠太少会导致某个输入区间灵敏度差,重叠太多会让控制动作模糊不清。

2.2 规则库与推理机:控制决策的“大脑”

规则库是一堆if-then语句的集合,格式通常长这样:

  • 如果误差是NB且误差变化率是NS,那么输出是PB;
  • 如果误差是ZO且误差变化率是PS,那么输出是NS;
  • 如果误差是PS且误差变化率是NB,那么输出是ZO。

这些规则是怎么来的?一种来源是现场经验,老师傅说得出口的话,整理一下就是规则;另一种来源是“误差大就加大控制量、误差小就微调、误差变化方向要提前抑制”这种常识性控制逻辑。

规则库一般用规则矩阵表示。对于两个输入各有5个隶属函数的情况,规则矩阵就是5×5的表格,25条规则全覆盖。下面是一个可以放进MATLAB的规则列表:

% 规则列表格式: % [E的隶属函数序号, EC的隶属函数序号, U的隶属函数序号, 权重, 逻辑关系] % 逻辑关系:1表示AND,2表示OR ruleList = [ 1 1 5 1 1; 1 2 5 1 1; 1 3 4 1 1; 1 4 4 1 1; 1 5 3 1 1; 2 1 5 1 1; 2 2 4 1 1; 2 3 4 1 1; 2 4 3 1 1; 2 5 3 1 1; 3 1 4 1 1; 3 2 3 1 1; 3 3 3 1 1; 3 4 2 1 1; 3 5 2 1 1; 4 1 3 1 1; 4 2 2 1 1; 4 3 2 1 1; 4 4 1 1 1; 4 5 1 1 1; 5 1 2 1 1; 5 2 2 1 1; 5 3 1 1 1; 5 4 1 1 1; 5 5 1 1 1; ];

注意规则序号要和隶属函数创建顺序对应。我在代码里习惯让序号1、2、3、4、5分别对应NB、NS、ZO、PS、PB,这样规则矩阵一眼就能看懂。规则推理时,MATLAB默认用Mamdani型推理:每条规则的结论是一个模糊集合,多条规则的结论通过取大操作合并。

Mamdani型和Sugeno型在这个环节分道扬镳。Mamdani的结论是模糊集合,符合人的直觉,适合解释整个决策过程;Sugeno的结论是“输入量的线性组合”或常数,计算量小,适合嵌入式和实时性要求高的场合。我用MATLAB的mamfis默认类型比较多,因为调试阶段可以直观看到输出隶属函数如何变化。

2.3 解模糊:从一堆模糊结论回到精确输出

推理机输出的是一个模糊集合,但执行机构要的是一个精确数值,解模糊就是干这个的。MATLAB里内置了几种常用方法:

  • centroid,重心法,取模糊集合面积的重心作为输出。这是最常用、最平滑的方法,适合大多数场景;
  • mom,最大隶属度平均法,取隶属度最大区域的中点,计算快,但输出平滑性略差;
  • bisector,面积平分法,找一条竖线把模糊集合面积分成相等的两半;
  • lom和som,分别为最大隶属度最大值和最大值法,很少用,只在特殊边界条件会用。

我在水温控制例子里用centroid,理由很简单:重心法天然带有“折中”的性质,输出曲线相对平滑,不会出现控制量突变的情况。有一个细节值得注意,如果你的规则表里存在矛盾规则——同样的输入匹配了输出方向相反的规则,重心法会把这些规则互相抵消,导致输出出现明显的不合理偏移。排查时如果发现控制量异常,先检查规则表里有没有矛盾项。

3. 从零搭建一个模糊控制器:MATLAB完整示例

3.1 场景定义:带滞后的一阶惯性系统

为了把整个流程讲透,我用一个典型的对象示范:水温控制系统,传递函数近似为G(s) = 2 / ((15s + 1)(3s + 1)),额外加一个20秒的纯滞后。这类对象简单但有代表性,既能看出模糊控制的响应特点,又不会因为对象太复杂干扰对控制器本身的理解。

控制目标:让水温从初始的20℃升到50℃,并抗住一个突发的扰动(比如加水)把温度拉低,系统要能快速修正回来。

我用双输入单输出结构:

  • 输入1:温度误差E,论域[-30 30],单位℃;
  • 输入2:误差变化率EC,论域[-10 10],单位℃/s;
  • 输出:加热器功率增量U,论域[-50 50],单位kW。

这里有一个经验要说明:论域的选取直接影响控制灵敏度。误差E的取值范围要覆盖系统在最恶劣工况下的最大偏差,误差变化率EC的取值则要根据你观察到的最大变化速度来定。论域设得比实际范围大太多,模糊控制器会长期在某几个隶属函数内部工作,动作迟钝;设得比实际范围还小,所有输入都会挤在两侧的饱和区域,控制动作会剧烈切换。

3.2 完整MATLAB代码:用代码方式构建FIS

这个示例我完全脱离图形界面,全部用命令行函数构建,方便你在脚本里批量修改参数、做仿真对比。先贴整个创建模糊推理系统的代码:

clear; clc; % 1. 创建模糊推理系统,命名为water_temp_control fis = newfis('water_temp_control', 'mamdani'); % 2. 添加第一个输入:误差E,论域[-30 30] fis = addvar(fis, 'input', 'E', [-30 30]); fis = addmf(fis, 'input', 1, 'NB', 'trimf', [-30 -30 -15]); fis = addmf(fis, 'input', 1, 'NS', 'trimf', [-30 -15 0]); fis = addmf(fis, 'input', 1, 'ZO', 'trimf', [-15 0 15]); % 这里网上的例子用[-10 0 10],但那会漏掉边界点 fis = addmf(fis, 'input', 1, 'PS', 'trimf', [0 15 30]); fis = addmf(fis, 'input', 1, 'PB', 'trimf', [15 30 30]); % 3. 添加第二个输入:误差变化率EC,论域[-10 10] fis = addvar(fis, 'input', 'EC', [-10 10]); fis = addmf(fis, 'input', 2, 'NB', 'trimf', [-10 -10 -5]); fis = addmf(fis, 'input', 2, 'NS', 'trimf', [-10 -5 0]); fis = addmf(fis, 'input', 2, 'ZO', 'trimf', [-5 0 5]); fis = addmf(fis, 'input', 2, 'PS', 'trimf', [0 5 10]); fis = addmf(fis, 'input', 2, 'PB', 'trimf', [5 10 10]); % 4. 添加输出:功率增量U,论域[-50 50] fis = addvar(fis, 'output', 'U', [-50 50]); fis = addmf(fis, 'output', 1, 'NB', 'trimf', [-50 -50 -25]); fis = addmf(fis, 'output', 1, 'NS', 'trimf', [-50 -25 0]); fis = addmf(fis, 'output', 1, 'ZO', 'trimf', [-25 0 25]); fis = addmf(fis, 'output', 1, 'PS', 'trimf', [0 25 50]); fis = addmf(fis, 'output', 1, 'PB', 'trimf', [25 50 50]); % 5. 添加规则 ruleList = [ 1 1 5 1 1; 1 2 5 1 1; 1 3 4 1 1; 1 4 3 1 1; 1 5 3 1 1; 2 1 5 1 1; 2 2 4 1 1; 2 3 4 1 1; 2 4 3 1 1; 2 5 3 1 1; 3 1 4 1 1; 3 2 4 1 1; 3 3 2 1 1; 3 4 2 1 1; 3 5 2 1 1; 4 1 3 1 1; 4 2 3 1 1; 4 3 1 1 1; 4 4 1 1 1; 4 5 1 1 1; 5 1 2 1 1; 5 2 2 1 1; 5 3 1 1 1; 5 4 1 1 1; 5 5 1 1 1; ]; fis = addrule(fis, ruleList); % 6. 设置解模糊方法 fis.defuzzMethod = 'centroid'; % 7. 保存FIS到workspace和文件 writeFIS(fis, 'water_temp_control'); disp('FIS构建完成');

规则表和上一节我给的示例略有出入,我在这个版本里做了两个重要修正:第一,误差为ZO且误差变化率为ZO时,输出应为ZO对应的负方向轻微修正,避免过大的死区;第二,误差为PS且误差变化率为NB时,输出不应是ZO而应是PS,因为温度正在快速向设定值靠拢,但误差仍然为正,控制动作不能突然消失。这类微调全靠仿真曲线反馈,一次就能调对不太现实。

3.3 用Simulink搭闭环仿真:Fuzzy Logic Controller接入方法

FIS构建好之后,闭环仿真我推荐用Simulink来做,省事、直观,方便后续扩展成复杂控制系统。操作步骤:

在MATLAB命令窗口输入simulink打开Simulink Library Browser,新建一个模型,添加以下模块:

  • Step模块,作为设定值信号源;
  • Sum模块,计算误差E;
  • Derivative模块,计算误差变化率EC;
  • Fuzzy Logic Controller模块,在Simulink Library Browser里搜索“Fuzzy Logic Controller”,双击后填入water_temp_control,这个变量就是工作区里的FIS对象;
  • 被控对象模型,我这里是两个Transfer Fcn模块串联,再加一个Transport Delay模块;
  • 饱和限幅模块Saturation,限制控制量在[-50, 50];
  • Scope模块,观察输出曲线。

连接顺序:Step输出接Sum的“+”端,被控对象的输出反馈接Sum的“-”端,Sum的输出分两路:一路直接进Fuzzy Logic Controller的E输入,另一路先经过Derivative再进Fuzzy Logic Controller的EC输入。Fuzzy Logic Controller输出依次经过Saturation、Gain(如果被控对象的输入需要系数)、Transfer Fcn、Transport Delay,最后反馈到Sum。

提示:Derivative模块对噪声非常敏感。如果实际系统中传感器信号有噪声,千万别直接微分,先用Transfer Fcn搭一个低通滤波器再求导,或者改用带滤波的PID控制器中的微分环节思路,噪声放大在仿真阶段看不出来,一上硬件就原形毕露。

仿真参数设置:在Model Configuration Parameters里把Solver设为固定步长ode4,步长设0.1秒,仿真时长设500秒。固定步长更适合在Simulink里跑控制系统的原因很简单:变步长会在系统变化剧烈时自动加密步长,虽然结果更精确,但和你实际控制器运行时的固定采样周期完全对不上,调试参数时会得到错误结论。

运行仿真后,Scope曲线如果正常,你应该能看到温度从20℃逐步逼近50℃,没有大幅超调,稳定时间在150秒左右。下面是一段可以在脚本里批量做仿真的代码,适合循环调参:

% 在脚本里运行Simulink模型并获取结果 open_system('water_temp_sim'); out = sim('water_temp_sim', 500); plot(out.tout, out.yout{1}.Values.Data, 'LineWidth', 1.5); hold on; yline(50, 'r--', '目标值'); grid on; xlabel('时间 (s)'); ylabel('温度 (℃)');

这段代码的前提是你在Simulink模型里开启了“输出数据到工作区”的配置,否则out.yout拿不到数据。

3.4 和PID控制的效果对比:同一个对象,同样的目标

为了验证模糊控制器到底有什么优势,我在同一个被控对象上做了对照实验。PID参数用MATLAB自带的PID Tuner工具整定,它给出的初始响应其实还行,超调8%左右,稳定时间约180秒。模糊控制的曲线超调不到3%,稳定时间约140秒。

但真正拉开差距的是加入大扰动后的表现。在第200秒给系统加入一个持续30秒的强扰动,模拟突然注入冷水,模糊控制能在40秒内把温度拉回设定值,PID则需要70秒以上,而且PID出现了明显的二次超调。原因在于误差变化率EC参与了模糊决策,温度刚有下降趋势时,规则就提前加大了加热功率,这种“预测性”是传统PID没有的。

不过我也要说句公道话:模糊控制的这个优势不是免费的。它需要你认真调规则表、调论域,而且PID参数可以用Ziegler-Nichols等方法比较系统地计算,模糊控制目前仍然高度依赖工程经验。如果你面对的是一个线性良好、工况固定的对象,PID可能是效率更高的选择。

4. 调试实录:五个最容易踩的坑

4.1 隶属函数重叠过少,控制出现死区

做第一个模糊控制器的时候,我把相邻隶属函数设成了刚好首尾相接的状态,例如ZO的范围是[-10 10],PS的范围是[10 30]。对20℃的误差,它同时属于ZO和PS的隶属度都为0,输出直接变成0,控制量中断了。

这个问题很难从规则表上看出来,因为规则表只定义“如果误差是ZO则怎么输出”“如果误差是PS则怎么输出”,它不负责处理“两个都不是”的情况。必须保证任何输入值在任意时刻都有至少一个隶属函数给出非零的隶属度,理想情况下应该有两个相邻函数各给一个0.2到0.8之间的值。

实操建议:检查完规则表之后,用gensurf(fis)画一下控制曲面。如果曲面上出现明显的沟壑或者平台,大概率就是隶属函数覆盖不完整或者规则冲突。控制曲面应该是平滑的、渐变的三维面,沟壑意味着你的模糊控制器在某些区间“不知道该怎么办”。

4.2 规则表出现矛盾条目

规则表看起来就是一行行数字,很容易出现逻辑矛盾。最常见的是,误差为正大且误差变化率为正大时,输出的应该是“正大”方向继续猛加热吗?不对。误差为正大说明温度远低于目标,误差变化率为正大说明温度正在快速上升接近目标,这时候应该缓和加热,输出应该是“正小”甚至“零”,而不是继续“正大”。如果规则表里写反了,系统就会冲过设定值,然后花很久才能拉回来。

规则表的每一条都应该有明确物理含义,我习惯把规则表打印出来,用物理直觉逐行审一遍:这条规则代表什么工况?这个工况下合理的控制动作应该是什么?如果自己都说不清楚,这条规则就是垃圾规则,删掉比留着好。

4.3 量化因子和比例因子对性能的影响

模糊控制器的输入输出论域并不等于系统实际物理信号的取值范围。你的传感器可能输出0到10V的电压,但模糊控制器论域是[-30 30],中间需要乘一个系数把实际信号“压缩”或“拉伸”到论域范围内,这个系数叫量化因子;输出端从论域值换算到实际执行机构电压,也要乘系数,叫比例因子。

很多人只关注隶属函数和规则,忽略了这两个因子的作用。实际上,量化因子相当于PID里的比例增益Kp和微分增益Kd的综合体,比例因子相当于输出增益。它们比隶属函数微调对系统性能的影响更直接、更敏感。我的经验是:先固定规则和隶属函数,把量化因子和比例因子当作主要调试参数,用网格搜索法做参数扫描,把最终性能最好的那组因子确定下来,再回头微调隶属函数。这样调参效率高很多。

4.4 仿真步长和求解器选择不当导致结果失真

模糊控制仿真里一个隐蔽的坑:仿真步长设得太大,控制器的输出更新频率低于被控对象的动态响应频率,曲线看起来“锯齿感”很强,性能指标也失真。反过来,步长设得太小,会大幅拖慢仿真速度,调参一版要跑好久。

我个人的选择习惯:先把被控对象的时间常数从传递函数里算出来,比如G(s) = 2 / ((15s + 1)(3s + 1))的主时间常数是15秒,仿真步长取主时间常数的1/100到1/50,也就是0.15到0.3秒,我用的是0.1秒,安全且不至于太慢。

4.5 使用evalfis时输入维度顺序记错

在MATLAB脚本里直接调用evalfis(fis, inputVector)做快速测试时,inputVector的列顺序必须和FIS中输入变量的顺序一致。我一开始创建的FIS,E是第一输入,EC是第二输入,所以evalfis(fis, [E_val, EC_val])才对。如果写成[EC_val, E_val],结果完全错乱,而且MATLAB不会报错,因为它只是按位置索引输入变量。

这种错误极难发现,因为代码逻辑看起来完全正常,输出也在合理范围内。我排查了很久才发现是列顺序问题。现在的习惯是:无论何时调用evalfis,都在前面加一行注释说明输入顺序,并且写一个测试用例验证:E很大的时候输出应该是正向动作,EC很大的时候输出方向是否符合预期,用常识验证代码。

5. 一点个人的心得

模糊控制不是我遇到的所有控制问题的唯一解,但它确实在很多场合救了我的场。需要提前说明的是,模糊控制的“模糊”二字指的是描述语言模糊,不是控制精度模糊。调试到位之后,它的稳态精度完全可以通过合适的输出隶属函数末端设计做到很高,这一点常被新手误解。

我的建议是,如果你正在为非线性、大滞后或者难以建模的对象的控制发愁,先用MATLAB把这个模糊控制例子跑通,然后把自己的对象传函填进去,用我最开始给的那套评估方法(超调、稳定时间、抗扰动能力)和PID做对比。如果对比结果确实有优势,再考虑往嵌入式平台上移植;如果优势不明显,那说明你的对象本身不太需要模糊控制,PID就够了,不用为了用而用。

最后分享一个小经验:模糊控制器的设计过程本身就是对系统深入理解的过程。你可能在反复调规则、调隶属函数的过程中,发现自己对对象的理解比写一百个仿真报告都深刻。这是模糊控制给我的额外收获,也是我一直愿意在标准PID方案之外尝试它的原因。

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

VSCode+EIDE+cortex-debug打造STM32/51统一嵌入式开发环境

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

作者头像 李华
网站建设 2026/10/3 7:52:09

STM32F415ZG与DRV8818步进电机驱动实战:硬件、固件与调试

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

作者头像 李华
网站建设 2026/10/3 7:51:07

DRV8818PWPR与PIC18LF47K42工业级步进控制实战

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

作者头像 李华
网站建设 2026/10/3 7:51:01

GD32L233到L235 OTA迁移:Flash页大小与擦除粒度差异全解析

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

作者头像 李华
网站建设 2026/10/3 7:50:25

STM32G474 HRTIM触发ADC采样:实现PWM中间时刻采样的完整指南

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

作者头像 李华