📑 查看全课大纲(第 10 / 26 节)

面向结构化数据的知识抽取

约 11 分钟

📺 正在播放小象官方高清录播(支持倍速与清晰度调节)

小象实战讲义 · 知识图谱

在上一节,我们学习了如何从非结构化的文本中抽取知识。本节我们将目光转向另一种极其重要的数据源——结构化数据。企业内部的业务系统、历史积累的数据库,是构建行业知识图谱最直接、最可靠的知识来源。我们将学习如何将关系数据库(RDBMS)中规整的“表”数据,高效、准确地转换为知识图谱所需的“图”数据(RDF三元组),掌握从结构化数据中抽取知识的核心标准、工具与思想。

💡 核心导读

  • 为何要抽取结构化数据? 关系数据库是企业数据的核心载体,蕴含丰富的结构化知识,是构建高质量、高可信度知识图谱的基石。
  • 核心标准:Direct Mapping 与 R2RML W3C 定义了两种将关系数据映射为 RDF 的标准方法:一种是基于固定规则的“傻瓜式”直接映射,另一种是支持灵活定制的“单反式”映射语言。
  • 关键工具:Ontop 我们将了解一个强大的虚拟化映射工具 Ontop,它允许我们将关系数据库“伪装”成一个 RDF 图,通过 SPARQL 查询动态转换为 SQL 执行,实现知识图谱的“虚拟化”访问。
  • 从 ETL 到 OBDA 理解从传统的数据抽取、转换、加载(ETL)到基于本体的数据访问(OBDA)这一技术范式的演进。

结构化数据:知识图谱的基石

在知识图谱的构建中,数据来源多种多样,但结构化数据,尤其是关系数据库,扮演着至关重要的角色。企业内部的客户关系管理(CRM)、企业资源计划(ERP)、产品生命周期管理(PLM)等系统,其核心数据都存储在关系数据库中。这些数据已经过严格的业务逻辑梳理和规范化处理,蕴含了丰富的实体、属性及实体间的关系。

将这部分数据转换为知识图谱,可以:

  1. 快速构建高质量核心图谱:避免了从零开始进行复杂实体识别和关系抽取的挑战。
  2. 实现数据价值最大化:将沉睡在“数据孤岛”中的业务数据,转化为可关联、可推理的知识资产。
  3. 支撑上层智能应用:为语义搜索、智能问答、决策分析等应用提供稳定、可靠的知识底座。

下图展示了知识图谱构建中,从关系数据库到知识库的转换路径:

[关系数据库] ---(知识抽取)--> [知识图谱/知识库]
    |                              |
   表 (Table)                     类 (Class)
   列 (Column)                    属性 (Property)
   行 (Row)                       实体/资源 (Resource/Entity)
   单元格值 (Cell Value)          属性值 (Property Value) 或 实体引用
   外键 (Foreign Key)             实体间关系 (Relation)

从关系表到 RDF 图:W3C 标准方法

为了规范这一转换过程,W3C 的 RDB2RDF 工作组于 2012 年将两套映射方法正式定为推荐标准:直接映射 (Direct Mapping)R2RML 映射语言。

直接映射 (Direct Mapping):基于规则的自动化转换

直接映射是一种基于预定义启发式规则的自动化转换方法。它的核心思想是:利用关系数据库模式(Schema)中已隐含的语义,按照一套固定的规则生成 RDF。

映射规则如下:

  • 表 (Table) → 类 (Class):每张表映射为一个 RDF 类。
  • 行 (Row) → 资源 (Resource):表中的每一行(通常基于主键)映射为一个具有唯一 URI 的 RDF 资源。
  • 列 (Column) → 属性 (Property):表中的每一列映射为一个 RDF 属性。
  • 单元格值 (Cell Value) → 字面量 (Literal):普通单元格的值映射为属性的字面量值。
  • 外键 (Foreign Key) → 对象属性 (Object Property):如果存在外键约束,则额外生成一个属性,其客体(object)是另一个资源的 URI,而非字面量。

示例: 假设我们有两张表 PeopleAddresses

关系表数据:

-- People 表
ID (PK) | fname | addr (FK -> Addresses.ID)
------- | ----- | -------------------------
7       | Bob   | 18
8       | Sue   | NULL

-- Addresses 表
ID (PK) | City      | State
------- | --------- | -----
18      | Cambridge | MA

直接映射生成的 RDF 三元组 (Turtle 格式):

@base <http://foo.example/DB/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# People 表的第一行映射为一个资源,并声明其类型
<People/ID=7> rdf:type <People> .
# 映射该资源的各个属性(字面量值)
<People/ID=7> <People#ID> 7 .
<People/ID=7> <People#fname> "Bob" .
<People/ID=7> <People#addr> 18 . # 注意:这里 addr 的值是字面量 18
# 由于 addr 是外键,额外生成一个引用属性,指向 Addresses 表的资源
<People/ID=7> <People#ref-addr> <Addresses/ID=18> .

# People 表的第二行(注意 addr 为 NULL,不生成对应三元组)
<People/ID=8> rdf:type <People> .
<People/ID=8> <People#ID> 8 .
<People/ID=8> <People#fname> "Sue" .

# Addresses 表的资源
<Addresses/ID=18> rdf:type <Addresses> .
<Addresses/ID=18> <Addresses#ID> 18 .
<Addresses/ID=18> <Addresses#city> "Cambridge" .
<Addresses/ID=18> <Addresses#state> "MA" .

直接映射的优缺点:

  • 优点:简单、自动化,类似于“傻瓜相机”,无需手动配置。
  • 缺点:灵活性差。生成的 URI、类和属性名完全依赖于数据库模式,无法映射到已有的、更符合业务语义的本体(Ontology)上。对于复杂的场景(如视图、嵌套查询)处理能力有限。

下面用 Python 最小实现直接映射:sqlite3 在内存中构造 People / Addresses 两张表,然后按上述五条规则逐行生成 RDF 三元组,最后用 rdflib 序列化验证。

# -*- coding: utf-8 -*-
"""代码块1:直接映射(Direct Mapping)最小实现
sqlite3 构造 People/Addresses 关系库 -> rdflib 按固定规则生成三元组 -> 序列化验证。
映射规则:表->类、行->资源、列->属性、单元格->字面量、外键->引用属性。"""
import sqlite3
from rdflib import Graph, Literal, RDF, URIRef

BASE = "http://foo.example/DB/"
g = Graph()

def class_iri(t):
    return URIRef(BASE + t)

def resource_iri(t, pk, v):
    return URIRef(f"{BASE}{t}/{pk}={v}")

def prop_iri(t, c):
    return URIRef(f"{BASE}{t}#{c}")

# ---------- 1. 用 sqlite3 构造示例关系数据库 ----------
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE Addresses (ID INT PRIMARY KEY, City TEXT, State TEXT)")
cur.execute("CREATE TABLE People (ID INT PRIMARY KEY, fname TEXT, addr INT REFERENCES Addresses(ID))")
cur.execute("INSERT INTO Addresses VALUES (18, 'Cambridge', 'MA')")
cur.execute("INSERT INTO People VALUES (7, 'Bob', 18)")
cur.execute("INSERT INTO People VALUES (8, 'Sue', NULL)")

# ---------- 2. 直接映射:People 表逐行生成三元组 ----------
cur.execute("SELECT ID, fname, addr FROM People")
for pid, fname, addr in cur.fetchall():
    subj = resource_iri("People", "ID", pid)
    g.add((subj, RDF.type, class_iri("People")))
    g.add((subj, prop_iri("People", "ID"), Literal(pid)))
    g.add((subj, prop_iri("People", "fname"), Literal(fname)))
    if addr is not None:  # NULL 值不生成三元组
        g.add((subj, prop_iri("People", "addr"), Literal(addr)))
        # 外键:额外生成引用属性,客体是另一张表的资源 IRI
        g.add((subj, prop_iri("People", "ref-addr"),
               resource_iri("Addresses", "ID", addr)))

# ---------- 3. 直接映射:Addresses 表逐行生成三元组 ----------
cur.execute("SELECT ID, City, State FROM Addresses")
for aid, city, state in cur.fetchall():
    subj = resource_iri("Addresses", "ID", aid)
    g.add((subj, RDF.type, class_iri("Addresses")))
    g.add((subj, prop_iri("Addresses", "ID"), Literal(aid)))
    g.add((subj, prop_iri("Addresses", "city"), Literal(city)))
    g.add((subj, prop_iri("Addresses", "state"), Literal(state)))

# ---------- 4. 输出验证 ----------
print(f"共生成 {len(g)} 条三元组")
print(g.serialize(format="turtle"))
conn.close()
共生成 12 条三元组(Bob 行 5 条含外键引用、Sue 行 3 条因 addr 为 NULL 无外键、Addresses 行 4 条)

R2RML:灵活的定制化映射

为了克服直接映射的局限性,W3C 推出了 R2RML (RDB to RDF Mapping Language)。R2RML 是一种声明式的映射语言,允许我们精确定义如何将关系数据库中的逻辑表(可以是物理表、视图或 SQL 查询结果)转换为 RDF 三元组。

R2RML 映射文件本身也是用 RDF (Turtle) 格式编写的。其核心概念是 三元组映射 (Triples Map),它定义了如何生成一组三元组。

一个三元组映射包含:

  1. 逻辑表 (Logical Table):指定数据来源(表名或 SQL 查询)。
  2. 主体映射 (Subject Map):定义如何为每一行数据生成主体(Subject)的 URI。
  3. 谓词-客体映射 (Predicate-Object Map):可以有一个或多个,定义如何生成谓词(Predicate)和客体(Object)。

示例: 目标是将经典的 EMP(员工)和 DEPT(部门)表映射为更符合语义的 RDF。

原始表数据与目标 RDF:

-- EMP 表
EMPNO | ENAME | DEPTNO
------|-------|--------
7369  | SMITH | 10

-- DEPT 表 (经过视图计算 STAFF)
DEPTNO | DNAME     | LOC      | STAFF
-------|-----------|----------|------
10     | APPSERVER | NEW YORK | 1
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ex: <http://example.com/ns#> .
# 目标 RDF 三元组
<http://data.example.com/employee/7369> rdf:type ex:Employee.
<http://data.example.com/employee/7369> ex:name "SMITH".
<http://data.example.com/employee/7369> ex:department <http://data.example.com/department/10>.

<http://data.example.com/department/10> rdf:type ex:Department.
<http://data.example.com/department/10> ex:name "APPSERVER".
<http://data.example.com/department/10> ex:location "NEW YORK".
<http://data.example.com/department/10> ex:staff 1.

对应的 R2RML 映射文件:

@prefix rr: <http://www.w3.org/ns/r2rml#>.
@prefix ex: <http://example.com/ns#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.

# 1. 映射 EMP 表
<#TriplesMap1>
    rr:logicalTable [ rr:tableName "EMP" ]; # 逻辑表为 EMP 表
    rr:subjectMap [
        rr:template "http://data.example.com/employee/{EMPNO}"; # 用模板生成员工 URI
        rr:class ex:Employee; # 声明该资源的类型为 ex:Employee
    ];
    rr:predicateObjectMap [
        rr:predicate ex:name; # 谓词是 ex:name
        rr:objectMap [ rr:column "ENAME" ]; # 客体来自 ENAME 列
    ].
    # 注意:部门关联的映射稍后通过连接条件添加

# 2. 定义一个逻辑视图(R2RML View),计算部门员工数
<#DeptTableView> rr:sqlQuery """
SELECT DEPTNO,
       DNAME,
       LOC,
       (SELECT COUNT(*) FROM EMP WHERE EMP.DEPTNO=DEPT.DEPTNO) AS STAFF
FROM DEPT;
""".

# 3. 映射部门视图
<#TriplesMap2>
    rr:logicalTable <#DeptTableView>; # 逻辑表使用上面定义的视图
    rr:subjectMap [
        rr:template "http://data.example.com/department/{DEPTNO}";
        rr:class ex:Department;
    ];
    rr:predicateObjectMap [ rr:predicate ex:name; rr:objectMap [ rr:column "DNAME" ]; ];
    rr:predicateObjectMap [ rr:predicate ex:location; rr:objectMap [ rr:column "LOC" ]; ];
    rr:predicateObjectMap [ rr:predicate ex:staff; rr:objectMap [ rr:column "STAFF" ]; ].

# 4. 关联员工和部门(处理外键)
<#TriplesMap1>
    rr:predicateObjectMap [
        rr:predicate ex:department; # 谓词是 ex:department
        rr:objectMap [
            rr:parentTriplesMap <#TriplesMap2>; # 客体来自另一个三元组映射(部门)
            rr:joinCondition [ # 连接条件
                rr:child "DEPTNO"; # 当前表(EMP)的 DEPTNO 列
                rr:parent "DEPTNO"; # 父表(DEPT 视图)的 DEPTNO 列
            ];
        ];
    ].

通过 R2RML,我们实现了:

  • 自定义 URI 模式本体类/属性
  • 使用 SQL 视图进行复杂计算(如 STAFF)。
  • 显式定义表间连接,生成实体间的语义关系。

下面用 Python 复刻 R2RML 的核心映射逻辑:sqlite3 构造 EMP / DEPT 表,用自定义 URI 模板和本体类生成语义化 RDF,再用 SPARQL 查询验证——这正是 Ontop 在后端做的事(只不过 Ontop 把 SPARQL 动态重写为 SQL,而非预先物化)。

# -*- coding: utf-8 -*-
"""代码块2:R2RML 风格定制映射 + SPARQL 查询验证
sqlite3 构造 EMP/DEPT -> 自定义 URI 模板 + 本体类 -> rdflib 生成语义化 RDF -> SPARQL 查询。
对应课件 P99-P105:TriplesMap 由 logicalTable / subjectMap / predicateObjectMap 组成。"""
import sqlite3
from rdflib import Graph, Literal, RDF, URIRef
from rdflib.namespace import Namespace

EX = Namespace("http://example.com/ns#")
g = Graph()
g.bind("ex", EX)

# ---------- 1. 构造 EMP / DEPT 关系表 ----------
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE EMP (EMPNO INT PRIMARY KEY, ENAME TEXT, DEPTNO INT)")
cur.execute("CREATE TABLE DEPT (DEPTNO INT PRIMARY KEY, DNAME TEXT, LOC TEXT)")
cur.execute("INSERT INTO DEPT VALUES (10, 'APPSERVER', 'NEW YORK')")
cur.execute("INSERT INTO EMP VALUES (7369, 'SMITH', 10)")

# ---------- 2. TriplesMap1:EMP 表 -> ex:Employee,自定义 URI 模板 ----------
cur.execute("SELECT EMPNO, ENAME, DEPTNO FROM EMP")
for empno, ename, deptno in cur.fetchall():
    emp = URIRef(f"http://data.example.com/employee/{empno}")
    g.add((emp, RDF.type, EX.Employee))
    g.add((emp, EX.name, Literal(ename)))
    # joinCondition:EMP.DEPTNO = DEPT.DEPTNO,生成实体间语义关系
    dept = URIRef(f"http://data.example.com/department/{deptno}")
    g.add((emp, EX.department, dept))

# ---------- 3. TriplesMap2:DEPT 表 -> ex:Department ----------
cur.execute("SELECT DEPTNO, DNAME, LOC FROM DEPT")
for deptno, dname, loc in cur.fetchall():
    dept = URIRef(f"http://data.example.com/department/{deptno}")
    g.add((dept, RDF.type, EX.Department))
    g.add((dept, EX.name, Literal(dname)))
    g.add((dept, EX.location, Literal(loc)))

# ---------- 4. SPARQL 查询:模拟 Ontop 接收 SPARQL 后返回关联结果 ----------
q = """
PREFIX ex: <http://example.com/ns#>
SELECT ?ename ?dname ?loc
WHERE {
  ?emp a ex:Employee ; ex:name ?ename ; ex:department ?dept .
  ?dept a ex:Department ; ex:name ?dname ; ex:location ?loc .
}
"""
for row in g.query(q):
    print(f"员工={row.ename}  部门={row.dname}  地点={row.loc}")

conn.close()
员工=SMITH  部门=APPSERVER  地点=NEW YORK

现代工具与实践:Ontop 与虚拟化知识图谱

有了映射标准,我们需要工具来执行它们。除了早期的一些工具如 D2R、Virtuoso 等,一个非常强大且活跃的现代工具是 Ontop

Ontop 是一个基于 OBDA (Ontology-Based Data Access) 理念的平台。它的核心思想不是将数据物理地转换为 RDF 存储起来,而是虚拟化

这里涉及两种知识图谱构建范式:物化(materialization)即通过 ETL 把关系数据预先转成 RDF 三元组、物理落盘到图数据库;虚拟化(virtualization)则只维护映射规则、底层数据仍留在关系库中,查询时才动态生成 RDF 视图。Ontop 走的是虚拟化路线。

工作原理:

  1. 定义映射:使用 R2RML 或 Ontop 自带的映射语言,描述关系数据库模式到目标本体(OWL 2 QL)的映射。
  2. 虚拟 RDF 图:对上层应用(如 SPARQL 查询端点)呈现出一个虚拟的、统一的 RDF 图。
  3. 查询转换:当用户提交一个 SPARQL 查询时,Ontop 会利用映射规则和本体公理(schema 层面的 TBox),将 SPARQL 查询重写优化为一组(或多个)SQL 查询。
  4. 执行与返回:在底层关系数据库上执行这些 SQL 查询,并将结果组装成 SPARQL 结果格式返回。

Ontop 的核心优势:

  • 性能:直接利用关系数据库成熟的查询优化器和索引,处理大规模数据时性能优异。
  • 数据实时性:查询的是真实的业务数据库,结果永远是最新的。
  • 降低复杂度:无需维护两套存储(关系库和 RDF 库),避免了 ETL 的延迟和一致性难题。
  • 标准支持:支持 SPARQL 1.0(课程年代部分支持 1.1,现代版本已支持完整 SPARQL 1.1)、OWL 2 QL 本体推理、R2RML 标准。
  • 集成友好:提供 Java 库/连接器,可接入 Sesame(现 Eclipse RDF4J)与 OWL API 生态,并与 Protégé 本体编辑器集成。

一个简单的 Ontop 使用示意(概念层面):

用户/应用层:     SPARQL Query: `SELECT ?name WHERE { ?p a ex:Employee; ex:name ?name }`

                     ↓ (查询重写与优化)
Ontop 引擎层:     将 SPARQL 转换为 SQL: `SELECT ENAME as name FROM EMP`
                     ↑ (基于 R2RML 映射和 OWL 本体)

数据存储层:     关系数据库 (MySQL/PostgreSQL/Oracle...) 执行 SQL 并返回结果集

通过 Ontop 这类 OBDA 系统,知识图谱的构建模式从传统的 “抽取-转换-加载 (ETL)” 演进为 “基于本体的数据访问 (OBDA)”。这对于需要与现有业务系统深度集成、要求数据实时性的行业知识图谱项目具有重大意义。

📝 动手练一练

  1. 概念辨析:直接映射 (Direct Mapping) 和 R2RML 映射的主要区别是什么?分别适用于什么场景?
  2. SPARQL 与 SQL:假设你有一个 Students(ID, Name, Major) 表,通过 R2RML 映射后,主体 URI 模板为 http://edu.example/student/{ID},类型为 ex:Studentex:nameex:major 属性分别映射到 NameMajor 列。请写出一个 SPARQL 查询,用于查找所有专业为“计算机科学”的学生姓名,并推测 Ontop 引擎可能会将其重写为什么样的 SQL 查询?
👉 点击查看参考答案
  1. 概念辨析

    • 直接映射:基于固定规则,自动将数据库模式元素(表、列、行)转换为 RDF(类、属性、资源)。优点是简单、自动化,适合快速原型或模式简单的场景。缺点是灵活性差,无法定制 URI、无法映射到已有本体、无法处理复杂查询逻辑。
    • R2RML:一种声明式的映射语言,允许用户精确定义从关系数据到 RDF 的转换规则。可以自定义 URI 模板、使用已有本体词汇、定义基于 SQL 视图的复杂逻辑表、显式处理连接。优点是高度灵活、语义丰富,适合生产环境构建与现有本体对齐的高质量知识图谱。缺点是需要手动编写映射文件,有一定学习成本。
  2. SPARQL 与 SQLSPARQL 查询:

    PREFIX ex: <http://example.com/ns#>
    SELECT ?name
    WHERE {
      ?student a ex:Student .
      ?student ex:name ?name .
      ?student ex:major "计算机科学" .
    }

    可能重写成的 SQL 查询(示意):

    SELECT Name AS name
    FROM Students
    WHERE Major = '计算机科学'

    Ontop 会根据映射规则,将 ?student a ex:Student 模式匹配到 Students 表,并将属性条件 ex:major "计算机科学" 转换为 SQL 的 WHERE 子句条件。

本章小结

本节我们深入探讨了从结构化数据(主要是关系数据库)中抽取知识构建知识图谱的核心技术与范式。

我们首先明确了结构化数据作为知识图谱高质量数据源的重要性。接着,系统学习了 W3C 的两种标准映射方法:直接映射的自动化规则与R2RML的灵活定制能力,并通过具体示例理解了其映射逻辑和生成的 RDF 形态。最后,我们介绍了现代知识图谱工程中的先进范式——基于本体的数据访问 (OBDA),以及其代表性工具 Ontop 如何通过查询重写实现关系数据的虚拟化 RDF 访问,从而在保证数据实时性和利用现有数据库投资的前提下,提供强大的知识图谱查询与推理能力。

📋 行动清单

学完本节,你可以立即着手以下实践:

  • 尝试一个在线 R2RML 映射器:搜索并试用一些在线的 R2RML 演示工具(如一些开源项目提供的 Web 界面),上传一个简单的 CSV 文件或连接到一个测试数据库,体验编写映射规则并生成 RDF 的过程。
  • 用 Python (rdflib) 模拟直接映射:编写一个简单的 Python 脚本,读取一个类似 PeopleAddresses 的 CSV 文件,按照直接映射的规则,使用 rdflib 库在内存中生成对应的 RDF 图,并打印出来。
  • 研究 Ontop 官方示例:访问 Ontop 项目的 GitHub 仓库,查看其 tutorialexamples 目录,了解一个完整的映射文件、本体文件和查询示例是如何组织的。

—— 小象教研组

配套学习资源与课件
  • 第3章课件:知识抽取与挖掘 I
    下载
  • 第3章练习(Assignment:Java + .ttl 数据)
    下载
  • 知识图谱课程思维导图(KG_Centralized.xmind 全课程结构图)
    下载
🎁 免费学习资源

领取《小象 11GB VIP 课件资料包与大厂真题手册》

包含全套实战 Jupyter 源码、清洗后数据集、大厂高频面试真题与专属学员答疑交流群。

  • 完整 Python / 数据分析 Jupyter 实战源码
  • 大厂真实业务数据集与练习题
  • 微信扫码添加顾问免费领取;想学什么,直接告诉顾问
微信二维码:扫码添加课程顾问微信扫码添加顾问