虚拟知识图谱实战:用Ontop实现数据语义集成与逻辑统一访问
1. 从数据孤岛到语义互联为什么我们需要虚拟知识图谱如果你在数据领域工作过几年大概率会碰到这样的场景业务部门提了个需求需要整合销售、供应链和客户反馈数据做一个全面的产品分析。你打开数据库发现销售数据在MySQL里供应链数据在Oracle里客户反馈是半结构化的JSON文件躺在HDFS上。接下来你开始写ETL脚本建数据仓库定义一堆映射规则吭哧吭哧搞了几周终于把数据“搬”到了一起。然而业务逻辑一变或者源头数据结构一调整整个流程又得重来一遍。这种“物理集成”的模式不仅耗时耗力更关键的是它把数据和特定的存储格式、查询语言SQL死死绑定在了一起数据本身蕴含的丰富语义比如“客户A购买了产品B”中的“购买”关系在搬运过程中几乎丢失殆尽。这就是传统数据集成方法的痛点也是虚拟知识图谱Virtual Knowledge Graph VKG技术要解决的核心问题。它不主张把数据从一个地方搬到另一个地方而是换了一种思路在数据源之上构建一个统一的、语义化的“视图”或“接口”。这个视图就是知识图谱。用户或应用程序通过SPARQL一种用于查询知识图谱的语义查询语言与这个虚拟的知识图谱交互而底层的数据依然安安稳稳地待在原来的MySQL、Oracle、CSV文件里。负责实现这种“魔法”的中间件就是Ontop。你可以把Ontop想象成一个极其聪明的“翻译官”和“导游”。它手里有两份关键地图一份是描述你业务领域内概念、属性和关系的“本体”Ontology比如定义了“客户”、“产品”、“购买”这些术语以及它们之间的联系另一份是描述这些抽象概念如何对应到底层具体数据库表、字段的“映射”Mapping规则。当用户用SPARQL语言问“找出所有购买了‘高端系列’产品且给出过负面反馈的客户”时Ontop会立刻行动它先理解这个SPARQL查询的语义意图然后根据映射规则将其“翻译”成一系列针对不同底层数据源的高效SQL查询执行这些查询最后再将SQL的结果“组装”回符合知识图谱形式的答案返回给用户。整个过程数据没有发生移动用户也无需关心数据到底存在哪里、是什么结构。所以Ontop虚拟知识图谱的核心价值在于“虚拟”和“语义”。它实现了数据的“逻辑集成”而非“物理集成”大幅降低了集成和维护成本。同时它通过本体引入了丰富的语义让数据不再是冰冷的行和列而是变成了机器可理解、可推理的“知识”。这对于构建企业级数据中台、实现智能问答、辅助复杂决策等场景意义重大。接下来我们就一步步拆解如何让这个“翻译官”为你工作。2. 核心组件拆解本体、映射与SPARQL端点要玩转Ontop必须吃透它的三个核心组件本体、映射和SPARQL端点。它们构成了Ontop工作的流水线理解每一环的设计意图和实操细节是避免后期踩坑的关键。2.1 本体定义业务的“通用语言”本体听起来很哲学但在Ontop里它就是一个用RDF SchemaRDFS或Web本体语言OWL写的“数据模型”文件。它的核心作用是建立一套关于你业务领域的、无歧义的词汇表和基本规则。举个例子假设你的公司有“员工”和“顾问”两个概念。在数据库A里他们可能都存在Person表里用一个type字段区分在数据库B里他们可能叫Employee和Consultant两张表。如果没有本体每次查询你都得去记这些差异。而有了本体你可以在本体文件中定义存在一个类Class叫:Person。存在两个子类SubClass叫:Employee和:Consultant它们都是:Person的子类。存在一个属性Property叫:worksFor连接:Person和:Company。这样无论底层数据如何存储你在查询时都可以统一使用:Employee、:worksFor这些术语。本体保证了语义的一致性。在实操中我建议从简单的RDFS开始用Protégé这类可视化工具来编辑。初期不要追求复杂的OWL推理先确保类、属性的层次关系定义清晰。一个常见的坑是过早引入复杂的等价类、属性链等推理这会导致查询性能急剧下降或结果出乎意料。记住本体首先是“数据字典”和“模式层”其次才是“推理引擎”。2.2 映射搭建虚拟化的“桥梁”映射是Ontop最核心、也最需要耐心的部分。它用R2RMLW3C标准或Ontop自有的Native Mapping语法精确地声明了本体中的抽象术语如何“落地”到具体的数据库表字段。以将“员工工作于部门”这个关系映射到数据库为例。假设数据库中有EMP表字段EMP_ID,NAME,DEPT_ID和DEPT表字段DEPT_ID,DEPT_NAME。用Ontop Native Mapping语法一个典型的映射规则看起来是这样的mappingId Map_emp_to_person target :emp/{emp_id} a :Employee ; :name {name} ; :worksFor :dept/{dept_id} . source SELECT EMP_ID, NAME, DEPT_ID FROM EMP这短短几行信息量极大mappingId: 给这条映射规则起个名字方便管理。target: 定义目标知识图谱中的模式。:emp/{emp_id}会生成一个唯一的URI如:emp/1001来代表这个员工实体。a :Employee声明它是一个Employee类的实例。:name {name}将数据库中的NAME字段值作为字面量Literal赋给:name属性。:worksFor :dept/{dept_id}建立了一个关系指向另一个实体部门。source: 就是一条标准的SQL查询从底层数据库取数。这里有几个至关重要的实操经验URI设计是门艺术{emp_id}这种模板化的URI生成方式最常见。确保花括号内的列能唯一标识一个实体否则会导致数据重复或信息丢失。对于复杂的联合主键可以使用{col1}_{col2}的形式。空值处理数据库中的NULL值在映射时需要特别小心。Ontop默认情况下如果生成主语URI的列为NULL整个三元组会被忽略。有时这符合预期有时则可能导致数据缺失。你需要根据业务逻辑决定是否在SQL层用COALESCE函数处理NULL。连接JOIN的映射像上面的例子worksFor的关系是通过DEPT_ID外键隐式关联的。更复杂的多表JOIN需要在source中显式写出SQL的JOIN语句。Ontop的优化器很强大但编写高效的源SQL仍然是你的责任。分步测试不要试图一次性写完所有映射。写好几条关键映射后就连接到Ontop的SPARQL端点用SELECT * WHERE {?s ?p ?o} LIMIT 10这样的简单查询测试看看生成的三元组是否符合预期。这是最快的调试方法。2.3 SPARQL端点提供统一的“查询窗口”当你配置好本体和映射文件并启动Ontop无论是作为独立应用、Spring Boot组件还是Docker容器它就会暴露出一个SPARQL端点。这个端点通常是一个HTTP接口如http://localhost:8080/sparql它接收SPARQL查询返回JSON或XML格式的结果。对于应用程序来说它不再需要知道任何关于MySQL、Oracle的事情它只需要学会“说”SPARQL。你可以使用任何SPARQL客户端如Apache Jena的fuseki、Python的SPARQLWrapper库来查询就像查询一个本地RDF数据库一样。Ontop在背后完成了所有繁重的翻译、查询下推、结果合并工作。这里的一个核心优化点是查询下推Ontop会尽其所能将SPARQL查询中的操作如过滤、排序、连接转化为SQL并下推到数据库执行。这意味着如果你在SPARQL中写了FILTER(?age 30)而?age映射自数据库的AGE列那么这个过滤条件会变成SQL的WHERE AGE 30在数据库层面执行最大化利用数据库的索引和计算能力。因此编写映射时尽量让属性直接对应到数据库的列而不是经过复杂计算的表达式这有助于下推优化。3. 从零开始一个完整的Ontop部署与查询实战理论说得再多不如动手跑一遍。我们假设一个最简单的场景有一个MySQL数据库里面有一张products产品表我们想通过Ontop将其虚拟成一个知识图谱并查询所有价格高于100的产品。3.1 环境准备与数据初始化首先确保你安装了Java 11或更高版本Ontop是基于Java的。然后从Ontop的GitHub仓库下载最新的CLI命令行界面发布包它包含了所有必需的依赖。接着准备你的数据源。在MySQL中创建数据库和表CREATE DATABASE ontop_demo; USE ontop_demo; CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), category VARCHAR(50), price DECIMAL(10, 2), in_stock BOOLEAN ); INSERT INTO products VALUES (1, Laptop Pro, Electronics, 1299.99, TRUE), (2, Desk Lamp, Home, 34.50, TRUE), (3, Wireless Mouse, Electronics, 25.99, FALSE), (4, Office Chair, Furniture, 299.99, TRUE);3.2 编写本体文件创建一个名为product-ontology.ttl的文件使用Turtle语法prefix : http://example.org/ontology# . prefix rdfs: http://www.w3.org/2000/01/rdf-schema# . prefix owl: http://www.w3.org/2002/07/owl# . :Product a owl:Class . :name a owl:DatatypeProperty ; rdfs:domain :Product ; rdfs:range xsd:string . :category a owl:DatatypeProperty ; rdfs:domain :Product ; rdfs:range xsd:string . :price a owl:DatatypeProperty ; rdfs:domain :Product ; rdfs:range xsd:decimal . :inStock a owl:DatatypeProperty ; rdfs:domain :Product ; rdfs:range xsd:boolean .这个本体定义了Product类以及它的四个数据属性。注意我们使用了xsd:前缀来指定数据类型字符串、小数、布尔值这对于后续的查询过滤和推理很重要。3.3 编写映射文件这是最关键的一步。创建product-mapping.obda文件Ontop Native Mapping格式[PrefixDeclaration] : http://example.org/ontology# ex: http://example.org/data# [MappingDeclaration] collection [[ mappingId MAP_product_id target ex:product/{id} a :Product ; :name {name} ; :category {category} ; :price {price} ; :inStock {in_stock} . source SELECT id, name, category, price, in_stock FROM products ]][PrefixDeclaration]部分定义了我们在映射中使用的前缀缩写。ex:product/{id}会为每个产品生成像http://example.org/data#product/1这样的唯一URI。source部分就是最简单的全表查询。3.4 启动Ontop并查询现在使用Ontop CLI启动SPARQL端点服务。你需要一个配置文件ontop.properties来指定数据库连接jdbc.urljdbc:mysql://localhost:3306/ontop_demo?useSSLfalseserverTimezoneUTC jdbc.user你的用户名 jdbc.password你的密码 jdbc.drivercom.mysql.cj.jdbc.Driver # Ontop 相关配置 ontop.ontologyFileproduct-ontology.ttl ontop.mappingFileproduct-mapping.obda # 指定本体的前缀用于简化查询结果中的URI ontology.prefixes : http://example.org/ontology#, ex: http://example.org/data#在命令行中运行./ontop endpoint --propertiesontop.properties如果一切顺利你会看到日志输出提示SPARQL端点已在http://localhost:8080/sparql启动。现在打开浏览器或使用curl等工具向这个端点发送SPARQL查询。我们查询价格高于100且库存为真的产品PREFIX : http://example.org/ontology# PREFIX ex: http://example.org/data# SELECT ?product ?name ?price WHERE { ?product a :Product . ?product :name ?name . ?product :price ?price . ?product :inStock true . FILTER (?price 100) } ORDER BY DESC(?price)将这个查询通过HTTP GET或POST发送到端点。你会收到一个JSON格式的响应其中包含了Laptop Pro和Office Chair的信息。注意FILTER (?price 100)这个条件已经被Ontop完美地下推成了SQL的WHERE price 100在数据库层面执行。4. 进阶挑战与性能调优指南当你成功运行了第一个Demo可能会觉得Ontop不过如此。但一旦应用到真实的生产环境面对数十上百张表、复杂的业务逻辑和性能要求时挑战才真正开始。下面分享几个我踩过坑后总结的进阶要点。4.1 处理复杂连接与视图现实中的数据库模型很少是单表查询。多表连接、嵌套查询、视图都非常常见。在映射中处理它们核心思想是在source标签内写出完整的、优化的SQL。例如产品信息可能分散在product_base和product_inventory两张表里。你的映射应该这样写mappingId MAP_product_complex target ex:product/{pb.id} a :Product ; :name {pb.product_name} ; :price {pb.msrp} ; :stockQuantity {pi.quantity} . source SELECT pb.id, pb.product_name, pb.msrp, pi.quantity FROM product_base pb JOIN product_inventory pi ON pb.id pi.product_id WHERE pi.warehouse_id WHS_01 -- 甚至可以把业务过滤条件也写进来关键经验不要试图让Ontop去“猜”如何连接表。把连接逻辑明确地写在SQL里。这给了你最大的控制权也便于你利用数据库的索引。你可以也应该直接映射数据库中的视图View视图本身就是预定义的查询逻辑这能让映射文件更清晰。4.2 优化查询性能下推、索引与物化视图虚拟化的代价是查询时需要进行额外的翻译和协调。性能优化是Ontop项目成败的关键。最大化查询下推这是最重要的原则。时刻检查Ontop生成的SQL通过日志设置logging.level.it.unibz.inf.ontopDEBUG可以查看。确保FILTER、ORDER BY、LIMIT以及基本的JOIN都被下推了。如果发现某个操作没有下推变成了内存操作通常是因为映射中的sourceSQL过于复杂或者属性映射涉及了数据库不支持的函数。简化映射尽量让一个属性直接对应一个字段。底层数据库索引是根基Ontop生成的SQL性能完全依赖于底层数据库。确保映射中sourceSQL的WHERE条件、JOIN字段上都有合适的索引。这和你优化普通SQL查询没有任何区别。谨慎使用UNION和OPTIONALSPARQL中的UNION并集和OPTIONAL可选匹配在转换为SQL时可能会生成较为复杂的查询结构特别是嵌套很深时。如果性能不佳考虑是否能在映射的SQL层面预先进行一些数据整合。物化视图作为终极武器对于极其复杂、耗时但查询模式固定的SPARQL查询虚拟化可能不再是最佳选择。这时可以考虑使用物化视图。你可以在数据库中根据这个复杂查询创建一个物化视图表然后让Ontop直接映射这个物化视图。这样查询就变成了对一张预计算好的表的简单扫描。当然这需要权衡数据的实时性你需要定期刷新这个物化视图。4.3 常见陷阱与排查技巧“无结果”或“结果不全”这是最常见的问题。首先检查映射文件的sourceSQL单独拿到数据库客户端里执行看是否能返回预期数据。其次检查URI模板中的列是否有NULL值导致整个三元组被静默丢弃。再次检查本体的定义特别是类的关系是否与映射的target一致。一个快速调试方法是写一个获取所有三元组的查询SELECT * WHERE {?s ?p ?o}看看究竟生成了什么。查询速度极慢开启DEBUG日志查看Ontop最终发送给数据库的SQL。把这个SQL复制到数据库客户端中执行用EXPLAIN命令分析其执行计划。十有八九是缺少索引或者生成的SQL包含了低效的操作如全表扫描、错误的连接顺序。优化源SQL或添加索引。内存溢出OOM如果查询返回的数据量极大几十万、上百万条Ontop在组装最终RDF结果时可能会消耗大量内存。解决方案是在SPARQL查询中务必使用LIMIT子句进行分页或者调整Ontop的JVM堆内存参数-Xmx。处理数据库方言差异Ontop支持多种数据库但不同数据库的SQL函数、日期处理等有差异。在映射的source中使用SQL函数时要小心。例如字符串连接在MySQL中是CONCAT在Oracle中是||。如果可能尽量使用兼容性高的标准SQL或者利用Ontop的DBTypeFactory进行适配。Ontop虚拟知识图谱不是一个“开箱即用一键解决所有问题”的魔法黑盒。它是一个强大的工具但需要你精心设计本体和映射深刻理解其“翻译”原理并具备扎实的数据库优化能力。当你跨越了初期的学习曲线你会发现自己获得了一种前所未有的数据集成与访问的灵活性这为上层的数据分析、智能应用打开了新的大门。

相关新闻

Agent评测体系:量化工具调用准确率与任务轨迹质量,驱动AI智能体从演示走向落地

Agent评测体系:量化工具调用准确率与任务轨迹质量,驱动AI智能体从演示走向落地

1. 项目概述:为什么我们需要关注Agent的“工具调用”与“轨迹质量”?最近和几个做AI应用落地的朋友聊天,大家不约而同地提到了同一个痛点:Agent(智能体)这玩意儿,Demo演示时惊为天人&#xff0c…

2026/10/16 18:36:56 阅读更多 →
从翻杯子问题看奇偶性不变量:数学思维与编程实战

从翻杯子问题看奇偶性不变量:数学思维与编程实战

1. 从“翻杯子”到“开关灯”:一个经典数学游戏的本质如果你家里有正在上小学高年级或初中的孩子,或者你自己对趣味数学有点兴趣,那你很可能听说过“翻杯子”或者“开关灯”这类问题。题目通常是这样的:桌上有7个杯子,…

2026/10/17 13:50:51 阅读更多 →
线程池如何调优?

线程池如何调优?

回答“线程池调优”这道题,最忌讳直接背诵《Java并发编程实战》里的书本公式(比如“CPU密集型 N1N1N1,IO密集型 2N2N2N”)。面试官听到这种标准答案通常会扣分,因为真实的生产环境远比公式复杂。 高分回答的底层逻辑是…

2026/10/17 4:55:28 阅读更多 →

最新新闻

Java泛型从入门到原理:类型擦除、通配符与实战避坑指南

Java泛型从入门到原理:类型擦除、通配符与实战避坑指南

如果你写过几年 Java,一定在某个深夜被ClassCastException支配过——那一行异常信息只告诉你哪里转失败了,就是不告诉你到底传进来的是什么。泛型就是解决这个问题的,它把本该在运行期爆发的类型问题,直接摁死在编译期。这篇就围绕…

2026/10/17 13:50:48 阅读更多 →
QT原生日志持久化方案:可配置、轮转、条件过滤的生产级实现

QT原生日志持久化方案:可配置、轮转、条件过滤的生产级实现

简介:本资源是一套基于Qt框架实现的日志管理完整源码方案,面向C桌面应用开发者,尤其适合需要在跨平台GUI项目中集成轻量级、可配置日志功能的中级程序员。它解决了Qt原生不提供滚动日志机制的痛点,支持按文件大小自动切分、时间戳…

2026/10/17 13:50:48 阅读更多 →
多媒体实验全流程:从环境搭建到特征提取与结果评测

多媒体实验全流程:从环境搭建到特征提取与结果评测

简介:面向高校“多媒体技术及应用”课程学生与Authorware初学者的实验报告资源。该报告以“文字的滚动效果”为主题,系统演示了显示、等待、运动、擦除四大图标在交互作品中的协同用法,涵盖背景导入、文字编辑、移动路径调整、擦除特效设置&a…

2026/10/17 13:50:48 阅读更多 →
[SWPUCTF 2023 秋季新生赛]Easy SMC

[SWPUCTF 2023 秋季新生赛]Easy SMC

将下述文件拉到kali命令行执行我的kali是64,所以选linux_server根据IP和端口填远程调试在此处打断点、 按F7进入GetNextNumber函数按U取消定义选中0000555555555527到00005555555557B6,此处有retn按c强制转换为代码按p转换为函数按f5查看伪函数exp impor…

2026/10/17 13:50:48 阅读更多 →
2026大平层装修高定木作品牌哪家好:口碑、优缺点与选购避坑

2026大平层装修高定木作品牌哪家好:口碑、优缺点与选购避坑

在大平层和高端大宅装修中,高定木作不仅是收纳与功能的载体,更是决定空间风格调性与生活品质的核心要素。2026年,国内高定木作市场品牌林立,不同品牌在工艺、材质、风格、价格和服务上差异显著,面对琳琅满目的选择&…

2026/10/17 13:50:48 阅读更多 →
ASP.NET WebForms 入门实战:30分钟跑通登录页

ASP.NET WebForms 入门实战:30分钟跑通登录页

简介:本资源是面向零基础初学者的《ASP.Net入门经典》系统性学习包,聚焦Web开发核心技能培养,帮助开发者快速掌握ASP.Net Web Forms、MVC架构、Razor语法、页面生命周期、数据绑定及IIS部署等关键能力,适用于高校计算机专业学生、…

2026/10/17 13:49:47 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →