☰
知识图谱里的数据错了怎么办?从 pySHACL 讲透 RDF 数据校验与 SHACL 规则
2026/9/27 10:51:43 网站建设 项目流程

很多人第一次接触知识图谱时,会把注意力全部放在:

  • RDF
  • 三元组
  • Ontology
  • OWL
  • SPARQL
  • GraphRAG

但真正把知识图谱放到生产环境里之后,你很快会遇到另外一个非常现实的问题:

知识图谱里的数据写错了怎么办?

例如我们有一个人员知识图谱:

张三 -> 姓名 -> "张三" 张三 -> 年龄 -> 28 张三 -> 邮箱 -> "zhangsan@example.com" 张三 -> 所属公司 -> 阿里巴巴

看起来非常正常。

但如果某一天数据变成这样:

张三 -> 年龄 -> "二十八" 张三 -> 邮箱 -> "123456" 张三 -> 所属公司 -> "Alibaba"

从 RDF 的角度来说,这些数据完全有可能依然是合法 RDF。

问题在于:

RDF 语法合法,不代表你的业务数据是正确的。

这时候,就轮到SHACL出场了。

而 Python 生态里,一个非常常用的 SHACL 校验工具就是:

pySHACL

项目地址:

https://github.com/RDFLib/pySHACL

pySHACL 是一个基于 Python 的 SHACL Validator,可以使用 SHACL Shapes 对 RDF Graph 进行数据验证。项目底层使用 RDFLib 处理 RDF 数据,并按照 SHACL 规范执行约束验证。

今天我们不讲特别复杂的知识图谱理论。

就讲清楚一个问题:

如果知识图谱里的数据错了,怎么通过 pySHACL 自动找出来?

我们重点看 5 类非常实用的规则:

minCount datatype class pattern cardinality

学完这几个,已经足够解决相当一部分实际的数据质量问题。


一、先搞清楚:SHACL 到底解决什么问题?

先来看传统关系型数据库。

假设我们有这样一张 MySQL 表:

CREATETABLEuser(idBIGINTPRIMARYKEY,nameVARCHAR(100)NOTNULL,ageINT,emailVARCHAR(255),company_idBIGINT);

数据库天然可以帮我们限制一些东西。

比如:

name 不能为空 age 必须是 INT id 必须唯一

甚至可以进一步添加:

ALTERTABLEuserADDCONSTRAINTchk_ageCHECK(age>=0);

也就是说:

关系型数据库不仅存数据,还可以通过 Schema 和 Constraint 对数据进行约束。

但是 RDF 世界不太一样。

例如:

@prefix ex: <http://example.org/> . ex:zhangsan ex:name "张三" ; ex:age "二十八岁" ; ex:email "哈哈哈" .

它是不是 RDF?

是。

语法上并没有什么大问题。

但从我们的业务角度:

age = "二十八岁"

可能就是错误数据。

因为我们希望年龄必须是:

xsd:integer

邮箱也不能随便写。

于是我们需要另外定义一套:

这个 RDF Graph 应该长什么样。

这就是 SHACL 的核心思想。

SHACL 全称:

Shapes Constraint Language

你可以把它简单理解成:

给 RDF / 知识图谱定义数据校验规则。

W3C 的 SHACL Recommendation 定义了包括sh:minCount、sh:maxCount、sh:datatype、sh:class、sh:pattern等约束组件,用来描述 RDF 节点和属性应该满足什么条件。


二、理解 SHACL 最简单的方法:把它当成“知识图谱版 JSON Schema”

很多程序员第一次看到 SHACL:

ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; sh:property [ sh:path ex:name ; sh:minCount 1 ; ] .

第一反应可能是:

这什么东西?

实际上思想特别简单。

如果你用过 JSON Schema:

{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}

SHACL 做的是类似的事情。

只不过:

JSON Schema

验证:

JSON

而:

SHACL

验证:

RDF Graph

你甚至可以粗略这样理解:

RDF ≈ 数据 SHACL ≈ 数据规则 pySHACL ≈ 校验器

整个流程就是:

RDF Data ↓ SHACL Shapes ↓ pySHACL ↓ Validation Report

三、准备一个故意写错的知识图谱

我们直接做一个人员知识图谱。

新建:

data.ttl

内容:

@prefix ex: <http://example.org/> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . ex:company001 a ex:Company ; ex:name "JavaPub Technology" . ex:zhangsan a ex:Person ; ex:name "张三" ; ex:age 28 ; ex:email "zhangsan@example.com" ; ex:worksFor ex:company001 .

这里的数据目前没有什么问题。

我们可以把它理解成:

张三 ├── 类型:Person ├── 姓名:张三 ├── 年龄:28 ├── 邮箱:zhangsan@example.com └── 公司:company001

接下来我们专门制定一套规则来检查它。


四、第一个规则:minCount,检查“必填字段”

这是 SHACL 里最好理解,同时也是最常用的规则之一。

假设我们的业务要求:

每一个 Person 都必须有 name。

那么可以写:

@prefix ex: <http://example.org/> . @prefix sh: <http://www.w3.org/ns/shacl#> . ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; sh:property [ sh:path ex:name ; sh:minCount 1 ; ] .

重点就是:

sh:minCount 1

意思是:

ex:name至少出现一次。

W3C SHACL 规范中,sh:minCount表示某个属性对应的 value nodes 最少应该有多少个;当实际数量小于这个值时,就会产生 Validation Result。

把它翻译成程序员熟悉的写法,大概就是:

iflen(person.name)<1:raiseValidationError("name不能为空")

或者 SQL:

nameVARCHAR(100)NOTNULL

五、故意制造一条错误数据

现在把:

ex:zhangsan a ex:Person ; ex:name "张三" .

改成:

ex:zhangsan a ex:Person ; ex:age 28 .

也就是说:

name 没了

按照我们的 SHACL Shape:

sh:minCount 1

这条数据应该无法通过。


六、安装 pySHACL

Python 环境直接安装:

pipinstallpyshacl

官方项目也提供 CLI,可以直接给它数据图和 Shapes Graph 进行校验。

例如:

pyshacl-sshapes.ttl data.ttl

其中:

data.ttl

是:

真正的数据

而:

shapes.ttl

是:

数据规则

可以进一步输出人类更容易阅读的报告:

pyshacl\-sshapes.ttl\-fhuman\data.ttl

当 Graph 不满足 Shapes 时,pySHACL 会返回 Non-Conformant 的校验报告。


七、用 Python 调用 pySHACL

很多时候我们不会单独跑命令,而是集成到自己的 Python 服务里。

例如:

frompyshaclimportvalidate conforms,results_graph,results_text=validate(data_graph="data.ttl",shacl_graph="shapes.ttl")print("校验是否通过:",conforms)print(results_text)

pySHACL 的validate()会返回三个主要结果:

conforms results_graph results_text

分别可以理解为:

conforms ↓ 是否通过 results_graph ↓ RDF 格式的验证结果 results_text ↓ 人类可读的验证报告

这也是 pySHACL 官方 Python API 的基本使用方式。

因此我们可以这样写:

frompyshaclimportvalidate conforms,result_graph,result_text=validate(data_graph="data.ttl",shacl_graph="shapes.ttl",inference="none",abort_on_first=False)ifconforms:print("数据正常")else:print("发现错误")print(result_text)

这已经可以变成真正的数据质量检查程序了。


八、datatype:年龄不能写成“二十八岁”

下面看第二个非常有价值的约束:

datatype

假设我们规定:

Person.age

必须是:

xsd:integer

SHACL 可以这样写:

@prefix ex: <http://example.org/> . @prefix sh: <http://www.w3.org/ns/shacl#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; ] .

正确数据:

ex:zhangsan ex:age 28 .

或者显式写:

ex:zhangsan ex:age "28"^^xsd:integer .

但下面这种:

ex:zhangsan ex:age "二十八岁" .

就不符合我们的约束。

因为:

"二十八岁"

本质是字符串。

而我们要求:

xsd:integer

这就是:

sh:datatype xsd:integer

真正有价值的地方。


九、datatype 非常适合抓 ETL 数据错误

想象你从 Excel 导入知识图谱。

原始表格:

姓名 年龄 张三 28 李四 35 王五 未知 赵六 31岁

程序批量转换成 RDF 后可能得到:

ex:zhangsan ex:age 28 . ex:lisi ex:age 35 . ex:wangwu ex:age "未知" . ex:zhaoliu ex:age "31岁" .

如果有几十万条数据,你显然不可能人工一个个检查。

这时候:

sh:datatype xsd:integer

实际上就相当于一台:

自动知识图谱质检机。

所有不是整数的数据都可以在入库阶段被拦下来。


十、pattern:邮箱格式对不对?

但是仅仅判断:

email 是 string

显然还不够。

因为下面这些都是字符串:

zhangsan@example.com 123456 abc 哈哈哈

所以还需要:

pattern

例如:

sh:property [ sh:path ex:email ; sh:datatype xsd:string ; sh:pattern "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" ; ] .

这里:

sh:pattern

就是正则表达式约束。

SHACL 对sh:pattern的定义是使用兼容 SPARQLREGEX的模式,对 value node 的字符串表示执行匹配。

例如:

zhangsan@example.com

可以通过。

但是:

123456

无法通过。


十一、pattern 不只是检查邮箱

很多人看到 pattern 第一反应就是正则。

但在知识图谱场景里,它非常有用。

例如检查手机号:

sh:pattern "^1[3-9][0-9]{9}$" ;

检查员工号:

EMP-2026-00001

可以写:

sh:pattern "^EMP-[0-9]{4}-[0-9]{5}$" ;

检查商品编号:

SKU-A001 SKU-B102

可以写:

sh:pattern "^SKU-[A-Z][0-9]{3}$" ;

甚至可以约束 URL:

sh:pattern "^https://.*" ;

所以:

pattern

特别适合解决:

数据类型没问题,但是内容格式错了。


十二、class:属性指向的对象类型对不对?

下面这个非常重要。

假设:

Person

有一个:

worksFor

属性。

我们的要求是:

worksFor 必须指向一个 Company。

比如:

ex:zhangsan ex:worksFor ex:company001 . ex:company001 a ex:Company .

这是合理的。

SHACL:

sh:property [ sh:path ex:worksFor ; sh:class ex:Company ; ] .

什么意思?

就是:

zhangsan | | worksFor ↓ company001 | | rdf:type ↓ Company

十三、如果 worksFor 指向一个 Person 呢?

假设有人把数据写成:

ex:zhangsan ex:worksFor ex:lisi . ex:lisi a ex:Person .

语法有没有问题?

没有。

它依然是一组三元组:

张三 -> 就职于 -> 李四

RDF 不会因为业务语义奇怪就自动拒绝。

但是:

sh:class ex:Company

就可以把它检查出来。

这也是 SHACL 和普通 JSON Schema 一个非常大的不同。

因为知识图谱里的数据关系往往是:

Node → Node

所以不仅要检查:

属性值是什么类型

还要检查:

属性指向的节点是什么 Class。

十四、class 在实际知识图谱里特别重要

比如医疗知识图谱:

Patient ↓ takesDrug Drug

你可以约束:

sh:class ex:Drug

公司知识图谱:

Employee ↓ worksFor Company

可以约束:

sh:class ex:Company

商品知识图谱:

Product ↓ belongsToCategory Category

可以要求:

sh:class ex:Category

科研知识图谱:

Paper ↓ author Researcher

可以要求:

sh:class ex:Researcher

这已经不仅仅是字段校验了。

而是在:

验证知识图谱中的关系语义是否正确。


十五、Cardinality:一个人到底能有几个出生日期?

接下来讲一个知识图谱里非常重要的概念:

Cardinality

中文一般叫:

基数

简单理解就是:

一个属性允许出现多少次。

SHACL 里非常常见的就是:

sh:minCount sh:maxCount

例如:

sh:minCount 1 ; sh:maxCount 1 ;

意思就是:

必须有,而且只能有一个。

SHACL 规范把sh:minCount和sh:maxCount都归到 Cardinality Constraint Components。minCount规定最少值数量,maxCount规定最多值数量。


十六、一个很典型的错误:两个出生日期

例如:

ex:zhangsan ex:birthDate "1995-01-01"^^xsd:date ; ex:birthDate "1996-01-01"^^xsd:date .

语法完全合法。

但是一般业务规则可能要求:

出生日期只能有一个

那么:

sh:property [ sh:path ex:birthDate ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:date ; ] .

这样:

0 个 birthDate

不行。

1 个 birthDate

通过。

2 个 birthDate

也不行。

是不是特别像数据库?

实际上就类似:

NOT NULL + 单值约束

十七、minCount 和 maxCount 可以组合出很多规则

可选字段

sh:maxCount 1

表示:

0 或 1 个

例如:

middleName

有的人有,有的人没有。


必填字段

sh:minCount 1

表示:

至少一个

必须且唯一

sh:minCount 1 ; sh:maxCount 1 ;

表示:

有且只有一个

例如:

身份证号 出生日期 员工编号

至少两个

比如团队要求至少两位负责人:

sh:minCount 2

最多三个标签

sh:maxCount 3

所以 SHACL 其实可以表达非常多业务规则。


十八、把规则组合起来

真正生产环境里不会只写一个约束。

我们可以完整定义一个:

PersonShape

例如:

@prefix ex: <http://example.org/> . @prefix sh: <http://www.w3.org/ns/shacl#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; sh:property [ sh:path ex:name ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ; ] ; sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ; ] ; sh:property [ sh:path ex:email ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ; sh:pattern "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" ; ] ; sh:property [ sh:path ex:worksFor ; sh:minCount 1 ; sh:maxCount 1 ; sh:class ex:Company ; ] .

翻译成人话:

Person: name: 必须存在 只能有一个 必须是字符串 age: 最多一个 必须是整数 email: 必须存在 只能有一个 必须是字符串 而且必须符合邮箱格式 worksFor: 必须存在 只能有一个 而且必须指向 Company

到这里,知识图谱已经第一次拥有了真正意义上的:

数据契约。


十九、现在故意造一份“全是 Bug”的 RDF

例如:

@prefix ex: <http://example.org/> . ex:zhangsan a ex:Person ; ex:name "张三" ; ex:name "老张" ; ex:age "28岁" ; ex:email "123456" ; ex:worksFor ex:lisi . ex:lisi a ex:Person .

这里至少有四个问题。

第一:

name 出现了两个

违反:

sh:maxCount 1

第二:

age = "28岁"

违反:

sh:datatype xsd:integer

第三:

email = "123456"

违反:

sh:pattern

第四:

worksFor → lisi

但:

lisi = Person

而我们要求:

worksFor → Company

违反:

sh:class ex:Company

这就是 SHACL 真正有意思的地方:

它不是检查 RDF 能不能解析,而是在检查这些 RDF 数据是否符合我们定义的业务语义。


二十、Python 里自动处理校验失败

我们可以进一步写一个实际一点的程序:

frompyshaclimportvalidatedefvalidate_graph(data_file,shapes_file):conforms,results_graph,results_text=validate(data_graph=data_file,shacl_graph=shapes_file,inference="none",abort_on_first=False,meta_shacl=False,advanced=False,debug=False)ifconforms:print("✅ RDF 数据校验通过")returnTrueprint("❌ RDF 数据存在问题")print("-"*80)print(results_text)returnFalseif__name__=="__main__":validate_graph("data.ttl","shapes.ttl")

这里特别注意:

abort_on_first=False

意味着:

不要发现第一个错误就停。

比如一份数据有:

27 个问题

我们希望一次全部找出来,而不是:

改一个 运行一次 再改一个 再运行一次

二十一、还可以直接使用 RDFLib Graph

如果你的知识图谱本来就是程序动态生成的,就没有必要先保存文件。

可以直接使用:

fromrdflibimportGraphfrompyshaclimportvalidate data_graph=Graph()data_graph.parse(data=""" @prefix ex: <http://example.org/> . ex:zhangsan a ex:Person ; ex:name "张三" ; ex:age "二十八" . """,format="turtle")shape_graph=Graph()shape_graph.parse(data=""" @prefix ex: <http://example.org/> . @prefix sh: <http://www.w3.org/ns/shacl#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; sh:property [ sh:path ex:name ; sh:minCount 1 ; ] ; sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; ] . """,format="turtle")conforms,results_graph,results_text=validate(data_graph,shacl_graph=shape_graph)print(conforms)print(results_text)

这样就能很容易接进:

Flask FastAPI Django ETL Pipeline Airflow 数据同步服务 知识图谱导入程序

里面。


二十二、SHACL 最适合放在哪一层?

我比较推荐一个思路:

Excel CSV MySQL API 爬虫 LLM ↓ 数据转换 ↓ RDF ↓ SHACL Validation ↓ 合法 ↓ Graph Database

也就是说:

SHACL 放在知识图谱入库之前。

例如:

rdf=transform_to_rdf(raw_data)result=validate_rdf(rdf)ifresult:save_to_graph_database(rdf)else:save_to_error_queue(rdf)

这样 Graph Database 里面尽量只留下干净数据。


二十三、尤其是现在 LLM 生成知识图谱,更需要 SHACL

这几年非常流行一种做法:

文章 PDF 网页 合同 论文 ↓ LLM ↓ 实体抽取 关系抽取 ↓ RDF ↓ 知识图谱

问题来了。

大模型很强。

但大模型不是数据库 Constraint。

例如你告诉模型:

请从文章中提取人物姓名、年龄、邮箱和所属公司。

它最终可能生成:

{"name":"张三","age":"大约30岁","email":"暂无","company":"可能是某科技公司"}

人看起来似乎还能理解。

但是知识图谱真正入库时:

age = "大约30岁"

到底是不是整数?

company

到底有没有对应 Company 节点?

email = "暂无"

到底算邮箱还是缺失?

所以一个越来越实用的架构就是:

LLM ↓ 生成 RDF ↓ SHACL ↓ 发现错误 ↓ 修复 / 重试 ↓ Graph Database

换句话说:

LLM 负责生成,SHACL 负责守门。


二十四、甚至可以让 LLM 根据 SHACL 自动修数据

再进一步。

假设 pySHACL 返回:

Person: zhangsan Property: age Constraint: datatype xsd:integer Current Value: "28岁"

我们完全可以把这个结果重新交给模型:

下面是一条 RDF 数据: ex:zhangsan ex:age "28岁" . SHACL 校验要求: ex:age 的 datatype 必须是 xsd:integer。 请修复 RDF。

模型返回:

ex:zhangsan ex:age 28 .

然后:

再次 pySHACL 校验

如果通过:

写入数据库

于是一个自动循环就出现了:

LLM Generate ↓ pySHACL Validate ↓ 失败 ↓ LLM Repair ↓ pySHACL Validate ↓ 成功 ↓ Graph DB

这个设计其实非常适合现在的:

GraphRAG 企业知识库 AI Agent Memory Ontology Pipeline

二十五、SHACL 和 OWL 到底有什么区别?

这里也是新手非常容易混淆的地方。

有人会问:

有 OWL 了为什么还要 SHACL?

可以先用一个非常粗略、但容易理解的方法区分:

OWL 更关心: 这个世界的知识意味着什么? SHACL 更关心: 这份数据合不合格?

例如:

Employee 是 Person 的子类

这比较像:

OWL / Ontology

而:

Employee 必须有 employeeId employeeId 只能有一个 employeeId 必须符合 EMP-XXXXX 格式

这明显更像:

SHACL

所以实际系统中,两者完全可以一起出现:

Ontology / OWL ↓ 定义知识语义 SHACL ↓ 定义数据质量约束

而 pySHACL 本身也支持配合 RDFS / OWL-RL 推理进行验证,例如其validate()API 可以通过inference参数配置none、rdfs、owlrl或both。


二十六、最值得先掌握的 5 个 SHACL 规则

如果你刚学 SHACL,我并不建议一开始把所有 Constraint 都背下来。

先记住下面几个就够了。

sh:minCount

解决:

这个字段是不是必须存在?

sh:maxCount

解决:

最多允许几个值?

sh:datatype

解决:

这个 Literal 是不是正确的数据类型?

sh:class

解决:

这个关系指向的是不是正确类型的节点?

sh:pattern

解决:

字符串内容是不是符合指定格式?

如果你能熟练使用这五个,已经能覆盖很多常见的数据质量问题。


二十七、最后看一张完整关系图

整个知识图谱校验流程其实可以压缩成:

PersonShape │ ┌────────────┼────────────┐ │ │ │ name age email │ │ │ minCount=1 datatype pattern maxCount=1 integer EMAIL │ │ │ └────────────┼────────────┘ │ Person │ worksFor │ ▼ Company │ class check

而 pySHACL 做的事情就是:

RDF Graph │ ▼ ┌────────────┐ │ pySHACL │ └────────────┘ ▲ │ SHACL Shapes │ ▼ Validation Report

写在最后

很多人学习知识图谱时,只关注:

怎么把数据存进去?

但真正进入生产环境后,更重要的问题往往变成:

怎么保证存进去的数据是对的?

因为错误的知识图谱,比没有知识图谱更加危险。

如果:

年龄错了

可能只是一个字段错误。

但如果:

人物关系错了 公司关系错了 药物关系错了 供应链关系错了

这些错误最终进入:

GraphRAG 搜索系统 推荐系统 AI Agent 智能问答

那么模型得到的上下文本身就是错误的。

再强的大模型:

Garbage In ↓ Garbage Out

所以在一个相对完整的知识图谱工程里,我更喜欢这样的流程:

原始数据 ↓ RDF ↓ Ontology ↓ SHACL ↓ pySHACL ↓ 验证报告 ↓ 清洗 / 修复 ↓ Graph Database ↓ GraphRAG / Agent

如果把 Ontology 看成:

知识图谱的语义说明书。

那么 SHACL 更像:

知识图谱的质检标准。

而 pySHACL:

就是那个真正拿着质检标准,一条条帮你检查 RDF 数据的人。

对于准备做知识图谱、GraphRAG、企业知识库的人来说,这一层非常容易被忽略。

但它可能恰恰决定了:

你的知识图谱到底只是“有数据”,还是“数据真的可信”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询