📑 查看全课大纲(第 6 / 26 节)
- 1.知识图谱和语音技术概述
- 2.典型知识库项目简介
- 3.知识图谱技术概览
- 4.典型应用案例
- 5.早期知识表示简介
- 6.基于语义网的知识表示框架
- 7.典型知识库项目的知识表示
- 8.基于本体工具的知识建模实践
- 9.面向非结构化数据的知识抽取
- 10.面向结构化数据的知识抽取
- 11.面向半结构化数据的知识抽取
- 12.实践:基于百科数据的知识抽取
- 13.面向文本的知识抽取
- 14.知识挖掘
- 15.从一个例子开始
- 16.图数据库介绍
- 17.什么是知识融合
- 18.知识融合的基本技术流程
- 19.典型知识融合工具简介
- 20.典型案例简介
- 21.LIMES实战演练
- 22.知识推理
- 23.语义搜索
- 24.知识问答
- 25.IBM watson Lite
- 26.行业知识图谱应用
基于语义网的知识表示框架
约 80 分钟
RDF、RDFS、OWL 与 SPARQL:搭一套让机器共享与推理的知识表示框架
小象实战讲义 · 知识图谱
上一节我们知道,知识图谱把世界建模成一张 “实体为节点、关系为边” 的有向图。但不同站点、不同系统之间要交换、合并、查询这些图,必须先约定一套统一的 “表示语言”,否则你画的图机器读不懂、更没法和别人的图自动拼起来。本节就讲 W3C 为这件事制定的语义网技术栈:RDF 负责表示数据,RDFS/OWL 负责定义模式与本体约束,SPARQL 负责查询,再加上 JSON-LD/RDFa/Microdata 三种把语义嵌进网页的轻量写法。学完你会亲手用现代 Python 的 rdflib 建图、写本体、跑查询,并真正理解为什么业界说 “智能的数据(Smart Data)比聪明的代码更重要”。
💡 核心导读
三层技术栈:RDF(数据模型,表示三元组)→ RDFS / OWL(模式层与本体,定义类型和约束)→ SPARQL(图查询语言),对应 “表示 — 约束 — 查询” 三件事。
一切皆 URI:RDF 用 “主体 — 谓词 — 客体” 三元组描述万物,资源和属性都用 URI 全局标识;客体既可以是另一个资源,也可以是带数据类型 / 语言标签的字面量,还可以是匿名的空白节点。
开放世界假设(OWA):图里查不到一条事实,不等于它不存在;知识还可以分布式定义、靠相同的 URI 自动合并。
表达能力与推理复杂度的权衡:RDFS 能力弱但简单,OWL / OWL2 一层层加上等价、属性特性、基数、复杂类等约束,并派生出 QL / EL / RL 三个面向不同场景的子语言。
SPARQL 本质是子图匹配:用带变量的三元组模板拼成 “查询图”,在数据图上做匹配;
OPTIONAL/FILTER/UNION/CONSTRUCT/ 子查询足以表达复杂检索,还能跨知识库查询。RDF 让关系显式存在数据里:数据粒度变更时同一条 SPARQL 不用改,这是它和 “关系隐式藏在 SQL 里” 的 ER 模型最本质的分野。
1. 语义网的技术蓝图:从 Tim 的第二个梦想到 W3C 标准栈
万维网之父 Tim Berners-Lee 说他有两个梦想:第一个是连接世界上的每一个人,这个梦想已经由万维网实现;第二个是连接世界上的每一个事物,这个使命交给了语义网(Semantic Web)。连接 “人” 靠的是网页之间的超链接,而连接 “事物” 靠的是一套让机器能读懂数据语义的标准。
W3C 为此推荐了一整套语义网标准栈,自下而上大致分为三层:最底层是数据的表示(用什么格式描述实体和关系),中间是模式与本体(如何定义类型、约束和推理规则),上层是查询与推理(如何把数据取出来、推出隐含结论)。对知识图谱而言,最主要的两项技术标准就是 RDF(表示)与 SPARQL(查询),而 RDFS / OWL 则承担本体建模。
与此配套的是 关联数据(Linked Data) 的发布理念:给每个事物一个全局唯一的 URI 标识、通过 HTTP 访问它、返回 RDF 等标准格式、并在数据里链接到其他事物的 URI。遵循这些原则相互链接的开放数据集汇聚成著名的 LOD(Linking Open Data)开放数据云图:图中每个圆圈代表一个数据集(圈的大小表示规模),圆圈之间的边表示两个数据集存在实体重合(可以互相链接),颜色则区分领域(地理、媒体、生命科学等)。
本节的学习路线正好沿着标准栈自下而上:
查询层 SPARQL(三元组模板 / 子图匹配 / OPTIONAL·FILTER·UNION·CONSTRUCT)
▲
本体层 OWL / OWL2(等价、属性特性、基数、复杂类;QL / EL / RL 权衡)
▲
模式层 RDFS(Class / subClassOf / type / Property / domain / range)
▲
数据层 RDF(主体-谓词-客体三元组,URI / 字面量 / 空白节点,多种序列化)2. RDF:用三元组描述世界上的一切
2.1 RDF 三个词的含义
RDF(Resource Description Framework,资源描述框架) 这个名字已经把它的设计说清楚了:
Resource(资源):页面、图片、视频,乃至任何一个被 URI 标识的对象;
Description(描述):资源的属性、特征,以及资源与资源之间的关系;
Framework(框架):承载这些描述的模型、语言与语法。
在 RDF 中,知识总是以 三元组(triple) 的形式出现,每一份知识都可以分解为 ( subject 主体, predicate 谓词, object 客体 )。课件的经典例子是一场 CCF ADL 学术讲座:
(CCF ADL, speaker, Haofen) # CCF ADL 讲座的讲者是王昊奋(Haofen)
(CCF ADL, theme, KG) # CCF ADL 讲座的主题是知识图谱(KG)RDF 同时也是一个图模型:每条三元组都可以看成图上的一条弧 (vertex 起点, edge 边, vertex 终点),主体和客体是两个节点,谓词是连接它们的、带语义标签的有向边。
2.2 一切皆 URI,客体可以是字面量或空白节点
RDF 里的资源和属性都用 URI 来标识。一个 URI 通常由 “域名 + 路径 + 本地名(local name)” 构成全局唯一的 id,例如 http://ex.org/ccf_adl、http://ex.org/schema#speaker。为了书写简洁,RDF 引入命名空间前缀(namespace prefix),把一长串 URI 缩写成 前缀:本地名,如 ex:ccf_adl、ex-schema:speaker。
三元组的客体有三种形态,这是最容易被忽略、却最关键的细节:
另一个 URI 资源:如
ex:ccf_adl ex-schema:speaker ex:haofen,客体ex:haofen本身也是一个可被继续描述的对象(于是图能连成网);字面量(literal):一段字符串,如主题
"KG";字面量还可以用^^xsd:integer这样的方式带上 XML 数据类型(如"3"^^xsd:integer表示时长 3 小时),或用@en/@zh带上语言标签;空白节点(Blank Node):记作
_:x,表示一个匿名、没有 URI 标识的资源。它常作为连接两个 URI 资源的 “桥梁”,用来表达需要二跳(two-hop)才能说清的复杂 / 多元关系 —— 例如 “Haofen 是某一次 KG 讲座的讲者”,这次讲座本身不必单独分配 URI,就用一个空白节点占位。
下面用 rdflib 把上述要点一次性建出来,并观察 RDF 的多种序列化写法和开放世界语义:
from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import XSD
EX = Namespace("http://ex.org/")
EXS = Namespace("http://ex.org/schema#")
g = Graph()
g.bind("ex", EX)
g.bind("ex-schema", EXS)
# 课件原例:CCF ADL 邀请 Haofen 作讲者,主题是知识图谱
g.add((EX.ccf_adl, EXS.speaker, EX.haofen)) # 客体是 URI 资源
g.add((EX.ccf_adl, EXS.theme, Literal("KG"))) # 客体是字面量 literal
g.add((EX.ccf_adl, EXS.nbHours, Literal(3, datatype=XSD.integer))) # 带 xsd 数据类型
g.add((EX.ccf_adl, EXS.title, Literal("知识图谱前沿讲习班", lang="zh"))) # 带语言标签
# 空白节点:Haofen 是“某一次匿名讲座” _:talk 的讲者,该讲座主题是 KG
talk = BNode()
g.add((talk, EXS.speaker, EX.haofen))
g.add((talk, EXS.theme, Literal("KG")))
print("=== Turtle(同一主语的多条三元组用 ; 缩写,空白节点显示为 [])===")
print(g.serialize(format="turtle"))
print("=== N-Triples(每行一条完整三元组,空白节点为 _:N)===")
print(g.serialize(format="nt"))
# 开放世界假设:图里只查到 1 位讲者,含义是“至少有 1 位”,而不是“只有 1 位”
q_count = """
PREFIX ex-schema: <http://ex.org/schema#>
SELECT (COUNT(?s) AS ?n) WHERE { <http://ex.org/ccf_adl> ex-schema:speaker ?s }
"""
for row in g.query(q_count):
print("CCF ADL 已知讲者数 =", int(row.n), "(至少有这么多位,而非只有这么多位)")
q_ask = """
PREFIX ex-schema: <http://ex.org/schema#>
ASK { <http://ex.org/ccf_adl> ex-schema:speaker ?x }
"""
print("CCF ADL 是否存在讲者(ASK):", g.query(q_ask).askAnswer)运行后,Turtle 用分号 ; 把同一主体 ex:ccf_adl 的四条谓词缩写在一起、空白节点打印为 [];N-Triples 则每行写全一条三元组、空白节点打印为 _:N;统计结果为 “已知讲者数 = 1”,ASK 返回 True。
2.3 RDF 是数据模型,不是序列化格式
必须强调:RDF 本质上是一个抽象数据模型(那张图),而不是某一种文件格式。同一张 RDF 图可以用多种序列化语法写出来,它们语义等价、可以互相转换:
| 序列化格式 | 特点 |
|---|---|
| RDF/XML | 最早的 W3C 推荐格式,用 XML 标签嵌套描述,机器友好但较冗长 |
| Turtle | 最常用、最紧凑,用 ; 缩写同一主语、, 缩写同一谓词,可读性最好 |
| N-Triples | 每行一条完整三元组,不做任何缩写,便于逐行处理与交换 |
| N-Quads | 在三元组外再加一个 graph id 成为 “四元组”,用于区分同一事实来自哪个图(如 Freebase 还是 DBpedia),默认图称为 default graph |
| JSON-LD / RDFa | 面向 Web 开发者 / 网页嵌入的轻量写法,本节第 5 节专门讲 |
2.4 开放世界假设、分布式定义与带标注 RDF
开放世界假设(Open World Assumption, OWA) 与经典数据库的 封闭世界假设(CWA) 相对。在 RDF 看来,一条三元组的缺失并不是什么大不了的事:图中只有 (CCF ADL, speaker, Haofen),意味着这场讲座 “至少有一位讲者是 Haofen”,而不是 “只有 Haofen 一位讲者”。没写出来的事实,只是 “暂时不知道”,而非 “不存在”。这也是为什么后面 SPARQL 里学生没填年龄,并不代表他没有年龄。
RDF 还允许分布式地定义知识:关于 ex:haofen 的一条事实(他在 CCF ADL 讲过课)可以发布在讲座网页上,另一条事实(ex:haofen foaf:knows ex:guilin,他认识桂林)可以写在他的个人主页里。只要两处用的是同一个 URI,这些分散的三元组就能被系统自动逻辑合并成一张完整的图。如果现实中同一个对象被不同站点用了不同的 URI,则要靠第 6 章的 知识融合(实体对齐) 来归并。
此外还有带标注 RDF(annotated RDF):在标准的 “主谓宾” 之外再挂一个上下文标注 λ,记录这条事实的 来源、采集时间、时态、置信度(trust) 等。典型代表是 YAGO2 / YAGO3 对 YAGO 的时空扩展 —— 例如 “某人就任总统” 这条事实会附带生效时间段、信息来源与可信概率。这样既能表示 “何时为真、有多可信”,又能追溯出处。
3. RDFS 与 OWL:给数据装上模式层与本体约束
3.1 RDFS:为 RDF 提供类型系统
裸 RDF 只有三元组,却没有说明 “哪些谓词能用在哪些对象上”。RDFS(RDF Schema)在 RDF 之上提供了一套术语 / 概念的定义方式,也就是一个基本的类型系统(vocabulary)。它预定义的核心词汇包括:
rdfs:Class(类)、rdf:type(个体属于某个类)、rdfs:subClassOf(子类);rdf:Property(属性)、rdfs:subPropertyOf(子属性);rdfs:domain(定义域:谁能当主体)、rdfs:range(值域:客体应是什么类型)。
由此 RDF 数据被分成两层:数据层(data) 描述具体个体(如 haofen rdf:type Person、ccf_adl rdf:type Talk),模式层(schema) 描述类与属性的约束(如 speaker 的 domain 是 Talk、range 是 Person)。RDFS 还能描述不同词汇集之间的关系,从而为网络上统一格式的元数据交换打基础。课件举的例子是”把自定义的 Author 声明为都柏林核心 Creator 的子类”。这里要做一个澄清:都柏林核心官方词汇中的 dc:creator 其实是一个属性(property),作用是把一份资源链接到创作它的主体,它本身不是一个类,所以严格来讲不能写成 Author rdfs:subClassOf dc:creator(subClassOf 的两端都必须是类)。若要让自定义类与公共词汇对齐,更规范的做法是把 Author 声明为 foaf:Person(FOAF 中表示”人”的类)的子类,或在自己的命名空间下显式定义 Author 类。课件把”Creator”当作类来举例属于教学简化,读者理解其”跨词汇对齐”的意图即可。
3.2 RDFS 推理:语义感知的知识压缩
有了模式层,系统就能从显式三元组推出隐含结论。课件给了两个经典例子:
已知 “谷歌
rdf:type人工智能公司”,且 “人工智能公司rdfs:subClassOf高科技公司”,可推出 谷歌 的rdf:type为 高科技公司;已知 “大卫·切瑞顿 投资 谷歌”,且 “投资” 的
domain是投资人、range是公司,可推出 大卫·切瑞顿 的rdf:type为 投资人、谷歌是一家公司。
这类推理在知识问答里非常实用:当问句问 “某投资人投资了哪些公司” 时,可以用 domain/range 推出的类型去约束候选答案的类型,显著提升答案的置信度。课件把推理概括为 “语义感知的知识压缩”—— 你不必把每个结论都手写一遍,规则会自动把它们算出来。
rdflib 的核心库不内置 RDFS 推理机,下面用 SPARQL CONSTRUCT 把三条 RDFS 规则显式写出来并应用,再顺带解析一段 OWL 本体,让你看清推理到底做了什么:
from rdflib import Graph, Namespace
from rdflib.namespace import RDF, RDFS
EX = Namespace("http://ex.org/")
g = Graph()
g.bind("ex", EX)
# ---- 数据层 ----
g.add((EX.Google, RDF.type, EX.AICompany)) # 谷歌 rdf:type 人工智能公司
g.add((EX.David, EX.investsIn, EX.Google)) # 大卫·切瑞顿 投资 谷歌
# ---- 模式层(RDFS 本体)----
g.add((EX.AICompany, RDFS.subClassOf, EX.HighTechCompany)) # 人工智能公司 ⊑ 高科技公司
g.add((EX.investsIn, RDFS.domain, EX.Investor)) # “投资”定义域是投资人
g.add((EX.investsIn, RDFS.range, EX.Company)) # “投资”值域是公司
# 用 CONSTRUCT 把三条 RDFS 推理规则显式写出来(rdflib 核心不内置推理机)
rule_subclass = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT { ?x rdf:type ?b }
WHERE { ?x rdf:type ?a . ?a rdfs:subClassOf ?b }
"""
rule_domain = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT { ?s rdf:type ?c }
WHERE { ?p rdfs:domain ?c . ?s ?p ?o }
"""
rule_range = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT { ?o rdf:type ?c }
WHERE { ?p rdfs:range ?c . ?s ?p ?o }
"""
inferred = Graph()
for rule in (rule_subclass, rule_domain, rule_range):
for triple in g.query(rule): # CONSTRUCT 逐行产出新三元组
inferred.add(triple)
g += inferred # 把推理结论并回原图
def types(node):
return sorted(str(o).split("/")[-1] for o in g.objects(node, RDF.type))
print("谷歌 推理后的全部类型:", types(EX.Google))
print("大卫 推理后的全部类型:", types(EX.David))
# 解析一段 OWL 本体(Turtle 语法):词汇合法即可被 rdflib 解析
owl_ttl = """
@prefix ex: <http://ex.org/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:ancestor rdf:type owl:TransitiveProperty . # 传递性:祖先
ex:descendant owl:inverseOf ex:ancestor . # 互反:后裔
ex:hasMother rdf:type owl:FunctionalProperty . # 函数性:每人只有一个母亲
ex:friend rdf:type owl:SymmetricProperty . # 对称性:朋友
ex:Athlete owl:equivalentClass ex:SportsPlayer . # 两个类等价
ex:Mother owl:intersectionOf ( ex:Person ex:HasChildren ) . # 母亲 = 人 ∩ 有孩子
"""
og = Graph()
og.parse(data=owl_ttl, format="turtle")
print("OWL 本体解析成功,三元组数:", len(og))运行结果:谷歌的类型被补全为 AICompany、Company、HighTechCompany(子类规则推出高科技公司、值域规则推出公司),大卫被补全为 Investor;那段 OWL 本体被成功解析(10 条三元组)。
3.3 RDF (S) 的表达能力缺陷
RDFS 能表达简单语义,但在复杂场景下能力太弱,主要缺五类特征:
值域是全局的:
rdfs:range对属性全局生效,无法声明 “同一属性用在某个具体类上时取特殊值域”(局部值域);无法声明等价:不能说两个类、两个属性或两个个体其实是等价的;
无法声明不相交类:只能声明子类(男人、女人都是人的子类),不能声明二者互不相交;
没有基数约束:无法表达 “一个人恰有两位双亲”“一门课至少一名教师”;
无法描述属性特性:不能声明一个属性是传递的、函数的、对称的,或与另一属性互逆。
3.4 OWL 与三个子语言
为补上这些缺口,W3C 于 2002 年 7 月 31 日发布了 OWL(Web Ontology Language,Web 本体语言)工作草案,在 RDF (S) 之上扩展出表达本体的推荐语言。OWL 最初分为三个表达能力 / 可判定性不同的子语言:
| 子语言 | 定位 | 关键限制 |
|---|---|---|
| OWL Lite | 只需要分类层次 + 简单属性约束的用户 | 支持基数,但基数只能取 0 或 1 |
| OWL DL | 包含 OWL 的全部约束,基于描述逻辑 | 逻辑蕴涵可判定;一个类不能同时又是另一个类的实例 |
| OWL Full | 允许在 RDF/OWL 预定义词汇上继续扩充,带二阶逻辑特点 | 逻辑蕴涵通常不可判定;一个类可以同时是 “个体的集合” 和 “集合中的一个个体”,目前没有能完整支持它的推理系统 |
三者与 RDF 的关系是:所有 OWL 文档(Lite/DL/Full)都是 RDF 文档;反过来,所有 RDF 文档都是一个 OWL Full 文档,但只有一部分 RDF 文档是合法的 OWL Lite / DL 文档。
OWL 新增的词汇正是为补齐 RDFS 的五个缺口,常用的有:
等价性:
owl:equivalentClass(类等价,如 运动员 ≡ 体育选手)、owl:equivalentProperty(属性等价)、owl:sameAs(个体同一;早期 DAML+OIL 草案曾写作 sameIndividualAs,OWL 标准已统一为owl:sameAs);属性特性:
owl:TransitiveProperty(传递,如 ancestor:小明→小林→小志,则小明→小志)、owl:inverseOf(互逆,ancestor 与 descendant)、owl:FunctionalProperty(函数性,如 hasMother、身份证号,取值唯一)、owl:SymmetricProperty(对称,如 friend);局部取值约束:
owl:allValuesFrom(全称,如 Person 的 hasMother 只能取 Women)、owl:someValuesFrom(存在,如语义网论文 “部分发表在” AAAI)、owl:cardinality/minCardinality/maxCardinality(基数);类的集合运算与枚举:
owl:intersectionOf(交集,如 Mother = Person ∩ HasChildren)、owl:unionOf(并集,如 歌手 ∪ 演员)、owl:disjointWith(不相交)、owl:oneOf(枚举)、owl:hasValue、owl:InverseFunctionalProperty。
3.5 OWL2 的 QL / EL / RL:在表达能力与推理代价之间取舍
OWL 的新版本称为 OWL2(老版本改称 OWL1)。它通过限制语法,定义了三个更易实现、面向不同应用的子语言。核心矛盾是:本体表达能力越强,推理的计算复杂度越高——OWL Full 不可判定;可判定的 OWL DL 复杂度还随版本上升:OWL 1 DL(描述逻辑 SHOIN (D))为 NEXPTIME-complete,OWL 2 DL(SROIQ (D))进一步升到 N2EXPTIME-complete(理论上都可判定,但最坏情况的计算代价极高),而工程上可用的子语言大多把复杂度压到多项式时间(PTime)甚至更低:
| OWL2 子语言 | 基于的描述逻辑 | 复杂度 | 最适合的场景 |
|---|---|---|---|
| OWL2 QL(query language) | DL-Lite | AC0(数据复杂度极低,属常数深度电路复杂度类) | 概念层很小、实例层极多的大规模场景;通过 query rewriting(查询改写) 把带本体的查询改写成等价的普通查询,直接交给数据库执行,三者中最简单 |
| OWL2 EL | EL++ | PTime-Complete | 概念层(术语)极复杂的本体,典型如生物医疗术语本体 SNOMED CT;允许 someValuesFrom、intersectionOf、传递属性 |
| OWL2 RL | 在 RDFS 上扩展(源于 ter Horst 2005 的工作,与描述逻辑无直接关系) | PTime-Complete | 专为高效的实例数据推理设计,在 RDFS 上加入函数性 / 互反 / 对称 / 等价 / 局部约束;Apache Jena、Oracle 11g 的推理机都属于 RL 阵营 |
工具说明(不要求复现) :与这些子语言配套的是一批重型 Java 推理机 —— 描述逻辑(DL)方向有 FaCT++(曼彻斯特)、HermiT(牛津)、Pellet;EL 方向有 CEL、ELK;RL 方向有 Apache Jena、Oracle 11g;QL 方向有 QuOnto、Quill。它们是课程录制年代的主流工程,环境较重,本课程不要求搭建;本节及后续章节统一用现代 Python 的
rdflib,通过CONSTRUCT规则或 SPARQL 属性路径把这些推理机制的 核心原理 演示清楚。
4. SPARQL:在知识图谱上做 “子图匹配” 查询
4.1 SPARQL 是什么、查询长什么样
SPARQL(SPARQL Protocol and RDF Query Language)是 RDF 的查询语言,基于 RDF 数据模型,可以对不同数据集撰写复杂的连接(joins),并被所有主流图数据库支持。如果说关系数据库对应 SQL,那么 RDF 图对应的就是 SPARQL。
一条 SPARQL 查询和 SQL 结构很像,也有 PREFIX、FROM、SELECT、WHERE、ORDER BY 等部分,但有一个关键区别:SQL 的 FROM 后面是一张表(table),而 SPARQL 的 FROM 后面是一个 RDF 数据集(dataset,由一个默认图和若干命名图组成);三元组本身只有主、谓、宾三列,图名(graph name)属于数据集这一层,并不是每条三元组隐含自带的字段。三个最基本的概念是:
变量:RDF 中的资源位置若用
?或$标记(如?student),就成了变量;三元组模板(triple pattern):写在
WHERE子句里、允许包含变量的三元组,用来描述 “想找什么样的关系”;SELECT子句:声明最终要返回哪些目标变量。
最简单的查询 “找出所有选修 CS328 课程的学生” 如下(查默认图时不需要 FROM):
PREFIX exp: <http://www.example.org/>
SELECT ?student
WHERE {
?student exp:studies exp:CS328 .
}4.2 OPTIONAL / FILTER / UNION / FROM 四个常用关键字
OPTIONAL(可选匹配,对应 SQL 的左连接 LEFT JOIN):查选修 CS328 的学生及其邮箱时,把邮箱放进OPTIONAL块,那么即使某学生没有邮箱,也会返回这条记录,只是邮箱处留空(未绑定)。FILTER(过滤):对三元组模板里的某个变量追加约束,通常针对字面量或带数据类型的值,例如FILTER (?age > 25)。年龄放在OPTIONAL里表示 “可以没填年龄,但填了就必须大于 25”—— 这正体现了开放世界假设:没填年龄不等于没有年龄,只是暂不知道。UNION(并集):查选修 CS328 或 CS909 的学生。要特别注意它和OPTIONAL的区别:如果邮箱模式写在UNION之外、又没用OPTIONAL包裹,那么邮箱就是必填项,没有邮箱的学生整条记录都不返回。FROM/FROM NAMED:指定要查询的默认图 / 命名图(一个 RDF 数据集可包含多个带名字的图),写在查询头部(WHERE之前);进入WHERE后若要把某段三元组模板限定到某个命名图,需用GRAPH关键字,而不是在WHERE里再写FROM。
下面这段代码先在学生选课数据上演示 OPTIONAL / FILTER / UNION,再用课件的金融收购例子演示一次经典的 “两跳” 图查询:
from rdflib import Graph, Namespace, Literal
from rdflib.namespace import RDF, FOAF, XSD
EXP = Namespace("http://www.example.org/")
g = Graph()
g.bind("exp", EXP)
g.bind("foaf", FOAF)
for t in [
(EXP.zhang, EXP.studies, EXP.CS328),
(EXP.zhang, FOAF.mbox, Literal("zhang@example.org")),
(EXP.zhang, EXP.age, Literal(27, datatype=XSD.integer)),
(EXP.li, EXP.studies, EXP.CS328), # 小李:没有邮箱、年龄 23
(EXP.li, EXP.age, Literal(23, datatype=XSD.integer)),
(EXP.wang, EXP.studies, EXP.CS909),
(EXP.wang, FOAF.mbox, Literal("wang@example.org")),
]:
g.add(t)
# OPTIONAL:选修 CS328 的学生及邮箱(无邮箱也返回,邮箱为空)
q_optional = """
PREFIX exp: <http://www.example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?student ?email WHERE {
?student exp:studies exp:CS328 .
OPTIONAL { ?student foaf:mbox ?email }
}
"""
print("--- OPTIONAL ---")
for r in g.query(q_optional):
print(" ", r.student.split("/")[-1], "|", r.email)
# FILTER:选修 CS328 且年龄 > 25
q_filter = """
PREFIX exp: <http://www.example.org/>
SELECT ?student ?age WHERE {
?student exp:studies exp:CS328 .
?student exp:age ?age .
FILTER (?age > 25)
}
"""
print("--- FILTER(age>25) ---")
for r in g.query(q_filter):
print(" ", r.student.split("/")[-1], "|", int(r.age))
# UNION:选修 CS328 或 CS909,且必须有邮箱(无邮箱整条不返回)
q_union = """
PREFIX exp: <http://www.example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?student ?email WHERE {
?student foaf:mbox ?email .
{ ?student exp:studies exp:CS328 } UNION { ?student exp:studies exp:CS909 }
}
"""
print("--- UNION(邮箱必填)---")
for r in g.query(q_union):
print(" ", r.student.split("/")[-1], "|", r.email)
# 金融收购:8 条三元组构成的小型图谱
FIN = Namespace("http://ex.org/finance#")
fg = Graph()
fg.bind("fin", FIN)
for t in [
(FIN["融创中国"], RDF.type, FIN["地产事业"]),
(FIN["孙宏斌"], FIN.control, FIN["融创中国"]),
(FIN["贾跃亭"], FIN.control, FIN["乐视网"]),
(FIN["孙宏斌"], FIN.hold_share, FIN["乐视网"]),
(FIN["王健林"], FIN.control, FIN["万达集团"]),
(FIN["万达集团"], FIN.main_income, FIN["地产事业"]),
(FIN["融创中国"], FIN.acquire, FIN["乐视网"]),
(FIN["融创中国"], FIN.acquire, FIN["万达集团"]),
]:
fg.add(t)
# 两跳查询:?P 掌控 ?c,?c 又收购 ?X —— 谁通过自己掌控的公司收购了谁
q_acq = """
PREFIX fin: <http://ex.org/finance#>
SELECT ?P ?X WHERE { ?P fin:control ?c . ?c fin:acquire ?X }
"""
print("--- 两跳子图匹配:control → acquire ---")
for r in fg.query(q_acq):
print(" ", r.P.split("#")[-1], "→ 收购 →", r.X.split("#")[-1])运行结果:OPTIONAL 下小李(li)依然返回、邮箱为 None;FILTER 只留下 27 岁的 zhang;UNION 因邮箱必填而剔除了小李,只返回 zhang 和 wang;金融两跳查询锁定 孙宏斌 →(融创中国)→ 收购乐视网、收购万达集团 两条结果。
4.3 查询的本质:查询图在数据图上做子图匹配
上面的金融查询为什么能两跳命中?因为 SPARQL 的 WHERE 块本身也是一张图 —— 称为查询图:?P -control→ ?c -acquire→ ?X,其中红色的 ?P、?X 是 SELECT 的目标变量,?c 是中间桥梁。SPARQL 执行的过程,就是拿这张查询图到 RDF 数据图上去做子图匹配(subgraph matching),把能对上的节点绑定给变量返回。至于 “怎么匹配得快”,则是各类图数据库在存储与索引层面要解决的问题。
除了 SELECT,SPARQL 还提供多种查询形态:CONSTRUCT 返回一张构造出来的新图(前面做 RDFS 推理时用过)、ASK 只返回 yes /no、SPARQL 1.1 还支持 INSERT / UPDATE 写操作,以及 聚合、子查询(WHERE 中嵌套 SELECT)、属性路径(property path)、否定 等更复杂的构造。
4.4 为什么离不开本体层:从一个 “成员 + 亲戚” 查询说起
RDF 给了建模灵活性,但如果没有上层 schema,查询会变成噩梦。设想需求是 “找出这样的人:他是某机构的成员,而该机构由他的亲戚创办”。难就难在 —— 什么叫 “成员(member)”?它可能是普通成员、董事会成员、CEO、雇员、雇主、实习生;什么叫 “亲戚(relative)”?可能是兄弟、父亲、母亲、子女。如果没有本体,你只能在 SPARQL 里把这十几种谓词用一大堆 UNION 手工枚举,查询又长又脆。
有了本体就清爽了:在模式层把 worksFor / employee / boardMember / CEO … 都声明为 member 的子属性,把 father / mother / … 声明为 relative 的子属性(并可声明 relative 对称、parent 与 child 互逆)。于是自然语言问题只需映射成一条用 member / relative 书写的、非常简洁的 SPARQL,再由推理机把它等价展开成那条冗长但完整的查询 —— 这正是 OWL 2 QL 的 query rewriting(查询改写) 思想。要特别注意:SPARQL 本身只按图模式匹配求值,本体并不会让 SPARQL 引擎自动改写查询;改写是由推理机或 OBDA(基于本体的数据访问)层依据本体规则完成的。SPARQL 是一门声明式语言:你只描述 “想要什么”,在配置了相应推理机制(entailment regime)的引擎中,本体与规则才会把它等价展开。
4.5 业务规则推理与跨知识库查询
有些知识不适合写进本体,而是业务规则。课件的 “关联交易” 例子:数据里并没有 conn_trans(关联交易)这个属性,它由规则推出 ——
hold_share(X, Y) :- control(X, Y) # 掌控一家公司,即持有其股份
conn_trans(Y, Z) :- hold_share(X, Y), hold_share(X, Z) # 同一方持股的两家公司构成关联交易把规则展开,控制 / 持股两种关系两两组合共有 4 种情形(control-control、hold_share-hold_share、control-hold_share、hold_share-control),最初要用 4 个 UNION 块枚举;利用 SPARQL 1.1 的子查询(在 WHERE 里嵌套 SELECT)则可以大幅简化写法。
SPARQL 还擅长跨知识库(联邦)查询,这是它相对跨库 SQL 的显著优势。课件的 “新药发现” 案例:阿尔茨海默症(老年痴呆症)目前无法治愈,研究希望找到药物靶标—— 已知 “信号转导通路(signal transduction pathways)在药物靶标中很丰富(即对化学疗法有反应的蛋白)”,且 “CA1 锥体神经元在阿尔茨海默症中受损”,于是想找出 “既参与信号转导、又活跃在锥体神经元中的候选基因”。这需要同时跨四个数据源:医学主题词 MeSH(含锥体神经元)、文献库 PubMed、基因库 Entrez Gene、基因本体 GO(Gene Ontology,含信号转导通路)。
这里要特别注意:这四个数据源是各自独立部署的远程 SPARQL 端点,并不在同一个本地 RDF 数据集里。因此不能用 FROM NAMED 把它们当成本地命名图来加载——FROM / FROM NAMED 只能声明本引擎已经持有的数据集里的图。SPARQL 1.1 为此专门引入了联邦查询(Federated Query)机制,用 SERVICE <端点IRI> { ... } 把要下发给某个远程端点执行的图模式包起来;查询引擎会自动把 SERVICE 子查询发往该端点、取回结果后再与其他图模式在本地做连接(join):
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX go: <http://example.org/ontology/go/>
PREFIX mesh: <http://example.org/mesh/>
SELECT DISTINCT ?gene
WHERE {
# 把这段图模式下发到远程 GO 端点:参与"信号转导通路"的基因
SERVICE <http://example.org/endpoint/go> {
?gene rdfs:subClassOf go:signal_transduction .
}
# 把这段图模式下发到远程 MeSH/文献端点:活跃在 CA1 锥体神经元中的基因
SERVICE <http://example.org/endpoint/mesh> {
?gene mesh:associatedWith mesh:CA1_pyramidal_neuron .
}
}在 SERVICE 块内部仍可按需表达 partOf、subClassOf 等推理需求(由对应端点自身的 entailment regime 负责展开)。这样一条 SPARQL 即可同时向多个远程端点发问、在本地做连接,缩小候选基因的搜索空间 —— 这比在关系数据库里手工跨库连接要方便得多。
5. 把语义嵌进网页:JSON-LD、RDFa、Microdata,以及 RDF 与 ER 的本质分野
5.1 三种轻量级网页语义表示
很多程序员不懂 RDF,但几乎都用过 JSON / HTML。于是有了三种让普通网页也能承载语义的轻量写法,目标是让页面 “人看着正常渲染、机器能抽出三元组”。三者与 RDF 的关系并不完全相同:JSON-LD 与 RDFa 本身就是 RDF 的具体语法、直接采用 RDF 数据模型;HTML5 Microdata 则是一套独立的网页标记机制,需要通过 W3C 规范定义的 Microdata-to-RDF 映射,才会产出 RDF 三元组。
(1)JSON-LD(JavaScript Object Notation for Linked Data):用 JSON 表示和传输关联数据、描述有向图,语法与普通 JSON 完全兼容。问题在于,普通 JSON 里的 name、homepage、image 这些 key 是人能猜、机器不懂的;JSON-LD 的做法是 把 key 统一成 Schema.org 等公共词汇的 URI,并对取值为资源(而非字符串)的字段用 @id 标注:
// 普通 JSON:机器不知道 name / homepage / image 是什么含义
{
"name": "Manu Sporny",
"homepage": "http://manu.sporny.org/",
"image": "http://manu.sporny.org/images/manu.png"
}// JSON-LD:key 用 schema.org 的 URI 标准化,资源型取值用 @id 标识为 URI
{
"http://schema.org/name": "Manu Sporny",
"http://schema.org/url": { "@id": "http://manu.sporny.org/" },
"http://schema.org/image": { "@id": "http://manu.sporny.org/images/manu.png" }
}JSON-LD 呈现了语义网 “围绕某类知识提供共享术语、不为 name 重复发明概念” 的目标,却刻意没有采用 Turtle / SPARQL / 四元组存储这套较重的技术栈,而是以简单、面向普通开发者的方式推进 —— 能解析 JSON 的工具基本都能处理它,PostgreSQL、MySQL 等数据库也都能存储和查询。
(2)RDFa(Resource Description Framework in attributes):W3C 推荐的网页标记标准,通过扩充 XHTML 的几个属性把三元组直接嵌进网页标签。它用 about 指定主体 URI、用 property 指定谓词、标签内文本就是客体:
<div xmlns:dc="http://purl.org/dc/elements/1.1/"
about="http://www.example.com/books/wikinomics">
<span property="dc:title">Wikinomics</span>
<span property="dc:creator">Mr right</span>
<span property="dc:date">2006-09-02</span>
</div>这就生成了 “该书标题 Wikinomics、创建者 Mr right、日期 2006-09-02” 三条三元组(词汇来自都柏林核心 Dublin Core),浏览器渲染照常、支持 RDFa 的搜索引擎却能直接解析。
(3)HTML5 Microdata(微数据):用带作用域的键值对给 DOM 打标记,通过 itemscope 划定实体范围、itemtype 指定实体类型、itemprop 标注属性,还允许自定义词汇表:
<section itemscope itemtype="http://data-vocabulary.org/Person">
<h1 itemprop="name">Andy</h1>
<p><img itemprop="photo" src="http://www.example.com/photo.jpg"></p>
<a itemprop="url" href="http://www.example.com/blog">My Blog</a>
</section>这定义了 “Andy 是一个 Person,具有 name /photo/url 属性” 共四条三元组(photo 复用 <img> 的 src、url 复用 <a> 的 href 作为属性值)。
三者的共同价值是:网页在保持人可读的同时,让搜索引擎爬虫能解析出语义标注、形成一个个片段化的小知识图谱,再彼此关联、反过来扩充搜索引擎自身的知识图谱。
5.2 RDF + SPARQL vs ER + SQL:关系是显式的还是隐式的
关系数据库的逻辑基础是关系模型(Relational Model);而 ER(Entity-Relationship,实体 - 关系模型) 主要用于数据库的概念设计阶段,画好的 ER 图再映射成一张张关系表。语义网这边则是 RDF + SPARQL。课件用同一个问题 “腾讯位于哪个国家?” 把两者的差别讲得非常透彻。
在 RDF 语义模型里,关系是显式定义的(relationships are explicit in the model):数据是 Tencent located_in Shenzhen、Shenzhen located_in China 两条三元组,再把 located_in 声明为 owl:TransitiveProperty(传递属性),系统就能推理出 Tencent located_in China,直接回答 “In China”。关系就明明白白写在图里、对应用程序直接可用。
在 ER 关系模型里,关系是隐式声明的(relationships are implicit):你需要建公司表、城市表、公司 - 城市关联表共三张表,用外键(CO_ID、City_ID)连起来,再写一条三表 JOIN 的 SQL 才能取出国家:
-- 关系藏在表结构、外键和这条 JOIN 里;机器并不“理解” ID=CO_ID 是什么语义
SELECT Country
FROM Company_Table, City_Table, Company_City_Table
WHERE Company_Name = 'Tencent'
AND ID = CO_ID AND City = City_ID;那么 “关系到底存在哪?” 数据定义语句(DDL)应用程序并不使用、不具描述性、作用域还仅限单个数据库;数据字典 / 数据注册中心是给人看的;实际上关系藏在文档、SQL 代码和开发者的 集体记忆(collective memories) 里,应用程序拿不到。
5.3 数据变更时,谁更从容
最能说明问题的是数据粒度变更。假设地理信息要变细:不再直接写 “深圳位于中国”,而是 “深圳位于广东、广东位于中国”。
RDF 这边:只需删掉旧三元组、插入
Shenzhen located_in Guangdong与Guangdong located_in China两条新三元组。靠located_in的传递性,Shenzhen located_in China、Tencent located_in China会被重新推理出来,同一条 SPARQL 查询一个字都不用改,答案仍是 China。ER 这边:三张表不够用了,必须新增一张省份表(State Table)和城市 - 省份关联表(变成四张表),原来那条三表 JOIN 的 SQL 不改就只能查出空结果(Get No Answer),必须重写查询、改动应用,代价高昂,以至于业界对这类结构变更 “不惜一切代价也要避免”。
下面用 rdflib 的 SPARQL 1.1 属性路径 located_in+(在查询时计算一条或多条边的传递闭包)实证 RDF 这边 “查询不变”。要特别注意:属性路径是查询语言自身的特性,在查询时临时计算闭包,它既不要求、也不检查该谓词是否被声明为 owl:TransitiveProperty,因此无需推理机;这与「显式声明传递属性 + 启用推理机」是两条不同的技术路线:
from rdflib import Graph, Namespace
GEO = Namespace("http://ex.org/geo#")
g = Graph()
g.bind("ex", GEO)
g.add((GEO.Tencent, GEO.located_in, GEO.Shenzhen))
g.add((GEO.Shenzhen, GEO.located_in, GEO.China))
# 属性路径 located_in+ 在查询时直接计算“一条或多条 located_in 边”的闭包;
# 它是 SPARQL 查询语言特性,不要求谓词声明为 owl:TransitiveProperty,因此无需推理机。
# (若走 OWL 传递属性推理这条路线,则必须显式声明 TransitiveProperty 并启用推理机。)
q_path = """
PREFIX ex: <http://ex.org/geo#>
SELECT ?place WHERE { ex:Tencent ex:located_in+ ?place }
"""
def reachable():
return [r.place.split("#")[-1] for r in g.query(q_path)]
print("变更前,腾讯沿 located_in 可逐级到达:", " → ".join(reachable()))
# 需求变更:地理粒度变细,插入“广东省”,去掉深圳直连中国
g.remove((GEO.Shenzhen, GEO.located_in, GEO.China))
g.add((GEO.Shenzhen, GEO.located_in, GEO.Guangdong))
g.add((GEO.Guangdong, GEO.located_in, GEO.China))
print("变更后,腾讯沿 located_in 可逐级到达:", " → ".join(reachable()))
print("(同一条 SPARQL 一字未改,China 始终在传递闭包中,答案不变)")运行结果:
变更前,腾讯沿 located_in 可逐级到达: Shenzhen → China
变更后,腾讯沿 located_in 可逐级到达: Shenzhen → Guangdong → China
(同一条 SPARQL 一字未改,China 始终在传递闭包中,答案不变)5.4 从 “哑巴数据 + 聪明代码” 到 “智能数据 + 统一推理”
对比至此,就能回答 “数据的智能性到底体现在哪”。今天绝大多数企业应用的基石仍是关系数据库,但关系数据库里的数据本身是哑巴数据(Dumb Data),智能性主要体现在应用层代码和 SQL 里(Dumb Data + Smart Application Code)。而知识图谱(尤其是带推理的知识图谱)把关系显式地表示在数据中,图天然是 schemaless 的 —— 增加节点和边非常容易,不必像关系表那样改字段、改表结构;再在其上叠加 RDFS / OWL 本体,这些词汇本身就携带推理能力。于是范式翻转为 Smart Data(RDF/OWL 本体)+ Uniform Inference Engine(统一推理引擎):数据自己带着语义和规则,推理则蕴含在后续的查询与问答过程之中。
📝 动手练一练
概念辨析(开放世界与查询关键字):判断下列说法是否正确并说明理由。
① “在一张 RDF 图里查不到某公司的 CEO 三元组,就说明这家公司没有 CEO。”
② “SPARQL 中,把邮箱模式写在
OPTIONAL里,与写在UNION分支之外、不加OPTIONAL,对 ‘没有邮箱的学生’ 返回结果相同。”
👉 点击查看参考答案
① 错误。RDF 遵循开放世界假设(OWA):三元组缺失只代表 “当前不知道 / 未录入”,不等于事实不存在。正确表述是 “图中已知没有记录该公司 CEO”,不能据此断言它没有 CEO。
② 错误,二者行为相反。OPTIONAL 对应左连接:即使学生没有邮箱,也会返回该学生、邮箱变量留空(未绑定);而邮箱模式写在 UNION 之外又不加 OPTIONAL 时,邮箱成为必填匹配项,没有邮箱的学生整条记录都不会返回。
- 动手构造与查询:用 Turtle 写出 “深圳位于广东、广东位于中国、腾讯位于深圳” 三条三元组,再用 SPARQL 1.1 属性路径
located_in+查出腾讯沿located_in能逐级到达的所有地点(属性路径在查询时计算闭包,无需声明传递属性、也无需推理机)。
👉 点击查看参考答案
from rdflib import Graph, Namespace
GEO = Namespace("http://ex.org/geo#")
g = Graph()
g.bind("ex", GEO)
ttl = """
@prefix ex: <http://ex.org/geo#> .
ex:Shenzhen ex:located_in ex:Guangdong .
ex:Guangdong ex:located_in ex:China .
ex:Tencent ex:located_in ex:Shenzhen .
"""
g.parse(data=ttl, format="turtle")
# 属性路径 located_in+ = 查询时计算一条或多条 located_in 边的闭包(无需推理机)
q = """
PREFIX ex: <http://ex.org/geo#>
SELECT ?place WHERE { ex:Tencent ex:located_in+ ?place }
"""
print(" → ".join(r.place.split("#")[-1] for r in g.query(q)))预期输出:Shenzhen → Guangdong → China,即无需专门推理机,仅靠 SPARQL 属性路径在查询时计算闭包,就能从 “腾讯在深圳” 一路得到 “腾讯在中国”。
本章小结
本节沿着 W3C 语义网标准栈自下而上,完整搭起了知识表示与查询的框架:
RDF 是数据模型而非文件格式:用 “主体 — 谓词 — 客体” 三元组描述万物,主体可以是 URI 或空白节点(匿名资源、无需 URI),谓词必须是 URI,客体可以是 URI、字面量(带数据类型 / 语言标签)或空白节点;同一张图可用 RDF/XML、Turtle、N-Triples、N-Quads、JSON-LD 等多种语法序列化。
开放世界与分布式:RDF 持开放世界假设(查不到≠不存在),知识可分布式定义、靠相同 URI 自动合并,带标注 RDF 还能附带来源、时间与置信度。
RDFS / OWL 是模式与本体层:RDFS 提供类型系统与子类、定义域 / 值域约束并支持简单推理;OWL / OWL2 补齐等价、属性特性、基数与复杂类,并在表达能力与推理复杂度之间派生出 QL / EL / RL 三个实用子语言。
SPARQL 是图查询语言:三元组模板拼成查询图、在数据图上做子图匹配,
OPTIONAL / FILTER / UNION / CONSTRUCT / ASK / 子查询 / 属性路径覆盖复杂检索与跨库查询;配合推理机或 OWL 2 QL 的查询改写(OBDA),本体层可让声明式查询等价展开、填补语义间隙(SPARQL 引擎本身不自动改写),业务规则可表达关联交易这类隐含关系。JSON-LD / RDFa / Microdata 让语义低成本地嵌进网页;RDF+SPARQL 让关系显式化、数据变更时查询不用改,对应 “智能数据 + 统一推理引擎”,这正是它相对 ER+SQL“哑巴数据 + 聪明代码” 的根本优势。
📋 行动清单
在本地用
uv run --with rdflib python跑通本节 4 段代码,亲手建图、做一次 RDFSCONSTRUCT推理、跑一条两跳 SPARQL。默画出 “RDF → RDFS → OWL/OWL2 → SPARQL” 的分层图,并说清每层各自解决什么问题、QL/EL/RL 分别适合什么场景。
用开放世界假设向身边人解释一个 “查不到 ≠ 不存在” 的例子,再对比说明
OPTIONAL与UNION对缺失值的不同处理。用 located_in 传递属性路径的例子,向自己复述一遍:为什么地理粒度变细时 SPARQL 不用改、而三表 JOIN 的 SQL 必须重写。
—— 小象教研组
领取《小象 11GB VIP 课件资料包与大厂真题手册》
包含全套实战 Jupyter 源码、清洗后数据集、大厂高频面试真题与专属学员答疑交流群。
- ✔完整 Python / 数据分析 Jupyter 实战源码
- ✔大厂真实业务数据集与练习题
- ✔微信扫码添加顾问免费领取;想学什么,直接告诉顾问
微信扫码添加顾问