☰
Python文本分析构建知识图谱:从非结构化文本到可推理图数据库
2026/10/11 1:23:43 网站建设 项目流程

简介:这份源码资源面向数据挖掘、信息检索与语义网络分析方向的学习者和开发者,提供了一套基于Python文本分析技术、从非结构化文本中自动抽取实体与关系并构建知识图谱的完整实现。项目共26个文件,以9个Python源码文件为核心,涵盖配置、主流程控制与图像转文本等辅助模块,另含12个txt数据与说明文档、2个prompt提示文件,以及license、gitignore和png图片各1个,压缩包约2.01MB,目录结构清晰,便于按模块阅读与二次开发。目前已有367人学习下载。读者可借此理解文本分析到图谱构建的自动化链路,参考实体抽取、关系组织与配置管理的代码组织方式,并基于现有脚本快速搭建实验环境、迁移到科研或情报分析等场景,具备较强的实践参考价值。

1. 从一堆文本到一张图:Python 文本分析构建知识图谱到底在做什么

你手头有一批行业文档、客服对话或者论文摘要,老板说“把这些东西做成知识图谱”,你打开 Python 发现不知道从哪一行开始写。这个标题要解决的就是这件事:用 Python 做文本分析,把非结构化的文字自动变成“实体—关系—实体”的三元组,再组装成一张可查询、可推理的图。它适合有 Python 基础、懂一点 NLP 但没完整搭过图谱的工程师,也适合想用工业知识图谱思路处理设备日志、工单、标准文档的团队。核心链路只有四步:文本预处理、实体与关系抽取、三元组规范化、图数据库写入。每一步都有现成库,但每一步也都有让新手翻车的地方。下面按我实际落地的顺序拆开讲,代码可以直接抄,参数可以按你的语料改。

2. 文本预处理与实体识别:把脏文本变成能抽的句子

2.1 为什么预处理决定了后面抽出来的东西能不能用

很多人一上来就调大模型抽三元组,结果抽出来一堆“的”“了”“我们”当实体。文本分析的第一步不是分析,是清洗。中文文本常见的脏数据包括:HTML 标签、全角半角混用、连续换行、页眉页脚、OCR 错字。我一般先用正则做一轮硬清洗,再用分词工具做句子边界切分。这里有个选型理由:知识图谱构建需要的是完整句子或短句,不是段落。段落里跨句的指代关系会让关系抽取模型直接懵掉。所以预处理的目标是产出“一句一行的干净文本”,每行长度控制在 20 到 200 字之间。太短没有上下文,太长模型截断后丢信息。工业知识图谱场景里,设备手册的句子往往很长,我会按分号、句号、换行符做二次切分,保证每个句子只表达一个完整事实。

import re def clean_text(raw: str) -> list[str]: # 去掉 HTML 标签和多余空白 text = re.sub(r'<[^>]+>', '', raw) text = re.sub(r'&[a-z]+;', ' ', text) # 全角转半角(保留中文标点) text = text.replace(' ', ' ').replace('\u3000', ' ') # 按中文句号、问号、感叹号、分号切句 sentences = re.split(r'(?<=[。!?;])', text) cleaned = [] for s in sentences: s = s.strip() # 过滤过短和纯符号的句子 if len(s) < 8 or re.fullmatch(r'[\W_]+', s): continue cleaned.append(s) return cleaned raw_doc = "设备A的额定电压是220V。<p>当温度超过80℃时,应启动冷却系统。</p>" for sent in clean_text(raw_doc): print(sent)

这段代码的逻辑说明:re.sub(r'<[^>]+>', '', raw)去掉 HTML 标签,re.split用零宽断言在标点后切分且不丢失标点。参数上,len(s) < 8这个阈值可以根据语料调整,工单短文本可以降到 5,论文摘要可以升到 15。注意,不要用jieba做句子切分,它擅长分词不擅长断句,断句用正则更稳。

2.2 用 spaCy 和 HanLP 做实体识别的取舍

实体识别是知识图谱构建的入口。Python 生态里常见做法有两类:一类是通用 NER 模型,比如 spaCy 的zh_core_web_trf或 HanLP 的预训练模型;另一类是自定义词典加规则。我一般会先跑通用模型看召回,再补规则。为什么?通用模型对“人名、地名、机构名”准,但对“设备型号、故障代码、工艺参数”几乎瞎。工业知识图谱里,后者才是核心实体。所以我的流程是:通用模型抽通用实体,正则和词典抽领域实体,两者合并去重。这里有个参数要注意:spaCy 的nlp.pipe批处理大小默认是 256,如果你的文本句子很短,可以调到 512 提高吞吐;如果句子很长,调到 64 避免显存爆掉。

import spacy from spacy.matcher import PhraseMatcher nlp = spacy.load("zh_core_web_trf") matcher = PhraseMatcher(nlp.vocab, attr="LOWER") # 领域词典:设备型号和故障代码 domain_terms = ["XJ-2000", "ERR-502", "冷却系统", "额定电压"] patterns = [nlp.make_doc(t) for t in domain_terms] matcher.add("DOMAIN", patterns) def extract_entities(sentences: list[str]): results = [] for doc in nlp.pipe(sentences, batch_size=256): ents = [(ent.text, ent.label_) for ent in doc.ents] matches = matcher(doc) for match_id, start, end in matches: span = doc[start:end] ents.append((span.text, "DOMAIN")) # 去重 seen = set() unique = [] for e in ents: if e[0] not in seen: seen.add(e[0]) unique.append(e) results.append((doc.text, unique)) return results

逻辑说明:PhraseMatcher用LOWER属性匹配,避免大小写问题。nlp.pipe是流式处理,比逐句调用快很多。参数上,batch_size根据内存调,zh_core_web_trf需要 GPU 才快,CPU 上换zh_core_web_sm但精度会降。注意,领域词典要定期从新语料里补充,我一般每处理 1000 条文本就人工扫一遍漏抽的实体,加进词典。这一步没有捷径,但补过三轮之后召回率会明显上来。

3. 关系抽取与三元组生成:从句子到“主语-谓语-宾语”

3.1 基于依存句法分析的关系抽取模板

实体有了,下一步是抽关系。最稳的起步方式不是上大模型,而是依存句法分析加规则模板。为什么?因为大模型抽关系会编造,而句法分析给出的主谓宾结构是确定的。中文依存句法里,nsubj是名词主语,dobj是直接宾语,nsubjpass是被动主语。我一般用 spaCy 的zh_core_web_trf跑依存分析,然后写几条模板:主语 + 动词 + 宾语、主语 + 是 + 宾语、主语 + 的 + 属性 + 是 + 值。这些模板能覆盖设备手册和工单里 60% 以上的关系。剩下的复杂关系再用模型补。参数上,依存分析对句子长度敏感,超过 80 字的句子准确率下降明显,所以预处理阶段切句很重要。

def extract_triples(doc): triples = [] for token in doc: # 模板1:主语 + 动词 + 宾语 if token.dep_ == "ROOT" and token.pos_ == "VERB": subj = [w for w in token.lefts if w.dep_ in ("nsubj", "nsubjpass")] obj = [w for w in token.rights if w.dep_ in ("dobj", "attr", "pobj")] if subj and obj: triples.append((subj[0].text, token.text, obj[0].text)) # 模板2:A 是 B if token.dep_ == "ROOT" and token.lemma_ == "是": subj = [w for w in token.lefts if w.dep_ == "nsubj"] attr = [w for w in token.rights if w.dep_ == "attr"] if subj and attr: triples.append((subj[0].text, "是", attr[0].text)) return triples

逻辑说明:token.lefts和token.rights分别取左右依存子节点。dep_是依存标签,pos_是词性。参数上,ROOT是句子根节点,通常落在谓语动词或“是”上。注意,这个模板对被动句和省略句会漏,所以后面要补基于规则的后处理。我一般会把抽出来的三元组先存成列表,人工抽检 100 条,看准确率和召回率,再决定要不要加模板。

3.2 三元组规范化:去重、对齐和置信度过滤

抽出来的三元组不能直接入库,因为会有重复、同义实体、错误关系。规范化做三件事:实体对齐、关系归一、置信度过滤。实体对齐我一般用编辑距离加词向量相似度,阈值设 0.85。关系归一用同义词表,比如“额定电压”和“额定电压值”统一成“额定电压”。置信度过滤用规则打分:句法模板抽的给 0.9,模型抽的给 0.7,低于 0.6 的丢掉。这一步的参数需要根据你的语料调,没有万能值。我见过有人把阈值设太高,结果图谱稀疏得没法用;设太低,噪声边把推理结果带偏。建议先跑一遍统计,看三元组数量分布,再定阈值。

from difflib import SequenceMatcher def normalize_triples(triples, sim_threshold=0.85): normalized = [] seen = set() for s, p, o in triples: # 实体对齐:相似度高于阈值视为同一实体 s_norm = s.strip() o_norm = o.strip() p_norm = p.strip() key = (s_norm, p_norm, o_norm) if key in seen: continue # 过滤过短实体 if len(s_norm) < 2 or len(o_norm) < 2: continue seen.add(key) normalized.append((s_norm, p_norm, o_norm)) return normalized

逻辑说明:SequenceMatcher可以换成rapidfuzz更快。seen集合做精确去重,模糊对齐需要两两比较,数据量大时用faiss或annoy做向量索引。参数上,sim_threshold从 0.8 到 0.9 之间调,中文实体建议 0.85。注意,不要对关系做模糊匹配,关系词很短,模糊匹配容易把“是”和“不是”混在一起。

4. 图数据库写入与查询:把三元组变成可推理的图

4.1 用 Neo4j 存储知识图谱的最小可用配置

三元组有了,存哪里?常见做法是 Neo4j,因为 Cypher 查询直观,社区版免费。我一般用py2neo或官方neo4j驱动写入。最小可用配置:本地 Docker 跑一个 Neo4j,端口 7687,用户名密码默认。写入时用MERGE而不是CREATE,避免重复节点。批量写入用UNWIND,比逐条快几十倍。参数上,UNWIND的批次大小设 1000 到 5000,太大事务日志会爆。索引要提前建,对实体名称建唯一约束,否则 MERGE 会全表扫。

from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password")) def write_triples(tx, triples): query = """ UNWIND $triples AS t MERGE (s:Entity {name: t.subject}) MERGE (o:Entity {name: t.object}) MERGE (s)-[r:REL {type: t.predicate}]->(o) """ tx.run(query, triples=[{"subject": s, "predicate": p, "object": o} for s, p, o in triples]) with driver.session() as session: session.execute_write(write_triples, normalized_triples)

逻辑说明:MERGE保证节点和关系不存在才创建。UNWIND把列表展开成多行。参数上,auth换成你的密码,生产环境用环境变量。注意,关系属性type存的是关系词,查询时用WHERE r.type = '额定电压'。如果关系类型很多,建议把关系词也建成节点,但那样查询会复杂,起步阶段用属性够了。

4.2 用 Cypher 验证图谱质量的三条查询

写完不等于对。我一般跑三条查询验证:第一,查节点总数和关系总数,看规模是否符合预期;第二,查度数最高的 10 个节点,看是不是通用词霸榜;第三,查某个已知实体的两跳邻居,看关系是否合理。如果“的”“了”出现在高度节点里,说明预处理没洗干净。如果两跳邻居里出现明显错误的关系,说明关系抽取模板需要调。这三条查询花不了五分钟,但能省掉后面大量返工。

// 节点和关系总数 MATCH (n:Entity) RETURN count(n) AS node_count; MATCH ()-[r:REL]->() RETURN count(r) AS rel_count; // 度数最高的节点 MATCH (n:Entity)-[r:REL]-() RETURN n.name, count(r) AS degree ORDER BY degree DESC LIMIT 10; // 某个实体的两跳邻居 MATCH (n:Entity {name: '冷却系统'})-[r:REL*1..2]-(m) RETURN n.name, r, m.name LIMIT 50;

逻辑说明:第一条查规模,第二条查枢纽节点,第三条查局部结构。参数上,LIMIT根据你的图大小调,起步 50 够看。注意,两跳查询在超大图上会慢,加LIMIT和索引。如果发现高度节点是“设备”“系统”这种通用词,考虑把它们设为类别节点而不是实体节点。

5. 避坑与排查:知识图谱构建里最容易翻车的五个地方

5.1 实体识别把停用词和标点当成实体

现象:图谱里出现“的”“了”“我们”“。”作为实体节点,度数还很高。原因:通用 NER 模型对中文短文本会误判,或者词典匹配时没过滤停用词。解决:在实体抽取后加一层停用词过滤,用jieba的停用词表加自定义领域停用词。另外,实体长度小于 2 的直接丢,标点用正则过滤。

5.2 关系抽取模板覆盖不全导致图谱稀疏

现象:抽出来的三元组只有几百条,但语料有上万句。原因:依存句法模板只覆盖了主谓宾和“是”字句,遗漏了“位于”“包括”“导致”等常见关系。解决:统计语料里高频动词,针对 Top 20 动词各写一条模板。比如“导致”用nsubj + 导致 + dobj,“包括”用nsubj + 包括 + pobj。补模板比换模型快。

5.3 Neo4j 批量写入时事务超时

现象:写入几千条后报TransactionTimeout或内存溢出。原因:UNWIND批次太大,或者没建索引导致 MERGE 全表扫。解决:批次降到 1000,提前对Entity.name建唯一约束CREATE CONSTRAINT FOR (n:Entity) REQUIRE n.name IS UNIQUE。另外,写入时关掉自动索引更新,写完再开。

5.4 实体对齐阈值设太高导致同实体分裂

现象:图谱里“XJ-2000”和“XJ2000”是两个节点,“冷却系统”和“冷却系统设备”也是两个。原因:模糊匹配阈值设了 0.95,稍微有点差异就不合并。解决:阈值降到 0.85,同时加规则:去掉空格、连字符、大小写差异后再比。对于领域术语,维护一个同义词表,精确匹配优先。

5.5 忽略指代消解导致关系挂错实体

现象:“它启动了冷却系统”里的“它”被当成实体,或者关系挂到了错误的设备上。原因:没有做指代消解,代词直接进了抽取流程。解决:预处理阶段用fastcoref或规则做代词替换,把“它”替换成前一句的主语。规则版:如果句子以代词开头,找前一句的nsubj替换。这一步能明显提升三元组准确率。

6. 进阶技巧:用规则加模型做半自动知识图谱迭代

走到这里,你已经有一个能跑通的知识图谱构建流水线了。但真实场景里,语料会变,实体会新增,关系会演化。我一般会留一个“人工反馈闭环”:每轮抽取后,随机抽 50 条三元组人工标注对错,把错误案例分成三类——实体错、关系错、句子切分错。实体错就补词典,关系错就补模板,句子切分错就调正则。这个闭环跑三轮,准确率能从 60% 提到 85% 以上。下面是一个简单的反馈记录表结构,用 SQLite 存就行。

字段类型说明
idINTEGER主键
sentenceTEXT原始句子
subjectTEXT抽取的主语
predicateTEXT抽取的关系
objectTEXT抽取的宾语
labelTEXT人工标注:correct / entity_error / relation_error / split_error
noteTEXT备注
import sqlite3 conn = sqlite3.connect("feedback.db") conn.execute(""" CREATE TABLE IF NOT EXISTS triples ( id INTEGER PRIMARY KEY AUTOINCREMENT, sentence TEXT, subject TEXT, predicate TEXT, object TEXT, label TEXT, note TEXT ) """) def add_feedback(sentence, s, p, o, label, note=""): conn.execute( "INSERT INTO triples (sentence, subject, predicate, object, label, note) VALUES (?, ?, ?, ?, ?, ?)", (sentence, s, p, o, label, note) ) conn.commit()

逻辑说明:label字段用枚举值,方便统计。每轮迭代后跑一个GROUP BY label看错误分布。参数上,抽样比例我一般设 5%,语料少于 1000 条时抽 10%。注意,反馈数据要定期导出成 CSV 备份,别只存 SQLite。这个闭环不需要大模型,规则加人工就能把图谱质量稳住。我自己的习惯是每次改完模板先跑回归测试,用之前标注的 200 条句子验证,准确率不掉才上线。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询