农业知识图谱从零落地:Neo4j源码包拆解与实战 简介这份资源面向计算机、人工智能、通信工程等专业的高校学生与科研从业者提供农业领域知识图谱构建的完整实践方案涵盖数据爬取、实体识别、关系抽取到Neo4j图数据库可视化落地的全流程可直接用于毕业设计、课程设计或项目立项演示。压缩包共46个文件约21.42MB以Python脚本、txt文本、csv数据表、xml配置及md说明文档为主脚本负责爬取与三元组抽取文本与表格承载分词词典、NER结果和结构化数据配置与说明文件辅助环境搭建与复现。目前已有58人学习下载。项目围绕茶叶、农作物、植物等农业语料包含百度百科数据获取、LTP分词与命名实体识别、三元组抽取及Neo4j建图等模块并附设计文档与停用词表便于读者理解知识图谱从原始语料到图数据库的完整链路也可在此基础上修改扩展实现其他领域或功能的迁移应用。1. 农业知识图谱从零落地一份能跑通的 Neo4j 源码包拆解农业领域的数据有个特点散、杂、非结构化。同一批茶叶数据可能一部分躺在百科词条里一部分藏在种植手册的段落中还有一部分是农技站表格里的半结构化记录。想把它们串成一张能查询、能推理的知识网络靠人工整理基本不现实。这份「农业领域知识图谱构建」源码包解决的正是这个环节从百科页面抓取原始语料用 LTP 做分词和命名实体识别再通过三元组抽取把实体关系落成结构化数据最后导入 Neo4j 做可视化查询。整条链路覆盖了数据获取、NLP 处理、关系抽取、图数据库写入四个阶段适合做毕业设计、课程设计也适合想快速搭一个领域图谱原型的开发者。代码是 Python 写的依赖 LTP 和 Neo4j目录里带了茶叶、农作物、植物等多个数据集的处理结果拿来就能对照跑。2. 数据链路拆解从百科页面到三元组文件2.1 爬取层getData_from_baike.py 怎么把百科页面变成干净文本这个脚本是整个链路的第一环负责从百科类页面拉取农业相关词条的正文内容。它的核心逻辑不复杂给定一批种子词条名逐个请求页面解析 HTML 拿到正文段落去掉导航、引用、广告等噪声输出成纯文本文件。项目里my_tmp.txt就是中间产物my_datas_tea.csv、my_datas_crops.csv这些则是按领域分好的结构化数据。实际跑的时候请求频率和页面结构变化是两个最容易翻车的点。百科类站点的 HTML 结构不是铁板一块不同词条可能用不同的模板渲染解析规则写死了就容易漏抓。我一般会在解析函数里加一层兜底先按主选择器提取拿不到内容再降级到通用段落选择器。import requests from bs4 import BeautifulSoup import time import csv def fetch_baike_content(keyword, max_retry3): 抓取单个词条的正文段落 keyword: 词条名称 max_retry: 网络抖动时的重试次数 url fhttps://baike.example.com/item/{keyword} # 实际使用时替换为真实入口 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } for attempt in range(max_retry): try: resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) # 主选择器大多数词条正文在这个容器里 content_div soup.find(div, class_lemma-summary) if not content_div: # 兜底降级到所有段落 paragraphs soup.find_all(p) text \n.join(p.get_text(stripTrue) for p in paragraphs) else: text content_div.get_text(stripTrue) return text except Exception as e: print(f[重试 {attempt1}] {keyword} 抓取失败: {e}) time.sleep(2) return def batch_fetch(keywords, output_csv): 批量抓取并写入 CSV每行一个词条 with open(output_csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([keyword, content]) for kw in keywords: content fetch_baike_content(kw) if content: writer.writerow([kw, content]) time.sleep(1.5) # 控制频率避免触发限流这段代码的关键参数有三个timeout控制单次请求超时建议不低于 10 秒time.sleep(1.5)是请求间隔太短容易被封太长效率低1 到 2 秒是比较稳的区间max_retry给 3 次足够再多说明目标站点本身有问题。输出 CSV 用utf-8编码不然后面读的时候中文会乱码。项目里my_datas_tea.csv和my_datas_chinese_agriculture.csv就是按这个逻辑产出的可以直接拿来当输入。2.2 NLP 处理层myLTP.py 与分词、实体识别的参数选择拿到原始文本后下一步是分词和命名实体识别。项目用 LTP语言技术平台做这件事myLTP.py是入口脚本ner_results_tea.txt、ner_results_crops.txt这些是输出结果。LTP 的分词和 NER 是流水线式的先分词再词性标注再命名实体识别。农业领域有个特殊之处很多专业词汇不在通用词典里比如「茶多酚」「光合速率」「土壤墒情」这类词默认分词器可能切错。常见做法是加载自定义词典。项目里lexicon.txt、crops lexicon.txt、tea lexicon.txt就是干这个用的。LTP 支持通过add_word方式动态加词也可以在初始化时指定词典文件。from ltp import LTP # 初始化 LTP默认加载小型模型速度快但精度略低 ltp LTP(LTP/small) # 加载农业领域自定义词典每行一个词 def load_lexicon(ltp_model, lexicon_path): with open(lexicon_path, r, encodingutf-8) as f: for line in f: word line.strip() if word: ltp_model.add_word(word) load_lexicon(ltp, lexicon.txt) load_lexicon(ltp, crops lexicon.txt) def extract_ner(text): 对输入文本做分词和命名实体识别 返回: (分词结果, 实体列表) seg, hidden ltp.seg([text]) ner ltp.ner(hidden) # ner 返回的是 [(实体类型, 起始位置, 结束位置), ...] entities [] for tag, start, end in ner[0]: entity_word .join(seg[0][start:end1]) entities.append((tag, entity_word)) return seg[0], entities # 示例 text 茶树喜欢酸性土壤适宜种植在排水良好的坡地。 words, ents extract_ner(text) print(分词:, words) print(实体:, ents)这里有几个参数值得注意。模型选择上LTP/small速度快适合数据量大、对精度要求不极端的场景如果做论文实验需要更高精度可以换LTP/base或LTP/large但显存和耗时都会上去。自定义词典的加载顺序有讲究先加载通用词典再加载领域词典避免领域词被通用规则切碎。项目里stopwords目录下的ltp_stopwords.txt、jiebadic.txt、百度停用词表.txt是停用词资源在后续三元组抽取时用来过滤无意义的高频词。2.3 三元组抽取triple_ie.py 的关系定义与抽取策略三元组是知识图谱的基本单元格式是「头实体-关系-尾实体」。triple_ie.py负责从 NER 结果和原始文本中抽取出这些三元组输出到triple_results_tea.txt、triple_results_crops.txt等文件。项目里my_triple_datas.txt和最新三元组.txt是汇总后的结果。关系定义是这一步的核心。农业领域常见的关系类型包括属于茶树属于经济作物、适宜茶树适宜酸性土壤、含有茶叶含有茶多酚、防治某药剂防治某病害。这些关系有些可以通过规则匹配抽出来有些需要依存句法分析。import re # 定义关系触发词模式 RELATION_PATTERNS { 属于: [r(.?)属于(.)], 适宜: [r(.?)适宜(.)], 含有: [r(.?)含有(.)], 防治: [r(.?)防治(.)], } def extract_triples(sentence, entities): 基于规则从句子中抽取三元组 sentence: 原始句子 entities: NER 识别出的实体列表 [(类型, 词), ...] triples [] entity_words [e[1] for e in entities] for relation, patterns in RELATION_PATTERNS.items(): for pattern in patterns: match re.search(pattern, sentence) if match: head match.group(1).strip() tail match.group(2).strip() # 只保留头尾都命中实体的三元组降低噪声 if any(h in head for h in entity_words) or any(t in tail for t in entity_words): triples.append((head, relation, tail)) return triples # 示例 sentence 茶树属于经济作物适宜种植在酸性土壤中。 entities [(作物, 茶树), (作物, 经济作物), (土壤, 酸性土壤)] triples extract_triples(sentence, entities) for t in triples: print(t)规则抽取的优点是可控、可解释缺点是覆盖不全。项目里还用了get_triple_slice.py做切片处理get_struct_data.py做结构化数据转换re_sampling.py做重采样平衡。这些脚本的组合使用能把原始语料逐步收敛成可导入 Neo4j 的三元组文件。实际跑的时候关系触发词需要根据你的领域数据调整不能直接照搬。农业领域的关系词比较固定但不同子领域茶叶、作物、植物的表述习惯有差异项目里按tea、crops、plants分开处理就是这个原因。3. Neo4j 导入与可视化createKG_neo4j.py 的完整操作3.1 图模型设计节点标签与关系类型的确定在导入之前得先想清楚图模型怎么设计。农业知识图谱的节点类型一般包括作物、土壤类型、病害、农药、种植技术、地理区域。关系类型对应三元组里的关系词。项目里createKG_neo4j.py是导入脚本它读取triple_results_*.txt文件把三元组逐条写入 Neo4j。节点标签的设计有个原则标签数量不要太多否则查询时索引效率会下降。我一般会把语义相近的实体归到同一个标签下比如「茶树」「小麦」「玉米」都归到Crop标签用属性区分具体种类。关系类型则保持和三元组中的关系词一致方便后续查询。from py2neo import Graph, Node, Relationship # 连接 Neo4j默认 bolt 端口 7687 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def create_node(label, name, **properties): 创建或合并节点 label: 节点标签如 Crop、Soil name: 实体名称作为唯一标识 node Node(label, namename, **properties) graph.merge(node, label, name) # 按 name 去重 return node def create_relation(head_label, head_name, rel_type, tail_label, tail_name): 创建两个节点之间的关系 head create_node(head_label, head_name) tail create_node(tail_label, tail_name) rel Relationship(head, rel_type, tail) graph.merge(rel) return rel # 示例从三元组文件批量导入 def import_triples(filepath, head_labelEntity, tail_labelEntity): with open(filepath, r, encodingutf-8) as f: for line in f: parts line.strip().split(\t) if len(parts) ! 3: continue head, relation, tail parts create_relation(head_label, head, relation, tail_label, tail) import_triples(triple_results_tea.txt)graph.merge是关键它保证同一个实体不会重复创建。如果用graph.create每次导入都会新增节点跑两遍数据就翻倍了。auth里的密码换成你本地 Neo4j 的密码默认端口是 7687如果改了要在连接串里同步改。导入大批量数据时建议每 500 条提交一次事务避免内存溢出。3.2 可视化查询Cypher 语句与常见查询场景数据导入后Neo4j 的 Browser 界面可以直接跑 Cypher 查询。农业知识图谱常见的查询场景有几种查某个作物的所有关系、查两个实体之间的路径、按关系类型统计。// 查询「茶树」的所有直接关系 MATCH (n:Crop {name: 茶树})-[r]-(m) RETURN n, r, m LIMIT 50; // 查询「茶树」和「酸性土壤」之间的最短路径 MATCH (a:Crop {name: 茶树}), (b:Soil {name: 酸性土壤}) MATCH p shortestPath((a)-[*..5]-(b)) RETURN p; // 统计各类关系的数量 MATCH ()-[r]-() RETURN type(r) AS relation, count(r) AS cnt ORDER BY cnt DESC;第一条查询用来做可视化展示LIMIT 50防止一次性渲染太多节点导致浏览器卡死。第二条用shortestPath找两个实体之间的关联路径在推荐场景里很有用。第三条做关系分布统计能快速看出哪些关系抽取得多、哪些少反过来指导抽取规则的优化。项目里README.md应该带了基本的查询示例但实际用的时候还是得根据自己的数据调整标签名和属性名。4. 避坑与排查跑通这份源码的五个血泪经验4.1 坑一LTP 模型下载失败或加载报错现象运行myLTP.py时卡在模型加载报ConnectionError或Model not found。原因LTP 的预训练模型需要从远端拉取网络不稳定时会失败。另外不同版本的 LTP 对应的模型名称不一样LTP/small在旧版本里可能叫small。解决先确认 LTP 版本pip show ltp看版本号。如果是 4.x模型名用LTP/small如果是 3.x用small。模型下载失败可以手动下载后放到缓存目录或者换用LTP/base试试。项目里__pycache__下有triple_ie.cpython-36.pyc说明原始环境是 Python 3.6如果你用 3.8 以上部分依赖可能需要重新装。4.2 坑二Neo4j 导入时中文乱码或节点重复现象导入后节点名称显示为乱码或者同一个实体出现多个节点。原因乱码通常是文件编码问题triple_results_*.txt如果不是 UTF-8 编码读进来就会乱。节点重复是因为用了create而不是merge或者merge的键没设对。解决统一把所有文本文件转成 UTF-8用iconv -f GBK -t UTF-8或者 Python 的codecs模块处理。节点去重检查merge的第二个参数确保用的是唯一标识属性比如name。如果已经有重复节点可以用 Cypher 清理MATCH (n:Entity) WITH n.name AS name, collect(n) AS nodes WHERE size(nodes) 1 FOREACH (n IN tail(nodes) | DETACH DELETE n);4.3 坑三三元组抽取结果噪声太多现象triple_results_*.txt里大量无意义的三元组比如「的-属于-了」这种。原因规则匹配太宽泛没有做实体校验。停用词表没加载导致「的」「了」「在」这些词也被当成实体。解决在抽取前先过滤停用词项目里stopwords目录下的几个文件就是干这个的。另外头尾实体必须至少有一个命中 NER 结果否则丢弃。还可以加一条规则实体长度小于 2 的不要。re_sampling.py可以做后处理平衡但前提是原始数据质量不能太差。4.4 坑四爬取数据被限流或页面结构变化现象getData_from_baike.py跑一半报 403 或返回空内容。原因请求频率太高触发限流或者目标页面改版导致解析规则失效。解决请求间隔调到 2 秒以上加User-Agent头必要时用代理池但注意合规。页面结构变化只能靠定期检查解析规则建议把解析逻辑单独抽成函数方便替换。项目里my_tmp.txt是中间缓存如果重新爬取可以先清空这个文件再跑。4.5 坑五Python 版本与依赖冲突现象pip install -r requirements.txt报错或者运行时报ImportError。原因项目原始环境是 Python 3.6部分库如旧版 LTP、py2neo在新版本 Python 上不兼容。解决建议用虚拟环境python -m venv venv然后激活。如果必须用 3.6可以用 conda 建一个 3.6 的环境。py2neo 的版本也要注意2021 之前的版本和之后的 API 有差异Graph的初始化参数可能不同。项目里.idea目录是 PyCharm 的配置不影响运行可以忽略。5. 进阶技巧用 APOC 做批量导入与图谱质量校验5.1 用 APOC 替代逐条 merge导入速度提升一个量级前面createKG_neo4j.py用的是逐条merge数据量小的时候没问题但三元组上万条之后速度会明显变慢。Neo4j 有个 APOC 插件支持从 CSV 批量导入速度能快很多。思路是把三元组文件转成 CSV然后用apoc.load.csv一次性读入。// 确保 APOC 插件已安装在 neo4j.conf 里加 dbms.security.procedures.unrestrictedapoc.* // 批量导入三元组 CSV CALL apoc.periodic.iterate( LOAD CSV WITH HEADERS FROM file:///triples.csv AS row RETURN row, MERGE (h:Entity {name: row.head}) MERGE (t:Entity {name: row.tail}) MERGE (h)-[r:REL {type: row.relation}]-(t), {batchSize: 500, parallel: false} );batchSize控制每批处理的行数500 到 1000 比较合适。parallel: false是因为merge在并发时可能产生锁竞争关掉更稳。CSV 文件要放到 Neo4j 的import目录下路径用相对路径。这个方式比逐条 Python 写入快 5 到 10 倍数据量大的时候值得换。5.2 图谱质量校验三个必查指标导入完成后怎么判断图谱质量我一般会查三个指标。第一孤立节点比例没有任何关系的节点占比超过 20%说明抽取或导入有问题。第二关系类型分布如果某一种关系占了 80% 以上可能是规则太宽泛。第三实体重复率同名实体出现在多个标签下说明标签设计有问题。// 孤立节点统计 MATCH (n:Entity) WHERE NOT (n)--() RETURN count(n) AS isolated_count; // 关系类型分布 MATCH ()-[r]-() RETURN type(r) AS rel_type, count(*) AS cnt ORDER BY cnt DESC; // 同名跨标签实体 MATCH (n) WITH n.name AS name, collect(DISTINCT labels(n)) AS label_sets WHERE size(label_sets) 1 RETURN name, label_sets LIMIT 20;这三个查询跑一遍基本能判断图谱的健康度。如果孤立节点太多回去检查三元组抽取的实体匹配逻辑如果关系分布太集中调整关系触发词如果同名跨标签统一标签命名规范。我自己的习惯是每次导入新数据后都跑一遍这三个查询确认没问题再做可视化展示。从那以后我每次导入前都强制走一遍校验省得后面返工。希望帮到你。本文还有配套的精品资源点击获取