news 2026/9/10 8:20:40

Python数据分析三剑客:Pandas、NumPy、Matplotlib核心用法与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python数据分析三剑客:Pandas、NumPy、Matplotlib核心用法与实战

我们总是习惯把几个库放在一起叫“三剑客”,但说实话,真正把它们当成一个整体来用,并且能在实际项目里想清楚“什么时候该用谁、怎么配合”的人,其实没有想象中那么多。新手阶段最常见的状态是:知道Pandas能读Excel,NumPy能算数组,Matplotlib能画图,可真拿到一批乱糟糟的数据,还是会卡在怎么组织代码、怎么排查报错、怎么把结果变成一张能用的图表上。这篇文章我会直接把Pandas、NumPy、Matplotlib拆开再拼起来,重点放在三个库各自主管的环节、相互之间的数据流转方式,以及三套我在实际场景里反复用的代码模板。适合刚学完Python基础、准备做数据分析和数据可视化的朋友,也适合已经在用但想理顺分析流程的读者。

1. 为什么Python数据分析绕不开这三件套

1.1 三剑客的分工定位与组合逻辑

很多初学者会问:Pandas已经能做筛选、分组、聚合了,为什么还要用NumPy?NumPy里的函数虽然能算均值、标准差,但Pandas不是也有mean()和std()吗?这两个问题恰好说明了三剑客的本质关系:它们不是三个互不相干的工具,而是一套有层级的数据处理体系。

NumPy解决的是底层数组计算问题。它的核心对象ndarray是一块连续内存上的同类型数值数组,所有运算都走编译好的C语言循环,所以速度远快于纯Python的for循环。Pandas的Series和DataFrame,底层很多地方都构建在NumPy数组之上,你可以把Pandas理解成“给NumPy数组穿上了带索引、带列名、带缺失值处理和SQL式操作的漂亮外套”。Matplotlib则是独立的绘图库,它不关心你的数据是怎么算出来的,只要你给它一组x和一组y,它就能画出图来。

三者的真正配合逻辑是:先用Pandas把数据进行读取、清洗、筛选、分组,满足了“数据是干净、整齐、符合分析需求”的条件;遇到需要高性能数值计算或者矩阵风格运算的时候,直接取出DataFrame里对应的NumPy数组,用np.mean、np.percentile、np.corrcoef这类函数快速出结果;最后把分析结果整理成两个一维数组(x和y)或者一张干净的统计表,再交给Matplotlib生成可视化的图表。这样每个工具都做自己最擅长的事情,代码的每一行都有明确目的,出了问题也容易定位。

1.2 什么场景下该用谁:一个判断思路

我见过太多人把Pandas当成万能工具,什么计算都往里塞,结果代码又慢又绕。自己也干过类似的事:有一回做时序数据的移动平均,我天真地用Pandas的apply加Python循环,一跑就是几分钟,换成NumPy的卷积操作后,几十毫秒就出结果了。

所以后来我给自己总结了一个简单的判断思路。拿到一个数据处理任务时,先问三个问题:第一,数据是表格形式的吗,有没有行索引和列名,需不需要做分组、合并、透视、清洗这些操作?如果答案是“是”,那主战场就是Pandas。第二,运算的核心是不是数值计算,比如求分位数、算相关系数、做标准化、矩阵乘法或者滑动窗口?如果数据量很大,或者计算逻辑有明显的数组运算特征,就要考虑把数据转成ndarray交给NumPy。第三,最终交付物是不是一张图?如果是,Matplotlib就是出口的那一步,剩下的问题只是图表的类型、坐标轴、颜色和标注。

这三个问题不冲突,一个完整的数据分析项目通常会把它们都用上。但关键是你在每一步都知道自己为什么用这个库,而不是打开一个库之后把所有操作全堆在里面,这样代码的可读性和运行效率都会好很多。

2. 核心API细节拆解:平时最容易踩坑的地方

2.1 NumPy:不仅是列表,更是数组运算基础

NumPy最基础、也是最重要的对象就是ndarray。创建它的方式很多,我最常用的是np.array()、np.zeros()、np.arange()这三个。np.array()适合把已有列表转成数组,np.zeros((3, 4))用于创建指定形状的全零矩阵,np.arange(10)生成0到9的一维序列。这些看起来很简单,但很多人栽在“数组形状”上。

比如你想要一个5行1列的列向量,写np.random.rand(5)得到的是形状为(5,)的一维数组,它既不是行向量也不是列向量,在线性代数运算里容易出问题。正确做法是改成np.random.rand(5, 1),形状才是(5, 1)。这种形状不匹配的问题在矩阵乘法里尤其常见,报错信息往往是“shapes (5,) and (5,) not aligned”。要彻底避免这个坑,可以养成立刻检查shape的习惯:每创建完一个数组,用print(arr.shape)看一眼。

广播机制也是NumPy一个很容易理解错的地方。它的规则可以简化成一句:两个数组从尾部维度开始对齐,如果某个维度长度相等或者其中一个长度为1,就能运算;否则就不能广播。举个实际例子,一个形状为(4, 3)的矩阵要减去每一列的平均值,可以直接用矩阵减去形状为(3,)的均值数组,因为(3,)会自动被认为是与列数对齐的维度。但如果想减去每一行的均值,就需要把形状为(4,)的数组先reshape成(4, 1),才能按行对齐。我见过很多人在这一步被坑过,所以遇到维数不一致的情况,第一反应不要猜,先用reshape或者np.newaxis把维度显式指定出来。

2.2 Pandas:Series与DataFrame的操作细节

Pandas的两种核心数据结构中,Series是一维带标签数组,DataFrame是二维表。实际项目里用DataFrame的次数更多,但Series经常出现在我们取出一列数据的场景中,所以它的特性也要理解清楚。

创建Series和DataFrame是练习的起点。Series可以通过字典创建,比如pd.Series({'a': 1, 'b': 2}),键会变成索引。DataFrame最常见的是从字典列表创建,比如pd.DataFrame([{'name':'张三','score':88}, {'name':'李四','score':92}]),每一行是一个字典,键是列名。还有非常实用的方式是从CSV读入,pd.read_csv('文件路径')。读入之后先别急着分析,先做三件事:df.head()看前几行,df.info()看每列类型和非空数量,df.describe()看数值列的统计摘要。这是数据清洗前的固定动作。

关于索引,普通整数索引是默认的,但Pandas真正强大的地方在于自定义索引和条件筛选。df[df['score'] > 90]是按条件筛选行,df.loc[df['score'] > 90, ['name', 'score']]是同时筛选行和列,df.iloc[0:5, 0:2]则是纯粹的按位置切片。很多人会把loc和iloc搞混,记住一个原则就行:loc用的是标签和布尔条件,iloc用的是从0开始的整数位置。

我特别想提醒的是Pandas的链式操作。有些代码写成df[df['score'] > 90]['name']这种连续取数据的方式,看起来简洁,但在某些版本会触发链式赋值的警告,尤其是当你准备对这个结果做修改的时候。更稳妥的做法是用loc一次性完成筛选和选列,避免链式操作的不确定性。

2.3 Matplotlib:绘图逻辑与中文显示问题

Matplotlib的绘图逻辑可以概括为“创建画布、创建坐标系、画图、展示”。很多教程直接教你plt.plot(x, y),但忽略了plt其实是在对“当前坐标系”操作。当一个脚本里画多张图,或者一张图里放多个子图的时候,这种隐式操作容易混乱。所以我更推荐显式的写法:fig, ax = plt.subplots(),然后所有绘制命令都通过ax来调。这样代码更清晰,也更接近大型项目的写法。

图表类型选择要结合数据和表达目的。折线图plt.plot适合观察趋势,柱状图ax.bar适合对比不同类别的数值,直方图ax.hist可以看单变量的分布,箱线图ax.boxplot能够展示数据的分散程度、离群点。热力图在相关矩阵分析中经常用到,可以通过ax.imshow或者plt.pcolor实现。还有散点图ax.scatter很适合观察两个变量之间的关系。一点个人经验:如果需要快速探索数据,直接画散点图和直方图就够了,箱线图在看到离群点时很好用。

中文显示是Matplotlib在国内使用者身上必然会遇到的问题。直接使用plt.title('订单数量')时,大概率会出现方框乱码。原因是Matplotlib默认字体里没有中文字形。解决办法是在画图前设置字体,一般这样写:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False

第一行指定中文字体,第二行让负号正常显示。如果你用的环境里既没有SimHei,也没有其他中文字体(比如在Linux服务器上),可以先查看系统有哪些中文字体,或者安装中文字体后再设置。我更推荐在Linux环境直接把字体名设置成系统已安装的字体,比如'Noto Sans CJK SC'或'WenQuanYi Zen Hei',这样更稳妥。

3. 场景实战:从清洗到分析再到可视化

3.1 用Pandas做数据清洗:空值、重复值与类型转换

数据清洗是数据分析中最花时间的一个环节。我拿一个电商订单表举例,假设数据里有订单编号、商品名称、销售金额、订单日期和客户ID五列,但原始数据并不干净,有空值、重复记录、金额里有“¥”符号,日期还被存成了字符串。

整个过程可以拆成四步。第一步是读取和观察,用pd.read_csv读入后,立刻用df.isnull().sum()看每列空值数量,用df.duplicated().sum()看重复行数量。第二步是处理空值,我判断的依据是空值占比和业务含义:客户ID缺失但订单号和金额都在,直接删除这条记录就能保证后续按客户分析时不算错;销售金额为空会影响汇总,也删掉;但有些列比如备注,空值可以保留,用fillna('')填成空字符串就行。第三步是去重,用df.drop_duplicates(subset=['订单编号'], keep='first'),这里的subset指定了按照哪个字段判断重复。第四步是类型转换,这是最容易踩坑的一步。

金额处理是最经典的例子。原始数据里是“¥198.00”这种字符串,直接astype(float)会报错,需要先把“¥”去掉,再转换。常见的处理办法有两种:一种是df['销售金额'] = df['销售金额'].str.replace('¥', '').astype(float);另一种是利用pandas的to_numeric,配合errors='coerce'参数,遇到无法转换的值变成NaN,之后再决定是删除还是填充。日期字段也一样,如果后续要计算时间差或者按月份汇总,就直接pd.to_datetime(df['订单日期']),这样Pandas才能进行时间运算。很多人做分析时发现日期排序不对,就是没有做这一步转换。

无论你选择哪种方式,数据清洗完成之后最后再做一次df.info(),确保每一列的类型已经变成float、int、datetime或者category,然后才能放心进入分析环节。这个过程我会反复强调,因为后续所有统计和可视化都建立在这些类型正确的前提之上。

3.2 用NumPy做统计分析与特征筛选

数据清洗完成后,分析阶段就轮到NumPy上场了。假设我们要分析订单金额的分布情况,可以先把Pandas的金额列转成NumPy数组,再调用numpy里的统计函数。

我在实际分析中常用的NumPy统计函数主要有这几个:np.mean求均值、np.median求中位数、np.percentile求分位数、np.std求标准差、np.sum求和、np.corrcoef求相关系数矩阵。比如要描述“销售金额”这个变量的整体水平,可以用一串代码一次性算出多个统计量:

amount = df['销售金额'].to_numpy() print('均值:', np.mean(amount)) print('中位数:', np.median(amount)) print('25%分位:', np.percentile(amount, 25)) print('75%分位:', np.percentile(amount, 75)) print('标准差:', np.std(amount)) print('总和:', np.sum(amount))

这里我特意使用.to_numpy()而不是.values,因为后者在Pandas的新版本里虽然还能用,但官方已经推荐前者,它给出的结果更干净,不会带有旧的索引信息。

如果要做特征筛选,一个常用角度是看不同数值特征之间的相关性。比如有一张表格,包含销售金额、客户年龄、购买次数、页面浏览时长等列。把相关数值列都转成二维数组后,用np.corrcoef配合行拼接,就能得到相关系数矩阵。再用np.argsort找到和某个目标特征相关性最强的几个特征,简化为Top N筛选。下面是个可以直接改着用的示例:

# 假设df里有这几列数值特征 cols = ['销售金额', '客户年龄', '购买次数', '页面浏览时长'] data = df[cols].to_numpy() corr_matrix = np.corrcoef(data, rowvar=False) # rowvar=False表示每一列是一个变量 print('相关系数矩阵:') print(corr_matrix)

相关系数矩阵会以二维数组形式展示,第1行第2列就是“销售金额”和“客户年龄”的相关系数。你想从数值上找最相关的特征,可以直接用np.argsort将某一行的相关性排序,取绝对值最大且不等于1的位置,那就是相关性最强的另一个特征。这一步看似使用Pandas也能实现,但数据量大一点之后,NumPy的向量化运算优势会非常明显,跑得非常快。

3.3 用Matplotlib输出一张能用于汇报的图表

统计结果出来后,就到了可视化环节。我举一个实际场景:假设我们要分析一周七天的订单量趋势,用Matplotlib画一张折线图,同时每个柱子显示具体数值,这样汇报起来更直观。先准备好数据,weekday列表存星期,order_count列表存每天的订单量,然后用subplots创建画布和坐标系,画一张柱状图并在顶部标注数值。

import matplotlib.pyplot as plt weekday = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] order_count = [156, 172, 168, 190, 205, 240, 230] fig, ax = plt.subplots(figsize=(8, 5)) bars = ax.bar(weekday, order_count, color='#4C72B0') for bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width() / 2, height + 3, str(height), ha='center', va='bottom', fontsize=10) ax.set_title('一周订单量分布') ax.set_xlabel('星期') ax.set_ylabel('订单量') fig.tight_layout() plt.show()

这段代码有几个细节值得说明。figsize=(8, 5)用来控制画布尺寸,如果是在Jupyter Notebook里运行,尺寸不合适会直接影响显示效果;color参数指定柱子的颜色,我会用公司VI色或者一套固定的配色,让输出看起来整齐;ax.text把数值标在柱子上方,height + 3是为了让文字不紧贴柱子顶部,稍微留出一点间距。

如果要把多张图拼在一起,我推荐使用subplots传入行数和列数,比如fig, axes = plt.subplots(2, 2)会创建2行2列共4个子图,然后用axes[0, 0]、axes[0, 1]分别操作每个子图。这样一张图就能同时展示多个变量的分布,也方便横向比较。另外,在Jupyter Notebook里画图时,建议在文件开头加上%matplotlib inline,确保图像能直接内嵌显示;如果在PyCharm里运行,一般会弹出一个单独的显示窗口,这也是正常的。

图表做完,最后用fig.tight_layout()调整布局,避免标题和坐标轴标签重叠,然后plt.show()展示。如果要保存图片,可以用fig.savefig('订单量.png', dpi=150, bbox_inches='tight'),dpi控制清晰度,bbox_inches='tight'可以自动裁剪掉多余空白。保存和展示是两回事,很多初学者以为plt.show()之后就保存不了了,其实是先保存后展示,或者直接保存不展示。

4. 常见报错与避坑指南:我踩过的坑帮你填平

4.1 安装环节:从pip到离线安装的备选方案

关于安装,最常见的问题是pycharm里装Pandas失败,或者公司内网环境没法在线装包。常规环境下,在PyCharm底部Terminal里执行pip install pandas numpy matplotlib即可。如果装的是Anaconda,直接用conda install pandas numpy matplotlib。这里有个细节:在终端里看到Requirement already satisfied并不代表你用到的Python环境里已经有了,要确认你安装的Pandas和当前解释器是同一个环境。我看过太多案例,用户在系统终端pip安装成功,但PyCharm的项目解释器是另一个虚拟环境,import pandas依然报ModuleNotFoundError。

离线安装也是常见需求。方法并不复杂:先在一台能联网的机器上执行pip download pandas -d ./packages,它会连依赖一起下载到指定目录,再把整个目录拷贝到离线机器,在那台机器上执行pip install --no-index --find-links=./packages pandas。有时候需要离线装Matplotlib,思路完全一样。需要注意的坑是,不同操作系统下的wheel包不通用,在Windows上下载的包可能无法在Linux上安装,所以最好在相同系统环境下去下载。

还有一个我见过的经典问题:UserWarning: failed to initialize numpy: no module named 'numpy'。这种警告通常不是你真的缺少NumPy,而是某个库对NumPy的版本要求和你当前环境不一致,或者Pandas安装时依赖的NumPy版本被其他操作覆盖了。处理方式是先把当前环境的numpy、pandas、matplotlib全部卸载,再重新安装一遍,让它们按照依赖关系自动装上匹配的版本。还有一个很低级的坑是给自己的Python脚本取名numpy.py或者pandas.py,运行时会因为文件命名和库同名,导致import语句导入到自己的文件,报出各种匪夷所思的错误,AudioModuleNotFoundError、AttributeError都可能是这个原因。

4.2 经典报错排查:np.float、pandas.core、numpy初始化失败

NumPy 1.20版本之后,np.float、np.int、np.bool这些Python内建类型的别名被移除了。所以很多老代码里的np.float(3.14)在新版本中会报AttributeError: module 'numpy' has no attribute 'float'.,解决办法是直接使用Python自带的float(),或者用np.float64(),但最好还是明确你的意图:如果只是把整数转成浮点数,用float()就行;如果需要NumPy的浮点类型,用np.float64()。更推荐的做法是写新代码的时候不使用这些旧别名,查找老代码里的这些调用然后批量替换。

另一个很典型的报错是AttributeError: module 'pandas' has no attribute 'core'。这个报错看起来像是Pandas缺失了某个模块,但绝大多数情况下是环境出了问题。比如你同时装了不同版本的Pandas,或者项目里恰好有一个叫pandas.py的文件,又或者安装过程不干净导致部分旧文件残留。排查思路有两条:第一,检查当前工作目录下有没有pandas.py文件,有就改个名字;第二,卸载后重新安装pandas,最好是在一个新的虚拟环境里pip install pandas,确保装的依赖是干净的。

还有一个与NumPy相关的老大难问题是failed to initialize numpy: no module named 'numpy'。这个报错容易误导人,因为你可能已经装了numpy。通常是因为某个包在编译时依赖特定版本的numpy,而当前环境中新装的numpy不再兼容,或者是numpy安装了一半导致init文件缺失。我的建议是用pip show numpy查看当前版本的安装位置,再用pip uninstall numpy彻底删除,最后pip install numpy重新安装。如果项目里有setup.py或environment.yml,检查声明的numpy版本要求是否和当前环境冲突。

4.3 数据类型转换的隐蔽坑:object类型与内存占用

Pandas里最容易被忽略的数据类型坑就是object。object列通常表示该列的内容不统一,可能是字符串、数字、空值混杂在一起。df.info()里看到object类型,一定要警惕,这往往是后续计算报错的根源。

比如客户ID这一列,如果有的值是"12345"字符串,有的是12345数字,Pandas读进来之后可能把整列判定为object。直接用astype(int)会报错或得到意外的结果。处理方法是先用pd.to_numeric(df['客户ID'], errors='coerce'),把能转的转成数值,不能转的变成NaN,再决定删除还是填充。类似地,字符串里的逗号、百分号也会导致数值列显示为object,例如销量写成"1,234"这种情况,需要先把逗号去掉再转换。

内存占用也是一个容易忽视的点。object类型在Pandas里的内存开销远高于纯字符串类型category或者数值类型int/float。如果一张表有几百万行,把不必要的object列变成category,内存可能直接下降一半以上。判断依据很简单:如果一个列的取值数量相对于行数来说非常少,比如性别、地区、产品大类,转成category很合适。用法是df['性别'] = df['性别'].astype('category')。转换之后再跑df.info(),能明显看到内存占用下降,这在处理大数据集的时候非常重要。

另外一个很隐蔽的坑是:Series在Pandas里如果有缺失值,astype(int)会直接报错,因为NaN没法转成整数。所以类型转换的顺序往往应该是先处理缺失值,再进行类型转换,最后再进入分析计算,顺序反了就会出各种莫名其妙的报错。

4.4 性能优化与使用习惯

数据处理量一大,性能问题就会浮出水面。很多人的第一反应是换更大的机器,但更多时候其实是代码本身不够高效。避免循环是最重要的一条经验。在Pandas里,如果要用for循环一行一行地遍历DataFrame,那基本可以断定这个思路有问题,改成向量化操作之后速度往往会提升几十倍。

举一个小例子,假设要根据销售金额打标签:大于200算“高”,否则算“低”。用循环判断要写好几行,但用np.where是更高效的方式,一行就能解决,而且更能看懂:

df['金额标签'] = np.where(df['销售金额'] > 200, '高', '低')

类似地,如果要对某列做条件变换,也可以尽可能用NumPy的where来替代循环。再比如对某列做加减乘除,直接用列整体操作而不是apply,效率会更高。还有一个很常见的优化点是:在最终做可视化或统计之前,只保留需要的列,不必要的列在读取时可以通过usecols参数指定。数据量几百MB甚至几个G的时候,这个习惯能省下大量内存和时间。

另外一个使用习惯上的建议:在Jupyter Notebook里做分析时,我会把代码分成几个清晰的单元格,读取数据、清洗、分析、可视化各占一块;但写成脚本文件时,我会在文件顶部集中放置所有的import,并且把核心函数用def封装起来,方便以后复用。这样可以保证即使换了新数据集,清洗和可视化部分也能快速套用。

我个人在实际操作中的体会是,数据分析真正值钱的部分不在代码本身,而在思路:你要先想清楚每一步为什么这样做,动手写代码反而是最后一步。如果一开始就把Pandas、NumPy、Matplotlib当成一套流水线,想清楚数据怎么流、谁该处理哪一段,那么无论拿到什么脏数据,心里都有底。最后再分享一个小技巧:我在这三件套的项目里,都会在脚本开头写一行pd.set_option('display.max_columns', None),这样DataFrame打印出来就能看到所有列,不会再因为被省略而误判数据结构。这个习惯已经帮我省掉了无数次来回检查列名的时间,推荐你也养成。

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

STM32F103闭环目标追踪系统:PID控制与双路PWM同步实现

简介:本资源是2023年全国大学生电子设计竞赛“运动目标控制与自动追踪系统”赛题的完整实现方案,面向STM32嵌入式开发初学者及电赛备赛学生,解决视觉定位、双云台协同控制、坐标映射与实时追踪等典型嵌入式系统工程问题。压缩包共258个文件&a…

作者头像 李华
网站建设 2026/9/10 8:17:43

前端手摸手跑路AI应用开发:从大模型API到SSE流式对话实战

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

作者头像 李华
网站建设 2026/9/10 8:15:14

LeetCode 110:平衡二叉树的递归后序遍历核心解法与工程启发

LeetCode 110这道平衡二叉树题目,我愿称它为“树结构入门体检单”。你去看大厂的笔试面试题单,几乎每一份都会把这道题放在二叉树板块的前十题里。它看起来就是判断一棵树是不是平衡的,但真正动手写的时候,你会发现不少人在递归边…

作者头像 李华
网站建设 2026/9/10 8:12:32

已读不回、语音条与撤回:即时通讯如何重塑我们的社交方式

你有没有过这样的瞬间:消息发出去了,屏幕显示“已读”,对方却像人间蒸发;手机明明满格信号,电话却怎么都拨不出去;对话框上方飘着一行“对方正在输入…”,几分钟后却一个字都没蹦出来。这些细碎…

作者头像 李华
网站建设 2026/9/10 8:06:51

愚人节课堂设计:用体验式教学提升中小学生信息辨别力

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

作者头像 李华
网站建设 2026/9/10 8:06:49

网站SEO问题快速诊断:从收录异常到排名下滑的排查思路

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

作者头像 李华