简介:基于Python文本分析技术的自动知识图谱构建源码,面向需要从非结构化文本中抽取实体、属性及关系,进而构建知识网络的数据挖掘、信息检索与语义分析场景。资源包含完整Python工程,共26个文件,涵盖9个Python源码模块(参数配置、主流程控制、图像转文本等)、11个文本数据与说明文件、2个提示文件,以及Git忽略规则、LICENSE授权和PNG图示文件,压缩包约2.01MB。系统以深度文本分析为核心,支持从CSV、JSON、XML及图像等不同格式数据中提取信息,通过自动构建知识图谱,实现实体间关系的可视化与高效查询,并支持知识图谱的实时更新与维护,适用于金融分析、健康医疗、社交网络分析等数据密集型领域。已有367人学习下载,适合具备一定Python基础、希望依托真实工程快速上手知识图谱自动构建与应用的开发者。
1. 先讲一句会得罪人的结论:源码拿到手,不等于图谱能建起来
把「基于Python文本分析技术的自动知识图谱构建源码」这个项目标题拆开看,真正的门槛不在Python,而在文本分析之后那一步——实体怎么对齐、关系怎么判、图怎么存。很多人下载一套源码,跑通demo就以为完事了,换一批语料立刻翻车:分词把专有名词切碎、同一实体出现七八种叫法、Neo4j写十万条三元组卡到怀疑人生。这篇笔记就把我做过的一个从原始语料到可查询知识图谱的方案完整讲清楚,从jieba分词的工程配置讲起,一路到关系抽取、图数据库写入和验证。适合刚入门知识图谱构建、手里有一批非结构化文档、想用Python自动抽出三元组并落库的从业者。你能照着复现,也能避开那些让源码变废纸的暗坑。
2. 文本分析先行:jieba分词、实体识别与关键词抽取的工程化配置
自动知识图谱构建的起点是把自然语言变成结构化候选。这一步做不好,后面全白搭。常见做法是先把语料做清洗,再做分词、词性标注、命名实体识别,最后抽关键词。源码环节常在这一步就埋了雷,比如直接用默认词典跑专业领域语料。所以第一步不是写代码,是配词典。
2.1 初始化词表:让jieba认识你的领域词
文本分析的第一件事是加载用户词典。默认的jieba词典对通用词覆盖好,但一进具体行业就失灵。我遇到过一个农业领域的语料,「三品一标」「地理标志产品」被切成「三品 / 一标」,这直接导致后续实体抽取出现大量碎片化实体,图谱里多出几百个无意义节点。
常见做法是在项目初始化阶段加载自定义词表:
import jieba jieba.load_userdict("user_dict.txt") jieba.initialize() print(jieba.lcut("三品一标认证流程")) # 期望输出: ['三品一标', '认证', '流程']user_dict.txt格式每行一个词,后面可选词频和词性:
三品一标 100 nz 地理标志产品 100 nz 农产品质量安全追溯 100 nz词频越高,分词器越倾向把它当整体保留。nz表示专有名词,词性标注时会沿用。这行代码之后要跑一遍全量语料统计,把切碎的词捞回来补进词典,通常迭代两三轮就稳定了。
2.2 词性标注过滤:实体候选不是所有名词都收
分词之后做词性标注,从中筛出候选实体。专门跑一遍词性标注看着简单,实际要结合停用词和长度阈值做过滤。比如单字名词「县」「乡」「品」这种,收了只会污染图谱。
import jieba.posseg as pseg import re STOP_POS = {"xc", "w", "u"} # 虚词、标点 MIN_LEN = 2 raw_text = "山区农业知识库构建需要关注地理标志产品的地理溯源属性" def extract_entity_candidates(text): candidates = [] for word, flag in pseg.cut(text): if not re.search(r"[\u4e00-\u9fa5]", word): continue if flag in STOP_POS: continue if len(word) < MIN_LEN: continue if flag.startswith("n") or flag.startswith("v"): candidates.append({"word": word, "flag": flag}) return candidates for item in extract_entity_candidates(raw_text): print(item["word"], item["flag"])这段代码的思路:只保留长度≥2的名词和动词,虚词直接丢弃,并用正则把不含中文的token过滤掉。实际工程里,pseg.cut返回的flag体系基于北大词性标注集,nz俗称专有名词,vn是名动词,这两类在实体抽取中命中率最高。如果语料里英文缩写多,还可以加一层规则把连续大写字母拼起来当实体候选,比如「KG」「NLP」这类,这在初始阶段能救回很多漏掉的实体。
2.3 关键词抽取:用TF-IDF而不是拍脑袋选实体
很多源码里实体候选和关键词是分开抽的,但构建知识图谱时更好的做法是合并:用TF-IDF把每篇文档的关键词抽出来,和词性标注的候选取交集,能显著降低低质量实体的入图谱率。
from sklearn.feature_extraction.text import TfidfVectorizer import jieba.analyse documents = [ "白茶地理标志产品保护范围包括福鼎市行政辖区", "泰顺三杯香茶属于农产品地理标志登记保护产品", "龙泉灵芝孢子粉的产地环境要求覆盖土壤、水源与空气", ] tfidf = TfidfVectorizer(tokenizer=jieba.lcut, min_df=1, max_features=100) matrix = tfidf.fit_transform(documents) feature_names = tfidf.get_feature_names_out() scores = matrix.sum(axis=0).A1 word_scores = dict(zip(feature_names, scores)) top_words = sorted(word_scores.items(), key=lambda x: x[1], reverse=True)[:10] print(top_words)TfidfVectorizer里tokenizer直接传jieba.lcut,注意这会丢掉词性信息,所以它和词性标注是两条并行管线。min_df设为1表示出现一次也算,但如果语料规模大,建议提到2或3,把只在单篇出现一次的噪声词过滤掉。max_features限制参与计算的词表大小,这里100词只是演示,正式跑建议设500到1000,太小会漏掉低频但高价值的领域词。
这里有一个容易被源码坑到的地方:sklearn的tokenizer回调会把整段文本按函数拆分,如果语料清洗不到位,换行符和特殊字符会混进切词结果,导致vectorizer的词表里出现\n这种畸形词。所以在进TF-IDF之前,必须先做一次正则清洗:re.sub(r"[\s\u3000]+", "", text),全角空格也要清掉。这一步在自动构建流程里是第一个质检点。
3. 关系抽取的三条路径:从Regex到LTP再到LLM辅助
实体有了,还缺关系。常见做法是三种路径搭配使用:规则模板、依存句法分析、LLM辅助抽取。源码里如果只写了一种,换语料就会失灵,工程上我一般把三条路径做成可插拔的抽取器,按领域语料匹配情况动态切换。
3.1 规则模板:透明、可解释、先顶上去
规则模板适合关系动词明确的语料,比如「属于」「位于」「成立于」「主要生产」。优点是结果可以直接跑,缺点是需要人工维护模板库。这块值得写细一点,因为90%的翻车都发生在模板写错但没发现。
import re RELATION_PATTERNS = [ { "relation": "belongs_to", "pattern": r"(?P<entity1>[\u4e00-\u9fa5]{2,20}(?:产品|技术|平台))属于(?P<entity2>[\u4e00-\u9fa5]{2,20}(?:标志|体系|目录))", }, { "relation": "located_in", "pattern": r"(?P<entity1>[\u4e00-\u9fa5]{2,20}(?:茶园|基地|产区))(?:位于|地处)(?P<entity2>[\u4e00-\u9fa5]{2,20}(?:县|市|省))", }, ] def extract_by_rule(text): triples = [] for rule in RELATION_PATTERNS: for m in re.finditer(rule["pattern"], text): triples.append({ "subject": m.group("entity1"), "relation": rule["relation"], "object": m.group("entity2"), "source_text": m.group(0), }) return triples这里关键的坑是中文字符区间\u4e00-\u9fa5不包含「〇」和生僻地名用字,遇到「𠀾」这类扩展B区字直接吃不到。解决方式是改用\w配合re.UNICODE,或在规则里把字符区间放宽成[\u4e00-\u9fa5\u3007A-Za-z0-9]。另外「属于」这个动词高度歧义,规则模板命中时要把整句打印出来人工复核一遍,因为「属于」可能是「张三属于李四团队」,也可能是「这种行为属于违法」。模板库每扩一条规则,就要用历史语料回归一次,不然召回率上去了,精度掉下来,图谱里全是错误关系。
3.2 依存句法分析:用LTP抽主谓宾三元组
规则模板撑不住的时候,上依存句法。哈工大LTP在中文关系抽取里用得最多,抽取逻辑是找句子的核心动词,再通过「主谓关系SBV」找主语、通过「动宾关系VOB」找宾语,组合成三元组。
from ltp import LTP ltp = LTP() texts = ["福鼎白茶属于国家地理标志保护产品"] seg, hidden = ltp.seg(texts) pos = ltp.pos(hidden) dep, hidden_dep = ltp.dep(hidden) for word, p, rel in zip(seg[0], pos[0], dep[0]): print(f"{word}/{p} 依存关系: {rel}")输出里每一对词都有head索引,比如「属于」的head是0表示它是根节点,「白茶」的head指向「属于」且rel为SBV,就能构成三元组(福鼎白茶, belongs_to, 国家地理标志保护产品)。LTP的模型文件首次运行要下载,源码里如果没做本地缓存判断,每次冷启动都会卡在下载环节。工程上的做法是判断模型目录是否存在,不存在才触发下载,并把下载超时设为5分钟。
3.3 LLM辅助抽取:用哪个模型,用什么Prompt
当领域语料里关系的表达方式太多变时,规则和依存句法都显得吃力,LLM辅助是最后的兜底。常见做法是用OpenAI兼容接口或者本地部署的Qwen、ChatGLM这类模型,让模型基于给定Schema抽取JSON三元组。
- 提示词模板要带示例,尤其是关系类型的枚举值固定传入,防止模型发明新关系名。
- 返回结果需要做JSON解析兜底:模型偶尔会输出Markdown包裹的JSON,源码里必须写一层剥离逻辑,把
```json壳去掉再json.loads。 - 成本控制:如果语料量有几十万篇,全量走LLM不现实。实践中先让规则模板抽一遍,再用LLM处理规则未命中的段落。
把三种抽取器的结果统一合并时,字段必须标准化,比如规则模板输出的belongs_to和LLM输出的belongs_to保持同一命名,不然写入图谱时会分裂成两种关系,造成图结构混乱。常见做法是定义全局关系枚举类:
from enum import Enum class RelationTypes(str, Enum): BELONGS_TO = "belongs_to" LOCATED_IN = "located_in" PRODUCES = "produces" REGISTERED_AT = "registered_at"每条三元组进入数据库前都做一次RelationTypes(value)强校验,不在枚举里的直接丢弃并写入日志。这个方法看起来很笨,但它能挡住90%的脏数据入库。
3.4 三种方法的选型对比:什么时候换路径
| 方法 | 优点 | 场景 | 成本排序 |
|---|---|---|---|
| 规则模板 | 结果可控、不需要模型、可解释性强 | 动词表达固定、领域封闭 | 低 |
| 依存句法分析 | 泛化能力强,能发现未预定义的关系 | 长句多、句子结构复杂 | 中 |
| LLM辅助抽取 | 理解语义、召回率高 | 语料表达开放、Schema定义清楚 | 高 |
常规流程是先用规则模板把批量语料过一遍,统计覆盖率;覆盖率低于40%再加入依存句法;最后才考虑LLM。顺序不能反过来,否则成本高还难定位错误来源。
4. 知识图谱落地避坑:Neo4j写入慢、实体歧义与孤立节点排查
自动构建源码最容易忽略的环节其实是数据入库。很多人把三元组打印出来就完事,却没有处理图中的重复实体、孤立节点和写入性能。下面是项目落地中高频出现的四个问题,每一条都是我实际踩过的。
4.1 现象:用CREATE一条条插入三元组,速度慢到无法忍受
源码常见写法是遍历三元组列表,逐条用graph.run("CREATE ...")写入Neo4j。十万条数据可能要跑几个小时,而且中途失败无法回滚。原因是每一次graph.run()都触发一次事务提交,网络往返和事务开销把吞吐拖垮。
解决方法是改用批量MERGE加UNWIND:
UNWIND $batch AS row MERGE (s:Entity {name: row.subject}) ON CREATE SET s.source = row.source_text MERGE (o:Entity {name: row.object}) ON CREATE SET o.source = row.source_text MERGE (s)-[r:RELATION {type: row.relation}]->(o)配合Python侧的批量提交,比如每500条一批:
from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password")) def write_triples_batch(triples, batch_size=500): with driver.session() as session: for i in range(0, len(triples), batch_size): batch = triples[i:i + batch_size] session.run(""" UNWIND $batch AS row MERGE (s:Entity {name: row.subject}) MERGE (o:Entity {name: row.object}) MERGE (s)-[r:RELATED {type: row.relation}]->(o) """, batch=batch) print(f"已写入 {i + len(batch)} / {len(triples)}")batch大小不是越大越好,Neo4j单条事务默认内存有限,超过2000条容易触发HeapSpace异常,我一般压在500上下。MERGE的本质是「存在即匹配、不存在即创建」,配合实体的唯一约束才能真正去重。
4.2 现象:同一个实体在图里出现多个节点:「Python语言」和「Python」
原因出在实体消歧上——统一统称和带修饰语的实体没做归一化。常见做法是在写入图谱前做一层entity_normalize:
import re def normalize_entity(name): name = re.sub(r"[\s\u3000]+", "", name) name = re.sub(r"(语言|编程语言|技术|框架)$", "", name) # 去后缀 return name.strip()括号内的别名也要处理,比如「Python(计算机语言)」应归一化成「Python」。把归一化后的名字作为图谱节点的唯一标识,原名保留在aliases属性里,查询和可视化时仍然可读。
4.3 现象:图谱一圈query出来,一半节点没有关系,全是孤立点
孤立节点的出现说明关系抽取的覆盖率极低,或者实体对齐环节把两个不同实体错误合并了。我通常拿下面这句Cypher做体检:
MATCH (n:Entity) WHERE NOT (n)--() RETURN count(n) AS orphans;如果orphans占比超过20%,就要回头检查关系抽取环节。另一个注意点:构建自动知识图谱源码时,不要把每篇文档单独跑完就入库,先全部抽取完,再做一次全局实体合并,否则同一实体在不同文档里写法略不同就会分裂成多个孤立点。
4.4 现象:Windows下跑源码直接报UnicodeDecodeError
中文语料在Windows默认GBK编码下被open()读入时频繁报错。这不算复杂问题,但确实最劝退新手。统一做法是读文件时显式指定编码:
with open("corpus.txt", "r", encoding="utf-8") as f: text = f.read()如果源码里到处都是open(),建议全局搜一遍。另外脚本文件头部加# -*- coding: utf-8 -*-并不能改变运行时读文件的编码,只有encoding参数才有效。
5. 成套自动构建源码:从原始语料到图谱的三层流水线
把前面几章的内容串起来,就是一个完整的自动知识图谱构建流水线。这一章给出源码的工程结构和关键实现。注意这不是某份开源包的原稿,而是我多年经验里的标准工程做法,你可以直接对照改造。
5.1 模块划分原则:先定接口,再写实现
我习惯把流水线拆成三个模块,对应文本分析、关系抽取和图谱写入:
| 模块 | 文件 | 职责 |
|---|---|---|
| 数据预处理 | preprocess.py | 清洗原始语料,生成干净句子列表 |
| 文本分析器 | analyzer.py | 分词、词性标注、实体候选生成 |
| 关系抽取器 | extractor.py | 规则+依存+LLM三路抽取,合并三元组 |
| 图谱写入器 | graph_writer.py | 实体归一化、批量MERGE入库 |
| 流水线入口 | pipeline.py | 组装各模块,支持断点续跑 |
接口先定成Python dataclass,让每个模块的输入输出清晰稳定:
from dataclasses import dataclass from typing import List, Dict, Optional @dataclass class Entity: name: str type: str aliases: List[str] attrs: Dict[str, str] = field(default_factory=dict) @dataclass class Relation: subject: str predicate: str object: str confidence: float source_text: str5.2 流水线入口:控制流程、记录断点、方便续跑
工程化的自动构建源码必须有断点续跑能力。语料几十万篇,跑一半中途挂了再从头开始会让人崩溃。常见做法是记录每篇文档的处理状态,已处理完成的跳过去:
import json from pathlib import Path class KGConstructionPipeline: def __init__(self, source_dir: str, checkpoint_file: str): self.source_dir = Path(source_dir) self.checkpoint_file = Path(checkpoint_file) self.done_ids = self._load_checkpoint() def _load_checkpoint(self): if self.checkpoint_file.exists(): return set(json.loads(self.checkpoint_file.read_text(encoding="utf-8"))) return set() def _save_checkpoint(self, doc_id: str): self.done_ids.add(doc_id) self.checkpoint_file.write_text( json.dumps(list(self.done_ids), ensure_ascii=False), encoding="utf-8", ) def run(self): for doc_path in self.source_dir.glob("*.txt"): doc_id = doc_path.stem if doc_id in self.done_ids: continue raw_text = doc_path.read_text(encoding="utf-8") entities = self._extract_entities(raw_text) relations = self._extract_relations(raw_text, entities) self._write_to_graph(entities, relations) self._save_checkpoint(doc_id) print(f"完成 {doc_id}: {len(entities)} 个实体, {len(relations)} 条关系")checkpoint_file的设计很轻量,本质是一个已完成任务清单。挂掉后重启,直接跳过已完成文档。但注意_save_checkpoint要放在_write_to_graph成功之后,否则会出现入库失败但标记已完成的情况。
5.3 实体合并:写入前必须做的最后一道防线
实体合并放在模块内部的好处是,它可以独立测试。常见做法是建立权威实体表,用精确匹配和别名映射去合并:
class EntityMerger: def __init__(self, alias_map: Dict[str, str]): self.alias_map = alias_map # 别名 -> 标准名 def merge(self, entity_name: str) -> str: if entity_name in self.alias_map: return self.alias_map[entity_name] normalized = normalize_entity(entity_name) return self.alias_map.get(normalized, normalized)alias_map需要人工维护或者从已有语料中挖掘。实践里我经常从分词结果里统计高频共现来生成候选映射,比如「地理标志」和「GI」在大量文档中共现,就自动加入alias_map,再加人工审核。这一步做得好不好,直接决定最终图谱的可信度。
6. 让图谱能自检:用Cypher验证构建质量与增量更新的一个习惯
最后这套技巧不是锦上添花,而是源码交付后能不能长期用的关键。常见误区是图谱建完就不管了,等到用的时候发现数据烂了也无从追溯。我习惯在每次构建结束后,让流水线自动输出一份质量报告,再用几个固定查询做回归验证。
6.1 质量报告怎么办:统计节点、关系、孤立率
构建脚本尾部追加这样的统计逻辑:
MATCH (n:Entity) RETURN count(n) AS entity_count; MATCH (:Entity)-[r:RELATED]->(:Entity) RETURN count(r) AS relation_count; MATCH (n:Entity) WHERE NOT (n)--() RETURN count(n) AS orphan_count;三个数字组合起来看:实体多、关系少,说明关系抽取召回不足;孤立点多,说明实体对齐失效。我给自己定的及格线是关系数/实体数≥1.2,孤立率≤15%。低于这个标准就回去调关系模板或检查合并逻辑。
6.2 增量更新怎么办:用MERGE做幂等写入
新语料进来时,不需要重建整个图谱。用第4章讲的MERGE方式天然支持增量。新增一篇文章,先抽取新三元组,再跑一遍批量MERGE,已有节点和关系不会被重复创建。检查一下幂等性是否生效:
MATCH (s:Entity {name: "福鼎白茶"})-[r:RELATED]->(o:Entity) RETURN s.name, r.type, o.name;重复执行两次,结果不变,就说明MERGE生效了。如果发现节点还是重复,多半是实体归一化没做干净,需要检查normalize_entity的规则。
6.3 把验证写进流水线,不靠人肉抽查
我在流水线末尾固定挂一个validate()方法,每次全量构建或增量更新都运行并写日志:
class GraphValidator: def __init__(self, driver): self.driver = driver def run_checks(self): with self.driver.session() as session: orphan_ratio = session.run(""" MATCH (n:Entity) WHERE NOT (n)--() WITH count(n) AS orphans MATCH (m:Entity) RETURN orphans * 1.0 / count(m) AS ratio """).single()["ratio"] return {"orphan_ratio": round(orphan_ratio, 4)}万一图谱质量下降,立即对照日志定位是新增语料引入的脏数据,还是抽取模板被新句式击穿。这个习惯帮我省掉了很多次「图谱越用越烂却查不出原因」的会议。知识图谱构建是迭代工程,源码只是起点,验证和修正才是长期要投入精力的部分。希望这篇笔记能让你把每一份语料真正变成可用的知识资产,少花点时间在填坑上——那些坑我替你先踩过了。
本文还有配套的精品资源,点击获取