news 2026/9/9 8:14:58

第三方API对接选型:Python与Rust性能与工程成本全对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第三方API对接选型:Python与Rust性能与工程成本全对比

前阵子我们组的网关服务又经历了一轮“要不要用Rust重写”的激烈讨论。起因很普通:一个聚合接口要同时调四五个第三方服务,Python版本在压测时P99延迟开始往上飘,某个供应商的响应稍微慢一点,整条链路就被拉得很长。于是有人甩出一句:“用Rust重写,性能直接翻好几倍。”但真正去调研和测试之后发现,问题远不是换门语言那么简单。第三方API对接这种场景,瓶颈到底在哪、语言能改变什么、不能改变什么,以及换语言之后工程上要付出哪些成本,我在实际项目里都踩过一遍,今天一次性说清楚。

这篇文章适合正在做第三方API对接、开放平台、BFF聚合层、爬虫采集、渠道网关这类工作的朋友读。不管你是团队技术负责人还是刚接触服务端开发的初学者,都可以从里面拿到可以直接用的对比数据、代码模板和选型思路。标题里的“性能对比”和“工程”这两个词,我认为不能分开看——性能只是一面,工程成本才是真正决定你选哪个方案的关键。

1. 先想清楚一个问题:你的瓶颈是语言还是网络

很多性能讨论刚开始就带偏了节奏。第三方API对接这条链路,看名字就知道大头不在这两种语言上,而在“第三方”这三个字。你调别人的接口,中间要经过公网或者专线,要走DNS解析、TCP握手、TLS握手,请求到了对方服务端还要等对方业务逻辑处理完再返回来。这段耗时里,你用C还是用Python,对“对方处理多久”没有任何影响。

1.1 API对接的耗时到底花在哪里

我习惯把一次第三方API调用拆成这样几段:

  • 客户端建立连接(TCP + TLS)
  • 发送请求头、请求体
  • 等待服务端处理
  • 接收响应体
  • 客户端解析响应体

真正由语言决定的部分只有两小段:连接的复用与管理、请求的编码和响应体的解析。中间网络延迟和服务端处理时间,语言再快也改变不了。举个很典型的例子:单次请求总耗时500毫秒,服务端处理占280毫秒,网络RTT占180毫秒,客户端解析、建连、发送只占40毫秒。就算把客户端这40毫秒优化到4毫秒,整体也只是从500毫秒变成464毫秒,优化幅度不到10%。

所以在对比Rust和Python之前,第一步不是写Benchmark,而是把日志里的耗时拆开看,确认瓶颈在客户端还是服务端。如果大部分时间都花在等待响应上,那么换语言的收益就主要体现在“同一时间能发起更多请求”上,而不是“单次请求变快多少”。

1.2 为什么很多团队在这样的场景下仍然坚持用Python

说实话,第三方API对接的大部分场景,Python相当合适。第一,业务代码通常就是“拼请求、发出去、解析响应、落库”,这种胶水性质的工作用Python写起来极快。第二,第三方官方SDK大都是Python优先,Rust的某些小众服务SDK要么没有要么年久失修。第三,团队里做对接业务的同学通常熟悉Python,出问题排查也顺。

但Python的软肋在于高并发下的事件循环开销和内存占用。异步IO虽然能把并发拉起来,但每个连接的任务调度、协程切换、对象分配都会吃掉CPU。一旦集群QPS上到几千甚至更高,CPU首先被打满的不是你的业务逻辑,而是框架本身的事件循环、JSON解析和连接管理。这时候Rust的并发模型优势就会非常明显。我在实践中得到的判断标准是:如果日常请求量在每秒几百这个量级,Python完全够了;如果单实例每秒要处理几千甚至上万个第三方API调用,Rust的收益会很明显。

2. 同环境下的实测对比:数据比感觉更有说服力

我挑了一个很贴近真实业务的场景来压测,不是那种空转Hello World,而是模拟一个第三方API聚合网关:收到客户端请求之后,并行调用三个模拟第三方接口,每个接口返回一份大约10KB的JSON,聚合之后再返回给客户端。这个场景在开放平台和数据中台里非常常见。

2.1 测试环境与方法

测试用的是一台4核8G的容器,Ubuntu 22.04。Python侧用FastAPI + httpx异步客户端,Rust侧用axum + reqwest。两边都实现了完全相同的逻辑:一个聚合接口,内部并行请求三个下游模拟接口,解析响应后合并返回。

为了不让客户端解析拖后腿,Python侧我用orjson替代标准json库,Rust侧用serde_json。下游模拟接口由另一个独立服务承担,固定返回10KB的JSON并强制加40毫秒服务端延迟,这样两个被测服务面对的是完全相同的网络环境。压测工具用wrk,100个并发连接,持续5分钟,压测前先预热30秒让连接池和JIT模式稳定下来。

2.2 压测结果与差异分析

结果整理成了一张表,单机测试环境下的数据,供参考:

指标Python (FastAPI + httpx + orjson)Rust (axum + reqwest + serde_json)差距
QPS18204060约2.2倍
平均延迟55ms26ms约2.1倍
P99延迟121ms44ms约2.8倍
最大延迟1.4s190ms约7倍
常驻内存约185MB约44MB约4.2倍
冷启动到可服务约1.6s约7ms约220倍

这个结果并不让人意外,但里面的细节很值得琢磨。QPS差2倍左右,并不只是“循环快”造成的,更关键的是Rust的tokio运行时可以多线程并行处理事件,而Python的asyncio在单进程内受GIL限制,即使开启多线程也未必能充分利用多核。100并发下,Python的事件循环已经出现调度瓶颈。

最扎眼的其实是最长延迟。Python的1.4秒最大延迟,大幅拉长了尾延迟。这通常来自GC暂停、事件循环里某个回调卡顿,以及高峰期连接池资源争抢。Rust因为没有GC,也没有解释器级别的全局锁,最长延迟能控制在200毫秒以内。对于网关类服务来说,P99和最大延迟往往比平均延迟更重要,因为用户感知的是“偶尔一次特别慢”。

2.3 JSON解析单测:没有想象中那么悬殊

还有一个单独测过的点:JSON解析。同样是解析一段10KB的嵌套业务JSON,Python用orjson,Rust用serde_json,单次解析耗时的量级差异大约是3倍左右。我没有单独跑足够多样本做严谨的微基准,但从实际压测的火焰图看,Python响应体解析占CPU的比例明显更高。

不过有一点需要说清楚:在很多第三方API对接场景里,真正的开销在I/O等待上,CPU解析反而不是最大的问题。只有在响应体巨大(比如几百KB甚至几MB)或者需要解析大量嵌套数组的时候,解析性能才会上台面。这里也可以看出,如果业务里有大量响应体字段校验、类型转换,Python的pydantic会有额外开销,Rust用serde反序列化到强类型结构体则几乎没有运行时损耗。

3. 工程化才是真正的分水岭

只看性能数据,很容易得出“Rust全面碾压Python”的结论,但真把项目做起来,工程成本会让你冷静下来。我见过不止一个团队因为崇拜性能把网关从Python重构到Rust,结果迭代速度掉了好几倍。性能对比只回答“跑得快不快”的问题,工程化回答的是“改得快不快、稳不稳、好不好排查”的问题,而后者在第三方API对接这种业务规则多变、供应商协议千奇百怪的领域,重要性不亚于前者。

3.1 类型安全:运行时兜底与编译期拦截

第三方API最让人头疼的事情,就是响应体字段随时可能变化。字段名改了、类型从字符串变成数字、某个字段可能为空,这些都是家常便饭。

Python的常规做法是用dict接收,然后在业务里做一堆防御性判断。后面为了代码可维护,引入pydantic模型做校验,但pydantic的校验是运行时的,每一条数据都要在运行时解析一遍,既增加CPU开销,也可能在校验逻辑出问题时把异常抛到业务代码里。

Rust这边,用serde的derive宏定义结构体,字段类型在编译期就固定了。下游返回的类型和结构体字段对不上,直接解析报错,不会带着脏数据继续往下跑。这个差异在接口文档齐全、协议稳定的场景里是加分项;但反过来,如果第三方接口经常加字段、改结构,Rust每次都要改代码重新编译发布,Python可能只需要在代码里加一个字段处理逻辑然后热重启。

3.2 错误处理:显式与隐式

我用Python做对接时,习惯用try/except包住所有网络调用,配合tenacity做重试。这种写法灵活,但有个隐患:异常处理一旦写得不够精细,很容易把“业务校验失败”和“网络临时故障”混在一起处理。Rust的Result类型强迫你把错误路径想清楚——连接超时、TLS错误、HTTP状态码错误、JSON解析错误,每一种都有明确的类型,编译的时候就会逼着你处理。

从工程规范的角度讲,Rust更利于长期维护,因为代码里很少出现“不知道哪里抛了异常”的情况。不过初期写起来确实更费劲,尤其是团队还没有完全适应所有权和生命周期的时候,一个简单的重试循环都可能被借用检查器拦住半天。选型时一定要权衡团队现有的能力储备。

3.3 可观测性与监控链路

第三方API对接的排障,基本靠日志、链路追踪和指标。Python生态里的opentelemetry已经非常成熟,FastAPI中间件一挂就能把所有请求路径的span采集出来。Rust生态的opentelemetry crate也在不断完善,但从面板到告警规则的整套闭环,往往需要更多手工搭建。

这一点看起来不起眼,真正出故障的时候能救命。我记得有一次对接的支付渠道在晚高峰出现部分超时,Python服务因为中间件齐全,很快定位到是某条专线路由抖动而不是代码问题;另一个Rust服务因为刚开始链路追踪没配全,排查花了好几倍时间。性能和类型安全再重要,出了问题不能被快速定位,再快的代码也白搭。

3.4 部署与发布:解释器环境 vs 静态编译

这个话题在热词里出现的频率极高,比如“python安装”“linux系统安装python”“python转exe文件”,说明Python部署问题困扰了很多人。Python项目上线时要处理虚拟环境、pip依赖、系统库、Python解释器版本,稍有不慎就会出现线上和线下环境不一致。Rust通过cargo build --release编译出一个静态链接的二进制文件,直接扔到服务器上就能跑,不依赖服务器上的解释器环境,这在容器化部署和批量发布场景里省了很多事。

但Rust也不是全无痛点。Python可以热加载代码,配合uvicorn的--reload参数做本地调试非常方便;Rust没有官方热部署机制,发布新版本只能滚动重启容器。如果业务需求是“没日没夜地快速改接口逻辑”,Rust的编译发布节奏会让你觉得非常累。而那些求稳求性能的核心链路,Rust的静态编译和启动速度又显得格外珍贵。

4. 两套完整对接代码,直接对照着抄

讲了这么多对比,还是要落到代码上。我以最常见的“查询天气聚合接口”为例,分别给出Python和Rust的实现。这里的核心诉求有三个:并行调用多个第三方接口、统一超时控制、响应解析后的结构体返回。

4.1 Python侧:FastAPI + httpx + orjson + pydantic

首先安装依赖:

pip install fastapi uvicorn httpx orjson pydantic

下面是核心代码:

import asyncio import httpx import orjson from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class WeatherResponse(BaseModel): city: str temperature: float humidity: float # 全局复用同一个Client,避免每个请求都重建连接 client = httpx.AsyncClient(timeout=10.0, limits=httpx.Limits(max_connections=200)) @app.get("/weather/{city}") async def get_weather(city: str): urls = [ f"https://api.provider-a.com/weather/{city}", f"https://api.provider-b.com/weather/{city}", f"https://api.provider-c.com/weather/{city}", ] # 并发收集三个第三方接口结果 resp_list = await asyncio.gather( *[client.get(url) for url in urls], return_exceptions=True ) result = {} for resp in resp_list: if isinstance(resp, Exception): # 这里只是简化,实际需要更细致的异常归类 continue payload = orjson.loads(resp.content) result[payload["source"]] = WeatherResponse( city=payload["city"], temperature=payload["temperature"], humidity=payload["humidity"], ) return result

用FastAPI要注意一点:多worker部署时,每个worker都要维护自己的httpx连接池,内存占用会进一步膨胀。另外,orjson只能序列化dict或者pydantic模型,不能直接序列化任意对象,所以我在返回前把模型放入dict。

4.2 Rust侧:axum + reqwest + serde

Cargo依赖配置:

[dependencies] axum = "0.7" tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["json", "rustls-tls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1"

核心代码:

use axum::{extract::Path, routing::get, Json, Router}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, Serialize, Deserialize)] struct WeatherResponse { city: String, temperature: f64, humidity: f64, } #[derive(Debug, Deserialize)] struct ProviderWeather { source: String, #[serde(flatten)] weather: WeatherResponse, } async fn get_weather( Path(city): Path<String>, client: axum::extract::State<Client>, ) -> Json<HashMap<String, WeatherResponse>> { let urls = [ format!("https://api.provider-a.com/weather/{city}"), format!("https://api.provider-b.com/weather/{city}"), format!("https://api.provider-c.com/weather/{city}"), ]; let futures = urls.iter().map(|url| client.get(url).send()); let responses = futures::future::join_all(futures).await; let mut result = HashMap::new(); for resp in responses.into_iter().flatten() { if let Ok(payload) = resp.json::<ProviderWeather>().await { result.insert(payload.source.clone(), payload.weather); } } Json(result) }

这里有个Rust初学者很容易踩的坑:reqwest的Client一定要用State注入,或者定义成全局静态变量,不能每个请求内新建。因为每次创建Client都会新建连接池和TLS上下文,开销极大。另外这里为了演示简洁,省略了统一的超时设置,实际上应该在Client初始化时加上timeout。

4.3 重试、限流与熔断的两种落法

第三方API对接要做到稳定,光有超时不够,还要有重试、限流、熔断。

Python的重试我习惯用tenacity,它支持指数退避和抖动。比如给一个第三方调用加上重试三次、每次按指数退避并加随机抖动:

from tenacity import retry, stop_after_attempt, wait_exponential_jitter @retry(stop=stop_after_attempt(3), wait=wait_exponential_jitter(initial=1, max=10)) async def call_provider(client: httpx.AsyncClient, url: str): resp = await client.get(url) resp.raise_for_status() return orjson.loads(resp.content)

Rust这边实现重试的方式很多,但背后原理和Python是共通的。我常用backon这个库,它提供了指数退避和全抖动策略,写起来也比较省事。限流两种语言都能做,Python有semaphore,Rust也有tokio::sync::Semaphore,逻辑上大同小异。真正难的其实是“熔断”,也就是当第三方持续异常时主动降级,避免线程池被拖死。这块在Python生态里没有特别好用的通用库,Rust生态的第三方熔断库同样不成熟,多数团队最终还是会选择自行维护一个状态机。

5. 选型建议:选语言等价于选系统边界

你不需要在每一个场景里都做非此即彼的选择。作为过来人,我强烈建议把整个系统的边界画出来,再决定哪一层用Python、哪一层用Rust。

5.1 一张决策速查表

我根据自己的项目经验整理了一张选型表,不能直接套用,但能帮你梳理思路:

场景特征推荐方案核心原因
业务接口经常调整,团队以业务开发为主Python开发迭代快,改接口成本低
高并发网关、消息聚合层、基础服务Rust吞吐高、延迟稳、内存占用低
数据管道跑批,每天处理千万级第三方响应Rust(解析与聚合部分)CPU占用低,带宽利用率高
以简单业务编排为主,接口调用量很低Python工程成本低,维护容易
大量复杂JSON处理 + 少量业务变化Rustserde强类型解析极大减少脏数据侵蚀
团队所有人都会Python,只有少数人熟悉RustPython为主,Rust局部引入减少团队学习成本,降低阻塞风险

这是个反过来说也成立的表。很多人一上来就想着“选哪个更好”,实际上应该先想清楚“这个服务未来三个月里改动频繁还是稳定不变”。频繁变动的部分用Python,长期稳定且性能敏感的部分用Rust,往往是最优解。

5.2 混编方案:Rust核心 + Python编排

如果不想做二选一,可以考虑用pyo3把Rust代码编译成Python扩展模块,然后在Python的编排层里调用。这个方案最适合的场景是:业务规则变化快,但核心路径存在大量计算密集型逻辑,比如签名算法、数据脱敏、响应体快速解析和校验。

我做过一个渠道对接服务,里面涉及SM2/国密算法签名、响应体必填字段校验、多响应源合并。纯Python实现时,这几步占了整个请求CPU的60%以上,后来我把它拆出来用Rust写成一个so扩展,Python只负责业务编排。改造之后那部分CPU时间下降了80%,同时保留Python业务层的全部灵活性。混编的缺点是部署更复杂,很多Python镜像里需要带Rust编译器才能编译扩展,或者预先编译好各个平台的so文件分发。

5.3 一个BFF聚合网关的改造案例

再说一个典型到不能再典型的案例。一个BFF聚合网关,逻辑上是把内部五个微服务的数据聚合成一个页面接口。原来用Python写,QPS大约1500,但每次大促流量高峰,P99延迟会从60毫秒飙到1秒以上,原因往往是某个下游响应慢,拖累了整个聚合链路。

后来小组整了一个PoC,把网关改写成Rust版本。第一版改造的效果是QPS提升到4000左右,P99稳定控制在80毫秒以内。但这里有个前提条件,团队里有三个人已经写了半年多的Rust,踩过不少坑。这个Rust版本上线后,每次改聚合逻辑都多了一道编译和发布流程,比原来慢不少。所以最终的管理层决策很有意思:网关保留Rust版本,但只在需求稳定的时候改;快速迭代的新业务聚合逻辑,单独起一个Python边缘服务来承接。这种分层设计既扛住了大促流量,又保住了迭代速度。

6. 踩坑实录与常见问题

这里列一些我实测中遇到过的坑,每一件事都是血泪教训,希望能帮你少走弯路。

6.1 常见问题速查表

现象可能原因解决办法
Python网关高峰期CPU飙高asyncio事件循环被同步阻塞、连接池耗尽、大量对象创建用uvloop替换事件循环,梳理代码里是否有time.sleep或同步requests
Rust服务编译后比Python慢没有开启release优化,或者用了debug模式压测务必用cargo build --release,并在Cargo.toml中配置opt-level = 3、lto = "thin"
reqwest请求莫名其妙超时每个请求都新建Client,导致连接不重用、TLS握手频繁全局复用Client,一次创建多次使用
第三方接口偶发返回脏数据响应字段类型不稳定,解析过程中静默失败Rust用强类型struct解析并显式处理错误;Python用pydantic校验并记录脏数据日志
连接池被下游慢请求占满超时设置不合理、没有熔断设置socket和总请求超时,引入信号量限制并发,超过阈值直接降级
压测时Rust表现异常差编译参数没有调优,或者把轻量请求也走了完整TLS检查profile.release配置;本地压测可用HTTP,生产再用TLS验证全链路
Python asyncio的超时没有真正取消请求asyncio.wait_for超时后底层socket未关闭,造成连接泄漏在except分支显式关闭对应连接,或者调用client.aclose()

有一个细节容易被忽略:Rust的Cargo默认是针对release做了优化,但如果你在开发机上直接cargo run做压测,默认是debug模式,性能会比release慢十几倍甚至几十倍。我之前见过有人拿debug模式得出“Rust不过如此”的结论,这就是典型的没有做基准条件控制。

6.2 几条亲身实践下来的性能建议

不管最后选了哪门语言,下面这几条经验对第三方API对接都适用。

第一,连接池一定要重点调优。Python侧httpx的limits、Rust侧reqwest的pool_max_idle_per_host,直接决定高并发下的建连开销。每次新建连接都要经历TCP和TLS握手,这个成本有时候比业务处理还高。

第二,重试要带抖动。固定间隔重试在服务恢复瞬间会造成“雷群效应”,一大堆请求同时打过去,把刚恢复的服务又打挂。指数退避加随机抖动,虽然会让个别请求晚一点成功,但整体稳定性好很多。

第三,日志里必须记录每个第三方调用的耗时、状态码、响应体大小、重试次数。没有这些数据,性能优化和故障排查都是盲人摸象。我甚至建议在业务低谷期把采样率调到100%,高峰期再按比例采样。

第四,用Rust时不要滥用clone。reqwest的Client本身是克隆友好的,因为内部是Arc的引用计数,但业务数据结构如果频繁clone,内存分配开销会抵消语言层面的性能优势。多借用,少复制,这是Rust性能调优最核心的思路。

第五,如果选择Python,尽量用orjson替换json库,用uvloop替换默认事件循环。这两个改造几乎是零成本提升性能,尤其适合高并发异步调用场景。安装uvloop后只需要在入口代码加一行:

import uvloop uvloop.install()

这个改动后,长连接场景下QPS提升20%到30%都是有可能的。别问我为什么不在前面实测部分用uvloop,因为我一开始忘了开,后来开了才发现差距不小。

回头再看整个对比,我个人在实际操作中的体会是:Rust和Python在第三方API对接里的差距,真实存在,但远没有“一个天上一个地下”那么夸张。真正影响系统表现的,是并发模型、连接管理、超时机制、错误处理这些工程细节,语言只是承载这些细节的容器。如果你团队主力是Python,先别急着重写,把连接池、超时、重试策略、uvloop这些低垂果实摘完,往往能获得接近一倍的性能提升。等这些手段都用尽了,瓶颈仍然卡在CPU或内存上,再考虑把核心链路用Rust重写,收益会大得多。

最后再分享一个小技巧:不管用什么语言写第三方API对接,先做一个响应体schema版本化的机制。每次第三方接口返回的时候,在日志里自动记录响应文档结构有没有变化,一旦发现字段增减立刻告警。这件事做对了,能让你的接口在第三方悄悄升级时提前预警,而不是等线上炸了才开始排查。性能再快,也没有“及时发现问题”来得有价值。

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

技术选型踩坑自救指南:从止损到重构的实战策略

技术选型这件事&#xff0c;翻过车的人才能懂那种焦灼感。2026年开发环境和工具链的变化速度比往年更快&#xff0c;很多团队当初拍板时觉得万无一失的方案&#xff0c;走到中期却频频卡壳——不是性能扛不住&#xff0c;就是生态跟不上&#xff0c;要么就是团队成员越写越痛苦…

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

百度之星备考全攻略:从历年真题看动态规划与图论命题规律

简介&#xff1a;这份压缩包收录了百度之星编程大赛历年试题&#xff0c;面向备战算法竞赛的程序员、计算机专业学生以及希望系统提升编程与算法能力的开发者。资源共116个文件&#xff0c;以jpg、css、htm、js等类型为主&#xff0c;压缩包整体仅983KB&#xff0c;其中htm页面…

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

AI Skills实战:5个开源场景搞定笔记、会议、数据、演示与配图

最近大半年我一直在折腾各类 AI 编程工具&#xff0c;从 Claude Code 到 Codex、OpenCode、Cline 来回切换。工具换了不少&#xff0c;最后发现真正让 AI 干活“稳下来”的&#xff0c;不是模型本体的强弱&#xff0c;而是一套叫 Skills 的东西。如果你把 AI 助手当成一个只会聊…

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

35岁抑郁峰值曲线:软件测试工程师如何应对职业压力

我做了十来年软件测试&#xff0c;中间也带过开发和测试团队&#xff0c;这两年陆陆续续有年轻同事私下跟我聊睡眠变差、上班前心慌、对群消息有一种条件反射式的烦躁。有个刚过完三十四岁生日的兄弟发给我一条热搜&#xff0c;问&#xff1a;“网上说开发者抑郁指数曲线三十五…

作者头像 李华
网站建设 2026/9/9 8:09:59

飞飞江湖v2.0商业版:服务器集群改造与运营实战解析

简介&#xff1a;飞飞江湖 v2.0正式商业版是一套采用BBS模型构建的论坛社区类源码资源&#xff0c;面向Web开发工程师、独立站长及社区运营相关人员&#xff0c;可用于搭建互动交流平台、开展二次开发或进行系统架构研究。该版本以rar压缩包形式发布&#xff0c;平台暂未标注文…

作者头像 李华
网站建设 2026/9/9 8:08:31

空标题项目如何破局?从“111111113”到清晰交付的完整思路

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

作者头像 李华