news 2026/10/4 1:19:49

MCP实战:用AI重构Excel工作流,打造自动化数据处理助手

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP实战:用AI重构Excel工作流,打造自动化数据处理助手

最近两周我一直在折腾一件事:给自己常用的 AI 助手装上一双能直接操作 Excel 的“手”。起因特别简单——每周我都要整理销售明细、汇总多张报表、按客户维度做数据透视,这些活虽然不复杂,但极其耗时。传统的做法是写 VBA、用 Python 脚本或者手动复制粘贴,每次需求一变化,代码就得跟着改,效率很低。后来我接触到MCP(Model Context Protocol),突然意识到:与其让 AI 只做一个“聊天框”,不如把它变成一个真正能干活的工作流引擎,而 Excel 就是最好的切入点。

这篇博文,我就完整记录一下“开发自己的第一个 MCP——用 AI 重构 Excel 处理工作流”的全过程。从协议的概念拆解、开发环境准备,到 Server 端代码实现,再到接入 AI 客户端实测,最后附上我踩过的坑和排查实录。如果你也天天跟 Excel 打交道,又想用 AI 把这些重复劳动自动化,这篇文章应该能帮你少走不少弯路。不管你是开发出身还是数据分析岗,只要愿意折腾,照着下面的步骤走,就能拥有一套属于你自己的 AI+Excel 自动化工作流。

1. 为什么我要用 MCP 重构 Excel 处理工作流

1.1 传统 Excel 自动化的三个痛点

在动手之前,先聊聊旧方案为什么让我越来越不舒服。以前处理 Excel,我基本就靠三样东西:Excel 自带的函数和透视表、VBA 宏、以及临时抱佛脚的 Python 脚本。

函数和透视表的问题在于:它们只能处理“规则明确”的数据。比如“统计某列含关键词的对应数值总和”,这种需求用 SUMIF 系列函数可以做,但一旦关键词变成动态的、或者说需求变成“帮我看看这个表格里有哪些异常”,函数就无能为力了。

VBA 宏则更尴尬——录制宏只能解决重复操作,遇到数据结构变化就得改代码,而且 VBA 的语法对现代开发者来说太古老了,调试起来非常痛苦。我见过太多同事的宏,换个 Sheet 名字就崩,那真的是在维护“祖宗代码”。

Python 脚本(比如用 pandas 或 openpyxl)在数据处理能力上确实强,但它有一个致命问题:脚本只能干你预先写好的事。你说“帮我统计 A 列的异常值”,脚本得先定义什么是“异常”;你说“帮我按区域汇总”,脚本得先知道你有哪些区域。这其实是在用另一种方式写规则,根本没用到 AI 的“理解力”。

1.2 MCP 到底解决了什么问题

MCP 的全称是 Model Context Protocol,名字里“上下文”三个字很关键。它本质上是一个标准化协议,让 AI 模型能够以统一的方式调用外部工具、读取外部数据源。你可以把它理解成一个 USB 接口——以前 AI 只是显示器(只能看不能摸),MCP 就是那个 USB 口,插上打印机、扫描仪、移动硬盘,AI 就变成了全能办公台。

具体到我的 Excel 场景,MCP 带来的核心改变是:

  1. AI 有了“手”:它不仅能理解“帮我统计每个区域的平均销售额”,还能真正打开 Excel 文件、读取数据、做计算、甚至把结果写回新文件。
  2. 工作流变成对话:以前处理一份报表要写一个脚本,现在我可以直接跟 AI 说“先看一下这个表的结构,然后按客户类型分组,把异常值标出来”,它会自主规划、调用工具、返回结果。
  3. 可复用性强:同一套 MCP Server,接 Claude、接国内大模型、接自己写的 Agent 都能用。工具写在 Server 端,客户端随时换,不用重新开发。

1.3 为什么选 Excel 作为第一个 MCP 项目

说实话,第一个 MCP 项目选择 Excel,除了它确实是我的刚需场景,还有一个原因:Excel 数据结构的“确定性”非常适合作为协议入门的练兵场。

MCP 涉及服务端、客户端、工具协议、参数定义这么多概念,如果你一上来就调外部 API、搞实时数据流,光是调试环境就能劝退一半人。但 Excel 的文件读写、单元格操作、数据筛选这些功能,逻辑上非常直白——读文件、解析 Sheet、执行操作、返回结果。这种清晰的功能边界,让我能把注意力集中在理解 MCP 协议本身,而不是被复杂业务逻辑带偏。

而且 Excel 处理是典型的“AI 能听懂、但自己干不了”的任务。AI 非常擅长理解“帮我统计这三张表的合同金额汇总”,但它天然没有文件系统访问能力,也没有表格计算能力。MCP 正好把这两者接起来。

2. 开发环境准备与 MCP 协议基础

2.1 开发环境与工具选型

先交代一下我最终的开发环境,都是目前社区里最主流、文档最全的搭配:

组件选择说明
编程语言Python 3.10+MCP 官方 SDK 支持最好,生态链完善
MCP SDKmcpPython 包官方提供的协议实现,已支持 typescript,但 Python 更直观
Excel 操作库openpyxl+pandas分别负责 xlsx 读写和数据处理
客户端Claude Desktop(或支持 MCP 的客户端)用于验证 Server 能否被 AI 调用
辅助工具uv(Python 包管理器)用 uv 管理虚拟环境,比 pip 更清爽

这里说一个小小的选择理由:为什么不用 xlwings 或 win32com?因为这两个库强依赖 Windows 上的 Excel COM 接口,一旦服务器上没有安装 Microsoft Excel 就直接歇菜。而openpyxl是纯 Python 实现,跨平台无痛,处理普通的 xlsx 文件完全够用。如果只是做数据分析和报表处理,openpyxl 加 pandas 的组合已经能覆盖九成需求。

安装命令也很简单,用 uv 初始化项目并添加依赖:

uv init excel-mcp-server cd excel-mcp-server uv add mcp openpyxl pandas

如果你还没有安装 uv,直接用 pip 也是可以的,只是虚拟环境管理上稍微繁琐一点。这一套下来,整个项目就是一个标准的 Python 包结构,后面写工具逻辑会很顺手。

2.2 MCP 架构拆解:三大角色是干什么的

在写代码之前,花五分钟搞清楚 MCP 的架构模型非常重要。很多教程一上来就贴代码,结果读者连 Server、Client、Tool 之间的关系都没弄明白,遇到报错就抓瞎。

MCP 架构里就三个核心角色:

  1. MCP Host(宿主):就是 AI 聊天客户端本身,比如 Claude Desktop、各种支持 MCP 的 IDE。它向用户显示对话,并在后台管理多个 MCP Server 的会话。
  2. MCP Server(服务端):轻量级服务,暴露出特定功能。在我这个项目里,它暴露的就是 Excel 的读写、统计、sheet 操作等能力。每个 Server 可以声明多个 Tool。
  3. MCP Client(客户端):在 Host 进程内部,负责与 Server 建立一对一的连接。它是协议的实际发起方。

这里最容易混淆的就是 Host 和 Client 的关系。用一句大白话解释:Host 是你的微信,Client 是你微信里打开的各个小程序,Server 是提供数据和服务的外部商家。你在微信里跟朋友聊天,顺手点开一个小程序订咖啡——聊天是 Host 的功能,小程序运行依赖 Client,而真正给你做咖啡的是商家(Server)的机器。

在这个 Excel 项目中,我要做的就是那个“商家”——实现一个 Server,里面提供诸如read_excel、write_excel、get_sheet_names这样的工具。AI 客户端(Host)收到用户指令“帮我把这些 Excel 合并了”后,会通过 Client 调用我的 Server 暴露出来的工具,再把工具返回的结果组织成自然语言回复给用户。

2.3 MCP Server 的两种传输方式

MCP 协议支持两种传输方式,虽然现在很多入门教程会跳过,但你后续部署到远程服务器、或者接到 Web 应用里时大概率会碰到,提前知道很有用:

  • stdio(标准输入输出):Server 作为 Host 的一个子进程运行,通过 stdin/stdout 进行 JSON-RPC 通信。这是本地开发最常用、最省事的方式。Claude Desktop 连接本地 MCP Server 用的就是这种。
  • HTTP + SSE(Server-Sent Events):Server 作为独立服务运行,Host 通过 URL 连接。适合部署在服务器上、多人共用、或者需要跨进程通信的场景。

我的开发阶段只用了 stdio 方式,零网络配置、零端口冲突,日志直接在终端里看,最适合学习调试。等你理解透整个协议,再去玩远程部署就不迟。

3. 从零实现一个 Excel 处理 MCP Server

3.1 项目结构设计

一个标准 MCP Server 的结构非常清晰,其实就是“Python 类 + 装饰器 + 生命周期管理”。我先把我最终的项目结构放出来:

excel-mcp-server/ ├── pyproject.toml ├── README.md └── src/ └── excel_server/ ├── __init__.py ├── server.py # MCP Server 主入口 ├── excel_ops.py # Excel 实际操作逻辑 └── models.py # 数据模型定义

这种分层设计有一个明显的好处:server.py里只负责协议相关内容(工具注册、参数校验、返回 JSON),excel_ops.py里放真正的 Excel 业务逻辑。如果将来你想加一个“读取 CSV 文件”或者“操作 PDF”的 MCP,不需要动协议层,只需要新增一个 ops 文件再注册工具就行。

3.2 核心代码:如何定义和注册一个 MCP Tool

MCP 的 Python SDK 提供了非常优雅的装饰器模式。我们直接在server.py里创建 FastMCP 实例,然后用@mcp.tool()注册工具。

先看最简单的工具定义——读取 Excel 文件的 sheet 列表:

from mcp.server.fastmcp import FastMCP import openpyxl from typing import Any # 创建 MCP Server 实例,名字会展示给 AI 客户端 mcp = FastMCP("excel-server") @mcp.tool() def get_sheet_names(file_path: str) -> list[str]: """获取 Excel 文件中所有 sheet 的名称列表。 Args: file_path: Excel 文件的完整路径(支持 .xlsx 格式) """ wb = openpyxl.load_workbook(file_path, read_only=True) return wb.sheetnames

代码看着很简单,但有几个关键点一定要说透。

首先是工具的描述和参数文档。在 MCP 协议中,工具函数名、docstring、参数名和类型标注都会被序列化后发给 AI 模型,模型靠这些信息来决定“什么时候调用哪个工具”。所以你写的 docstring 越清晰、参数说明越准确,AI 的调用成功率就越高。我实测下来的经验是:每个参数都要说清楚格式(是 .xlsx 还是 .csv)、单位(是万元还是元)、允许的范围,这能显著减少 AI 乱传参的概率。

其次每个工具只干一件事。我第一次设计时犯过一个错误,做了一个“全能处理函数”,把读文件、筛选、统计全塞进去,参数多达八个。结果 AI 模型经常漏传参数或者传错类型,调试得脑壳疼。后来拆分成read_excel、filter_rows、sum_column这种小粒度工具,虽然工具多了几个,但每个调用都非常稳定可靠。这也符合 MCP 官方推荐的“最小工具原则”。

3.3 扩展工具集:筛选、统计、写入、透视

光有一个get_sheet_names显然不够,我接着注册了几个最实用、最能体现“重构 Excel 工作流”价值的工具。

读取数据并转换为 JSON——AI 理解的中间格式:

import pandas as pd from typing import Optional @mcp.tool() def read_excel_to_json(file_path: str, sheet_name: Optional[str] = None, max_rows: Optional[int] = 0) -> list[dict[str, Any]]: """读取 Excel 数据并转换为 JSON 列表。 Args: file_path: Excel 文件路径 sheet_name: 要读取的 sheet 名,默认为 None(取第一个非空 sheet) max_rows: 最多读取多少行,默认 0 代表全部读取 """ df = pd.read_excel(file_path, sheet_name=sheet_name) if max_rows: df = df.head(max_rows) return df.to_dict(orient="records")

按条件筛选并统计求和——这直接对应热搜词里那个经典需求“统计同一列中含关键词的数据求和”:

@mcp.tool() def filter_and_sum(file_path: str, column: str, keyword: str, sum_column: str) -> float: """筛选指定列包含关键词的行,并对另一列求和。 Args: file_path: Excel 文件路径 column: 用于筛选的列名 keyword: 筛选关键词,支持模糊匹配 sum_column: 需要求和的数值列名 """ df = pd.read_excel(file_path) filtered = df[df[column].astype(str).str.contains(keyword, na=False)] return float(filtered[sum_column].sum())

把结果写入新的 Excel 文件:

@mcp.tool() def write_dataframe_to_excel(data: list[dict[str, Any]], output_path: str, sheet_name: str = "Sheet1") -> str: """将 JSON 格式的数据写入 Excel 文件。 Args: data: 要写入的数据,必须是字典列表 output_path: 输出文件路径 sheet_name: sheet 名称 """ df = pd.DataFrame(data) with pd.ExcelWriter(output_path, engine="openpyxl") as writer: df.to_excel(writer, sheet_name=sheet_name, index=False) return f"文件已写入:{output_path}"

到这步你可能会发现一个趋势:有了这几个工具,AI 就像一个熟练的表哥表姐,能读、能筛、能算、能写。MCP 的价值在于,你不需要预先定义“处理步骤”,只需要定义“能力边界”。具体怎么做,AI 会根据你说的话自己组合工具去完成。

3.4 Server 启动入口:如何与客户端建立连接

工具定义好了以后,需要一个入口把这些都启动起来。在server.py末尾加上标准的启动逻辑:

if __name__ == "__main__": mcp.run()

sdk的run()方法在本地默认监听 stdio,也就是说我们现在只要运行python src/excel_server/server.py,这个 MCP Server 就会在标准输入输出上等待调用请求。

我自己的习惯是顺手加一个if __name__ == "__main__"的测试分支,在启动前模拟一下调用,确保工具代码本身没有语法错误或路径问题。这比直接连客户端再排查要快得多:

if __name__ == "__main__": # 本地冒烟测试:做一次真实调用 test_result = get_sheet_names("./测试数据.xlsx") assert isinstance(test_result, list), "测试失败" print("自检通过,sheet 列表:", test_result) mcp.run()

3.5 运行时的参数校验与安全边界

在这版里,我特意做了几件“防呆”的事,也算是最想分享的经验。

第一,所有涉及文件路径的工具,统一做存在性检查。如果文件不存在,立刻抛一个异常,而不是让 pandas 或 openpyxl 去报一堆混乱的底层错误。MCP 协议会把这个异常字符串原样返回给 AI,我的经验是,越明确的错误提示,AI 越能自己纠正后续操作。

第二,默认不要在全量数据上做聚合操作。我的read_excel_to_json里加了max_rows参数,默认读前 100 行。目的是防止 AI 在一个几万行的表上不知轻重地全量加载,把上下文塞爆。等它确认结构之后,再通过专用工具做全量统计,这样上下文占用能控制到比较安全的范围。

第三,写入类的工具,不直接覆盖原文件。我的write_dataframe_to_excel要求传入一个新的output_path,而不是覆盖输入文件。原因很原始但也很重要:AI 生成的结果可能有偏差,我不能让它把源数据改错了。输出到新文件,出了问题大不了重来,原始数据文件永远是安全的。这一点是 AI 自动化和传统程序之间最大的差异——AI 是概率模型,必须给它预设犯错的空间。

4. 接入 AI 客户端:让模型开始“动手”操作 Excel

4.1 本地客户端配置:以 Claude Desktop 为例

Server 写完了,下一步就是接入一个支持 MCP 的 AI 客户端。目前市面上支持 MCP 的客户端越来越多,Claude Desktop、Cline、Cherry Studio、还有各种 IDE 插件都支持。

配置方式大同小异,核心都是编辑客户端的 MCP 配置文件,告诉它“去哪个地址启动 Server”。以 Claude Desktop 为例,配置文件在claude_desktop_config.json,操作原理是把 Server 的启动命令写进去:

{ "mcpServers": { "excel-server": { "command": "python", "args": ["D:/projects/excel-mcp-server/src/excel_server/server.py"], "cwd": "D:/projects/excel-mcp-server/src/excel_server" } } }

这里有一个特别容易踩的坑:绝对路径里的任何反斜杠都必须写成正斜杠或双反斜杠。Windows 用户在配置 JSON 里写路径时经常在这里翻车。我自己的建议是直接在项目里用pathlib解决跨平台路径,但配置文件的路径字符串只能手动处理好。

配置好后重启客户端,MCP Server 会在客户端启动时作为子进程自动拉起。你可以在对话界面上看到一个带锤子图标的工具列表被加载,说明 Server 注册成功了。

4.2 实测场景:让 AI 完成一整套报表汇总

为了验证重构效果,我准备了一个真实的测试场景。文件夹里放着三家分公司的月度销售表(格式略有差异),需求是:合并这三张表,按产品类别汇总销售额,并输出一个新的汇总表。

我在对话里直接输入:

“请查看 D:/sales 下的三家分公司销售表,分别获取它们的 sheet 结构和前几行数据,然后合并它们,按类别汇总销售额,把结果输出到 D:/sales/汇总表.xlsx。”

然后观察 AI 的表现。它很自然地先调用了get_sheet_names和一个read_excel_to_json的工具来理解三张表的结构,发现三张表的列名不完全一致(比如“金额”和“销售额”实际上是同一字段),然后它会主动询问我是不是要统一映射,或者直接基于常识做标准化处理。最后它会多次调用write_dataframe_to_excel写出结果。

整个过程肉眼可见地顺畅,这就是 MCP 带来的质变:AI 不是一次性把数据处理完,而是把一个长任务拆解成多个工具调用,每次调用后都会检查中间结果。这跟人类处理复杂 Excel 任务的思路完全一致。

4.3 让 AI 识别“不规则需求”:从死规则到活理解

前面提到函数和宏解决不了“模糊需求”,MCP 最大的优势就在这。我故意测试了一个不规则的指令:

“看看这个销售表里有哪些异常数据,帮我清理一下,然后在原文件旁边生成一个清洗后的版本。”

这个需求里“异常数据”没有明确定义。AI 会先读数据,然后根据字段名(比如数值列出现负数、日期列格式错误、关键列有空值),判断哪些属于“异常”,并告诉它自己的判断依据,然后调用过滤和写入工具生成新版文件。

这在传统 VBA 脚本里几乎没法做——你怎么给 VBA 定义“异常”?但在 MCP 世界,AI 的理解能力就是规则引擎,工具只是它的双手。这也是我为什么推荐所有人都尝试一下“AI + Excel”组合的根本原因——它不是在替代你做重复劳动,是在替代你思考“怎么定义问题”。

5. 常见问题与排查技巧实录

5.1 典型报错与解决方案速查表

我在开发过程中踩坑不少,这里整理一个速查表,基本都是社区里高频出现的问题:

报错现象根本原因解决方案
“Connection closed” 客户端连不上Server 启动失败,多半是依赖没装或路径错误手动运行python server.py,先看控制台有没有报错
工具调用后返回空列表工具执行有异常但被吞了,或者读取的 sheet 里确实没数据在 Server 里先做assert冒烟测试,排除业务问题
参数校验失败 / 类型不匹配工具函数参数未写类型标注,AI 无法推断所有参数必须标注类型,docstring 里写清格式
“Tool not found”工具没有正确注册,或者客户端加载的是旧配置检查@mcp.tool()是否装饰了函数,重启客户端
中文字段读取乱码openpyxl 默认编码问题,或读取时用了错误的路径统一用 pandas 读取,它对中文支持更好
FileNotFoundError 反复出现AI 传的路径带中文或空格,没有转义在 Server 内部做路径规范化处理,用os.path.abspath

5.2 排查工具链的实战心得

排查 MCP 问题时,最重要的一件事是:学会分开看两段日志。一段是客户端侧的(Host 是否发起了调用、传了什么参数),一段是 Server 侧的(工具是否真的执行成功、返回值是什么)。

开发阶段,我最常用的排查操作是:在不启动客户端的情况下,手动用命令行喂给 Server 一条模拟请求,观察输出是否符合预期。比如:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/call"}' | python server.py

这样可以快速定位是协议层的问题还是工具逻辑的问题,不用反复重启客户端。

另外强烈建议在 Server 代码里加logging.basicConfig(level=logging.INFO),把每次工具调用的参数和结果长度打印出来。这不仅对调试有用,还能反过来看到 AI 是怎么思考的——它每一步选择了什么工具、传了什么参数。我经常通过日志纠正自己写给 AI 的工具描述,而这也正是不断优化 MCP 工作流的关键。

5.3 我对安全边界的一些实践思考

最后聊聊安全这个话题。MCP 给了 AI 真正的文件系统读取和写入能力,这确实带来了一些风险。在我自己的实践中,有几点相对保守的做法值得参考:

  1. Server 默认只读取“指定目录”下的文件,不开放全盘访问。可以在 Server 初始化时传入一个base_dir白名单,所有文件路径必须在这个目录下,才允许读取。
  2. 写入操作强制走新文件输出,这在我前面的代码里已经体现了。AI 可以创建新文件,但不能覆盖源文件。配合定期备份习惯,安全性就能兜底。
  3. 敏感数据(比如身份证号、手机号)可以在工具层做脱敏处理,再返回给 AI,避免隐私信息进入模型上下文。

当初我决定拿 Excel 作为第一个 MCP 项目,回头看这个选择非常正确。MCP 的协议细节比想象中简单,真正的门槛在于如何把业务需求拆解成清晰、单一职责的工具。我现在已经把这套 Server 拓展到了 CSV 清洗、Excel 批量转 PDF、多表合并等好几个场景,但整个架构还是最初这三个核心工具的模式——读数据、处理数据、写数据。

如果你也想上手玩,我的建议很简单:别追求一步到位,先把最简单的“读取 Excel 文件名列表”跑通,然后再逐步添砖加瓦。当你第一次看到 AI 自动调用你自己的工具把一张乱糟糟的表整理成干净文件时,那种成就感会告诉你,这一趟折腾值了。

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

RK3566适配YT8512C百兆PHY的DTS配置与硬件握手详解

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

作者头像 李华
网站建设 2026/10/4 1:18:57

Android Automotive开发入门:从手机App到车载系统的范式转换

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

作者头像 李华
网站建设 2026/10/4 1:18:57

SPWM调制度本质:能量映射系数而非占空比参数

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

作者头像 李华
网站建设 2026/10/4 1:18:49

UVM寄存器模型前门与后门访问机制深度解析

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

作者头像 李华
网站建设 2026/10/4 1:18:28

C++重载机制在工厂模式中的工程实践与避坑指南

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

作者头像 李华
网站建设 2026/10/4 1:17:51

VirtualBox安装Ubuntu虚拟机教程:零基础打造Linux开发环境

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

作者头像 李华