news 2026/9/30 1:39:20

BLE设备地址类型全解析:Public、Random与隐私地址实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BLE设备地址类型全解析:Public、Random与隐私地址实战指南

做BLE开发的人,十有八九都被“MAC地址”坑过。手机扫描出来一个地址,打印出来看着像48位的设备地址,拿去绑设备,重启后却变了;同一个设备在协议栈启动前后不一样;抓包软件里明明标着 Public Device Address,和手机系统里显示的又对不上。这些困惑,基本都源自同一个问题:BLE 并不像 Wi-Fi 那样只有一张静态的 MAC 地址,它把设备地址分成了好几类,Public Device Address、Random Device Address、Non-resolvable Private Address 这些名词听着像在背协议规范,实际上直接决定了你的设备能不能被发现、连接之后会不会掉线、绑定关系还能不能对上。这篇文章就把这几个地址类型讲透,顺便把开发中常见的“MAC 变了”、“平台显示不一致”问题一起解决了。

1. 先搞清楚:BLE里的“MAC地址”为什么这么难懂

1.1 蓝牙地址不只是“一张网卡一个号”

接触过以太网或者 Wi-Fi 的朋友,对这个“MAC 地址”一般会有一个刻板印象:设备出厂时烧录一个全球唯一的地址,比如路由器标签上印的那一串 12 位十六进制数,这辈子基本不变。但到了 Bluetooth Low Energy,这套经验直接失效。BLE 的链路层地址也是 48 位,结构上跟 MAC 地址完全一致,都是XX:XX:XX:XX:XX:XX这种形式,但它的语义远比网卡地址复杂。

为什么?因为 BLE 的广播和连接机制天然需要“身份”这个概念。网卡地址唯一,是因为我们通过它来寻址,设备身份和物理地址是绑定的。而 BLE 的应用场景,既包含长期连接的传感器,也包含大量一次性的广播数据包,还有注重隐私的可穿戴设备。如果所有设备都用一成不变的固定地址,那任何人都可以扫描你、跟踪你、记录你每天去过哪里、用过哪些设备。所以蓝牙规范在经典蓝牙的 Public Address 之外,增加了一整套随机地址体系,让设备在不暴露身份的情况下依然能正常通信。

这样一来,BLE 的“MAC 地址”就不再是一个固定不变的值,而是一个会变、可以配置、还能加密解析的地址系统。很多开发者在第一天接触 BLE 时,都会拿着手机扫描工具看到设备地址还分 Public 和 Random,一脸懵。没关系,先记住一句话:BLE 地址本质是个 48 位二进制数,地址类型由最高的两个 bit 决定,不同 bit 组合代表不同含义,后面所有内容都围绕这个展开。

1.2 地址家族全景:Public 和 Random 两大分支

蓝牙核心规范(Bluetooth Core Spec)里,BLE 的设备地址分成两大类,第一类只有一种,叫 Public Device Address;第二类叫 Random Device Address,Random 下面又分三种:Static Random Address、Non-resolvable Private Address、Resolvable Private Address。

很多人看到这个分类会问:Non-resolvable Private Address 好像是单独一个概念,标题里为什么把 Random Device Address 和它并列?因为 Random Device Address 是一个大类,而 Non-resolvable Private Address 是其中的一种具体实现。它们不是同一层级的概念。实际开发中,你看到的地址类型字段大概率就是这么标的:

地址类型最高两位 bit是否固定能否解析身份典型用途
Public Device Address00固定直接可识别出厂设备、Beacon、经典蓝牙
Static Random Address01可固定直接可识别无 OUI 厂商的设备、模组地址
Non-resolvable Private Address10定期变化不可解析匿名广播、周期性扫描
Resolvable Private Address11定期变化可解析(需 IRK)隐私保护连接、防追踪

这张表基本就是全文的地图。前两类是人畜无害的“显式身份”,后两类是让你又爱又恨的“隐私地址”。下面逐个展开。

2. Public Device Address:唯一不变的一类

2.1 组成结构:公司标识加上厂商分配

Public Device Address 的逻辑最好懂,它要求设备厂商向蓝牙技术联盟(Bluetooth SIG)申请一个 Company Identifier,这个标识一共 24 位,通常你看到的 MAC 地址前缀00:0C:29、50:65:83这类,就是公司标识。剩下的 24 位由厂商自己分配,可以是流水号,可以是芯片唯一 ID,只要整个 48 位组合不冲突就行。这样的地址,就是典型的固定地址,和以太网卡一样,出厂烧录后一般不改变。

很多人会把蓝牙的 Public Device Address 和 Wi-Fi 的 MAC 地址完全混为一谈,实际上它们确实长得一模一样,连最高两位 bit 都是 00。这也造成一个常见误判:如果我只拿蓝牙设备的地址前三位去查 OUI 数据库,查到某个厂商,就以为这个设备一定是该厂商生产的。这个判断在 Public Device Address 下基本成立,但对于后面的 Random Address 完全不适用。注意,BLE 的 Public Device Address 可以由芯片内部一次编程区域来存,也可以是软件配置到 Flash 里,但从蓝牙链路层视角,它没有义务对外证明自己“真的来自那个 OEM”。

2.2 实际用在哪,怎么查

Public Device Address 最常见的用途是:设备出厂时需要有一个稳定身份、又不打算做复杂配对绑定的场景。比如 iBeacon 采集终端、防丢器、部分智能家居传感器,都会直接用 Public Device Address 作为自己的唯一标识。上位机扫描时,拿这个地址做成白名单,只要广播包里的地址匹配,就认为找到了目标设备。

查这类地址的方式很简单。Linux 上可以用btmgmt或者hcitool lescan扫描广播设备,Android 端可以在开发者选项里打开“蓝牙 HCI 信息收集日志”,再用 Wireshark 打开日志文件,就能看到完整的广播数据和地址类型。这里有一个很容易踩的坑:很多芯片出厂烧录的地址是写在一次性 Fuse 或者寄存器里的,如果你同时用多个同型号开发板,它们的 Public Device Address 可能非常相似,甚至前面的 24 位完全相同。如果你在代码里写死了“地址前缀等于XX” 就认定是自己设备,那同厂商的不同设备会全部命中,反而找不到真正的目标。

2.3 OUI 前缀不能迷信

看到热搜里有“000c 29开头的mac地址都是虚拟机吗”这个问题,顺便说一句,这类问题在 BLE 开发里也很典型。00:0C:29是某虚拟化平台申请的公司标识,出现在以太网虚拟网卡里非常常见,但假如你在分层网络环境里看到一个随机设备地址正好也是00:0C:29开头,那基本可以断定是有人在软件层面伪造或者借用了这个 OUI。同理,BLE 设备如果宣称自己的 Public Device Address 是00:0C:29:xx:xx:xx,实际是某厂商的虚拟化平台 OUI,也不能说明它一定来自该平台,只是用户手动配置了一个类似的地址。真正要确认身份,还是要结合广播数据、配对密钥、服务 UUID 等综合判断。前缀只能缩小范围,不能当成铁证。

3. Random Device Address:一类地址,两种命运

3.1 Static Random Address:没申请到 OUI 的“固定地址”

Static Random Address 是 Random Device Address 里最接近“固定地址”的一种。生成规则是:整个地址 48 位里,最高两位固定为 01,剩余的 46 位随机生成,但不能全是 0 也不能全是 1。它不需要向蓝牙技术联盟申请,任何厂商都可以自己生成,只要不跟同一批设备里的其他地址冲突就行。

这个特性对小厂商和做模组的团队特别友好。因为申请一套 OUI 要花钱走流程,很多小团队做产品,根本不需要全球唯一的前缀,只要在我这一批设备里不冲突、重启后稳定不变就行。于是 Static Random Address 成了最常见的“低成本身份方案”。很多国产蓝牙芯片出厂时烧录的地址,就是这种 Static Random Address,它的特征也很明显:地址的最高两位是 01,转换成十六进制后,地址的最左边两位大概率是4x到7x这个区间,因为最高字节的最高两位已经是 01 了。

Static Random Address 有一个重要的行为特点:它“可以”变,但“通常”不变。规范里说设备上电时可以重新生成,也可以沿用原先的值。如果 SDK 的默认行为是每次启动都重新随机生成,你看到的设备地址就会“每次开机都变”。这就是搜索词里“杰理701芯片mac地址为什么会改变”的一类典型原因:不是芯片坏了,而是这个地址本身就是 Random Device Address 里的 Static Random,固件没把它固化下来,每次初始化都重新随机了一次。解决办法也很简单,在协议栈初始化时,把上次生成的地址保存到 Flash,下次启动读回来重新设置,或者直接写死一个固定地址。

3.2 Non-resolvable Private Address:不想被认出来的“路人甲”

Non-resolvable Private Address 这个名字有点劝退,但它的思想很简单:我不要实名,甚至连“可解析的假名”都不要,我就是个路人甲。它的最高两位固定为 10,剩下的 46 位随机生成,定时更换。因为随机部分不含任何可识别的身份信息,收到这个地址的对端设备,没有任何办法通过计算或者查表,知道这个地址背后是哪个设备。

那这种地址有什么用?最常见的场景是:设备只是周期性地向外发广播,不需要被连接,也不希望被追踪。比如一个商场里的客流统计信标,它每隔几百毫秒广播一次,每次地址都换,路过的人无法把它和特定商场、特定品牌联系起来,更无法判断“这个信标是不是我之前见过的那个”。在这种情况下,Non-resolvable Private Address 就是最合适的匿名方式。

我自己在开发环境里见过很多新手犯的错是:把 Non-resolvable Private Address 当成了设备标识,存到后台数据库里。结果就是设备每换一次地址,服务器后台就多出一条“新设备”记录。这个问题在设备长时间运行、周期性广播时特别严重。所以遇到“设备数量越积越多”“同一物理设备被重复入库”这类现象,第一反应应该去查地址类型,看是不是 Privacy 相关的地址在捣乱。

3.3 Non-resolvable 能不能发起连接?

这个问题值得单独说。Non-resolvable Private Address 不是不能用来发起连接,而是在连接建立后,对端设备无法通过地址识别你。假设你的传感器用 NRP 地址去连接手机,手机收到连接请求时,扫描到的地址一直在变化,它根本不知道这是已经配对过的设备,还是陌生设备。如果你们之间又没有绑定关系,手机大概率会直接忽略或者进入普通未配对流程。反过来,如果手机用白名单过滤,只允许“已知设备”连接,而你的地址每过一段时间就变了,那白名单也形同虚设。

所以 Non-resolvable Private Address 更适合广播、扫描这些“不需要长期身份”的场景。真正常用的连接型隐私方案,是下面要讲的 Resolvable Private Address。

4. Resolvable Private Address:既隐藏又能认出来

4.1 核心原理:用 IRK 加密生成地址

标题虽然只提到了 Public、Random、Non-resolvable 三类,但实际开发里,Resolvable Private Address 才是隐私功能的主角。它解决了一个看似矛盾的问题:设备不想让别人通过固定地址跟踪我,但同时,我家手机要是能认出我来。

RPA 的最高两位是 11,地址的组成分两部分:24 位的 prand(随机部分)和 24 位的 hash(哈希部分)。生成过程大致是:设备自己保存一个 128 位的密钥,叫 IRK(Identity Resolving Key),类似于设备的“私钥”。每次需要生成新地址时,先用随机数生成 prand,再对 prand 做哈希运算,取结果前 24 位作为 hash,最后拼成一个 48 位地址。

对端设备怎么认出来?前提是它在之前的配对过程中,已经拿到了你这个设备的 IRK。它在扫描到地址后,会把地址拆开,取出 prand,用同样的哈希算法算一次,看算出来的结果是不是跟地址里的 hash 部分一致。如果一致,说明这个地址就是用我手上这把钥匙生成的,那这台设备就是之前跟我配对过的设备。这就是“Resolvable”的含义:地址看起来是随机的,对外保密,但持有 IRK 的对端可以识别出来。

4.2 连接之后怎么保证不掉线

这里有一个容易混淆的点:RPA 的地址是定期变化的,那设备正在连接的时候,地址变化会不会导致断连?答案是不会。BLE 的连接链路在建立之后,链路层地址已经固定作用在当前连接上,中高层不再依赖地址去寻址。地址变化发生在每次广播、扫描、重新连接时。也就是说,设备在断网外广播时用地址 A,过一会儿换地址 B,已经建立的连接不受影响;但如果它断开后想重新连接到手机,就必须用新的 RPA,手机再通过 IRK 解析出来,才能知道“哦,还是原来那台设备”。

这也是为什么你在 iOS 和 Android 上看到的蓝牙设备 ID 呈现方式完全不同。iOS 出于隐私保护不暴露底层链路层地址,给上层的是一个 UUID;Android 在某些版本里直接暴露 MAC 地址,但地址可能是随机的。如果你在开发上层应用,直接拿系统返回的 MAC 地址当设备唯一标识,在 RPA 环境下会非常痛苦。正确做法是:先做 Bonding(配对绑定),把 IRK 和 Identity Address 保存下来,后续靠绑定信息识别设备,而不是靠扫描到的瞬时地址。

4.3 三种随机地址的边界:什么时候该用谁

实操经验里,我用一句话帮助团队做选型:你希望设备“稳定可见”就用 Static Random,你希望设备“彻底匿名”就用 Non-resolvable Private,你希望设备“既匿名又能被信任方识别”就用 Resolvable Private。

需要注意,Static Random Address 和 Non-resolvable Private Address 之间有一道分界线后,不能随便混用。有人为了省事,把 Non-resolvable 地址也当成“会变的随机地址”,在程序里直接保存下来用于回连,结果设备重启后地址变了,回连失败。而 Static Random 地址虽然用户可能没给它烧录一个漂亮的 OUI,但只要你在 SDK 里固定下来,它实际上是可以长期稳定工作的。我自己做低功耗外设时,如果没有严格的防追踪需求,倾向使用固化后的 Static Random Address,省心,也方便用手机扫描工具调设备。

5. 实操:如何查看与配置 BLE 地址类型

5.1 抓包软件里的地址类型怎么看

排查问题永远要先看事实。最简单的方式是用抓包工具。PC 端可以用 nRF52840 Dongle 配合 Wireshark 的 nRF Sniffer 插件,抓空中的广播包和连接包。打开广播包后,展开 Bluetooth LE Link Layer 字段,能看到AdvA: xx:xx:xx:xx:xx:xx,紧接着下面就会标注Address Type: Public或者Random,如果是 Random,还会继续细分 Static、Non-resolvable、Resolvable。

很多工程师抓包之后只盯着地址本身,忽略地址类型字段,这是不对的。同样是4A:C5:...这样的地址,如果类型是 Static Random,你固化下来没问题;如果类型是 Resolvable Private,你固化下来就是给自己挖坑。抓包看到的现象一定要和地址类型绑定起来看。

如果手头没有专业抓包硬件,手机也能凑合。Android 手机在开发者选项里打开“蓝牙 HCI 信息收集日志”,再用 Bug Report 或日志导出功能拿到 HCI 日志,放到 Wireshark 里同样能看地址类型。iOS 的隐私限制比较严,不容易直接拿到链路层日志,但如果自己用 CoreBluetooth 开发,可以从系统返回的CBPeripheral.identifier变化频率侧面判断对方是否在使用隐私地址。

5.2 手动解析地址类型的小工具

很多团队喜欢把抓包文件导出后,自己写脚本统计“哪些设备在广播”。这时候如果你不理解地址类型,统计结果一定是错的。我写过一个非常简单的 Python 函数,用于在事后分析时按地址类型归类:

def parse_ble_addr_type(addr_int: int) -> str: # addr_int: 48 位的链路层地址 top_two = (addr_int >> 46) & 0b11 if top_two == 0b00: return "Public Device Address" if top_two == 0b01: return "Static Random Address" if top_two == 0b10: return "Non-resolvable Private Address" if top_two == 0b11: return "Resolvable Private Address" return "Unknown"

这里核心逻辑就是提取最高两位 bit。需要注意的是,市面上不同抓包脚本展示地址时,字节序处理不统一。有的把地址按大端字符串输出,有的按蓝牙传输序输出,导致你从字符串里取最高字节时容易取反。建议先从数据源里拿到原始 48 位整数值再做解析,而不是对显示字符串盲猜。在我实际处理过的日志里,因为工具字节序问题把 RPA 判断成 Public 的例子有两三次,每次排查都要花掉半天时间。

如果你要在开发板上配置 Static Random Address,可以根据自己 SDK 的接口来处理。例如很多协议栈里都提供类似set_random_address(addr)的接口,你只需要准备一个合法的 48 位地址传入。这里也写一个生成合法 Static Random 地址的参考逻辑:

import secrets def generate_static_random_addr() -> int: val = secrets.randbits(48) val &= ~(0b11 << 46) # 先清空最高两位 val |= (0b01 << 46) # 最高两位置为 01 # 规范要求剩余 46 位不能全 0 或全 1,随机情况下概率极低 return val

把生成的整数转成 6 字节,存进 Flash,下次启动直接读回来设置,就能固定住这个“伪静态地址”。注意,不同芯片的 API 对字节序要求不一样,传参前要看清楚 SDK 里地址数组是[LSB, ..., MSB]还是反过来,这是最容易踩到的地方。

5.3 平台层统一识别方案

做应用层开发的同学,最头疼的就是“MAC 地址在 iOS 和 Android 上拿到的完全不一样”。这里直接给结论:iOS 从CBPeripheral.identifier拿到的是一个 UUID,这个 UUID 在系统重新启动、设备卸载 App 后可能变化;Android 6 以后获取蓝牙地址需要定位权限,Android 8 以后很多设备返回的是随机地址,不是 Public Device Address。所以两个平台都不建议直接拿“MAC 字符串”作为设备唯一 Key。

更可靠的做法是用 BLE 的配对绑定机制:扫描到设备后,发起配对,配对成功系统会保存 Bonding 信息,绑定后的 Identity Address 才会稳定下来。Android 侧可以通过BluetoothDevice.getBondState()判断绑定状态,配合getAddress()使用,但要注意这个地址是否来自绑定信息。iOS 侧则是依赖系统的CBPeripheralManager和蓝牙配对数据库。总结成经验法则:能依赖绑定成功后拿到的 Identity Address,就不要依赖扫描阶段的瞬时地址。

6. 常见问题与排查实录

6.1 设备 MAC 地址为什么一直在变

这个问题是被问得最多,也是最容易自己解决的。先抓包或者用手机扫描日志,确认地址类型。如果是 Non-resolvable Private Address,那它本来就会周期变,你要做匿名广播就别把这个当设备 ID;如果是 Static Random Address,但每次重启都变,说明固件初始化时没有固化地址,需要把地址保存到 Flash;如果是 Resolvable Private Address,变化是正常的,但需要在配对后保存 IRK 和 Identity Address,否则无法回连。

我遇到过一个实际案例:一批传感器使用某个国产芯片,初始固件里没有做地址固化,每次 OTA 重启后地址都变。后台服务器每 5 分钟收到一条“新设备上线”的消息,实际只有 30 台设备,后台却录了上千条记录。最后定位到原因,就是芯片 SDK 默认会调用一次随机地址生成,而不是读取保留寄存器。给协议栈打了一个补丁,把首次生成的 Static Random Address 写入 Flash,问题立刻解决。

6.2 为什么应用层拿到的 “MAC” 和抓包看到的不一样

这个也不少见。手机系统拿到的地址,和空中抓包看到的链路层地址,有时会不一样。原因有两个:一个是系统做了地址随机化,扫描阶段用随机地址,连接成功后换成身份地址;另一个是抓包工具和手机日志导出的时间点不一致,刚好跨越了地址更新周期。尤其在 iOS 平台上,App 根本拿不到链路层地址,看到的 UUID 是系统映射出来的,不代表实际空中的地址。

所以排查时,建议把“空中地址”“系统接口地址”“上层应用 UUID”这三个概念区分开。它们可能相同,也可能不同,但都对。不要一发现不一致就认为抓包工具坏了或者系统出 Bug。经验是,凡是要对设备做长期识别,都优先依赖配对绑定之后的 Identity Address,或者设备广播数据里自定义的设备序列号,而不是依赖链路层 MAC。

6.3 地址套路排查速查表

现象可能原因处理思路
同一设备扫描地址周期变化使用了 Non-resolvable / Resolvable Private确认场景是否需要隐私保护
每次重启地址都变Static Random 未固化将地址存入 Flash 或配置为固定值
手机 App 显示 MAC 与抓包不同系统层随机/隐私策略以绑定后的 Identity Address 为准
白名单过滤失效地址变化后未匹配改用 IRK / 绑定信息过滤
设备数量重复入库把隐私地址当设备 ID使用广播数据中的自定义序列号
前缀和厂商对不上手动配置或伪 OUI结合服务 UUID 等综合判断

把这张表存下来,下次遇到地址相关的问题,先按现象对号入座,能省去大量周旋时间。

6.4 再说一点关于绑定和隐私的坑

最后提醒一件事:如果你的设备支持配对绑定,并且开启了隐私功能,一定不要只保存对端的瞬时地址。正确做法是,在配对完成事件里读取 Identity Address 和 IRK,保存到 Flash。后续不管对端换多少次 RPA,你都能通过 IRK 解析出来。很多低功耗手环项目,设备在手机侧表现为“连接一次后,下次扫描不到”,十有八九就是因为只存了瞬时地址,没有存绑定信息。

我在实际项目里见过最隐蔽的问题:某个模组默认开启了“Privacy 1.2”,广播地址每 15 分钟换一次,但调试工具的过滤规则还写死了 Public Address,结果现场调试时设备时远时近、时有时无,排查了整整一天,最后把抓包里面的地址类型字段一展开,真相大白。从那以后,我把“先看地址类型”写进了团队调试规范,任何蓝牙连接疑难杂症,第一件事就是抓包,第二件事就是看 AdvA 的地址类型。这个习惯,比记忆任何 API 都管用。

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

Linux发行版全解析:从分支谱系到实战选型指南

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

作者头像 李华
网站建设 2026/9/30 1:38:39

C++ httplib 库实战:从 HTTP 客户端到 HTTPS 服务端的完整指南

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

作者头像 李华
网站建设 2026/9/30 1:38:35

HGE引擎下超级玛丽源码解析与VS2019编译实战

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

作者头像 李华
网站建设 2026/9/30 1:38:35

机房UPS供电系统配置计算:蓄电池、空开与电缆选型方法

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

作者头像 李华
网站建设 2026/9/30 1:38:29

C++类型转换全解析:隐式转换、显式转换与四种cast避坑

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

作者头像 李华
网站建设 2026/9/30 1:37:21

FreeRTOS嵌入式移植四大生死关卡与多核协同实战

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

作者头像 李华