2026/10/11 1:23:48

Python文本分析构建知识图谱:从非结构化文本到可推理图数据库

Python文本分析构建知识图谱:从非结构化文本到可推理图数据库 简介这份源码资源面向数据挖掘、信息检索与语义网络分析方向的学习者和开发者提供了一套基于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, attrLOWER) # 领域词典设备型号和故障代码 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_size256): 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)) # 模板2A 是 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_threshold0.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_errornoteTEXT备注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 条句子验证准确率不掉才上线。希望帮到你。本文还有配套的精品资源点击获取