简介:这份资源面向计算机、数学、电子信息等专业的学生与知识图谱初学者,提供一套基于Neo4j的农业领域知识图谱构建完整源码,可用于课程设计、期末大作业或毕设项目参考。项目覆盖从百度百科爬取农业数据、数据分类,到结构化数据生成三元组、非结构化数据分句(LTP)、分词(jieba)、命名实体识别(LTP)以及基于依存句法分析(主谓关系等)抽取关系的全流程,并最终在Neo4j中完成可视化呈现。压缩包共46个文件,以23个txt文本、8个Python脚本、7个csv数据表为主,另含xml配置、md说明等,整体约21.41MB,目录按茶叶、农作物、植物等主题分模块组织,便于对照理解。目前已有87人学习下载。读者可借此掌握知识图谱从数据采集、清洗、实体关系抽取到图数据库落地的完整链路,并参考其中的停用词表、词典与三元组结果文件进行调试与二次开发。
1. 从一堆 CSV 到 Neo4j 图谱:这套农业知识图谱源码到底能跑出什么
手里有一批农业领域的文本,散在 CSV、txt 和爬虫结果里,想把它变成 Neo4j 里能点开、能查关系的可视化图谱,中间缺的往往不是算法思路,而是一条能跑通的流水线。这套源码解决的就是这件事:从百度百科抓取农业词条,做数据分类,结构化数据直接生成三元组,非结构化文本走 LTP 分句、jieba 分词、LTP 命名实体识别,再用依存句法分析里的主谓关系等抽取三元组,最后写进 Neo4j 出图。它适合做课程设计、毕设、算法源码参考的从业者,也适合想拿一套完整链路练手的新手。我拆完的感受是:链路完整,但每一步都有参数和格式的坑,照着跑之前得先搞清楚数据长什么样、代码在哪一步等你。
2. 数据爬取与分类:百度百科词条怎么变成可用的 CSV
2.1 爬取脚本的入口与字段设计
getData_from_baike.py是整条链路的起点。它的任务很明确:给定一批农业相关词条名,去百度百科抓取摘要、基本信息栏等文本,落成 CSV。常见做法是用 requests 拿页面,再用 BeautifulSoup 或 lxml 解析,把词条名、摘要、基本信息键值对分别存列。这里要注意,百度百科的页面结构会变,选择器写死就容易翻车,所以脚本里一般会把解析逻辑单独抽出来,方便你换选择器。
# getData_from_baike.py 核心逻辑示意 import requests from bs4 import BeautifulSoup import csv def fetch_baike(keyword): url = "https://baike.baidu.com/item/" + keyword headers = {"User-Agent": "Mozilla/5.0"} resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "lxml") # 摘要通常在 meta 或特定 class 的 div 里,按实际页面调整 summary = soup.find("meta", {"name": "description"}) summary_text = summary["content"] if summary else "" return {"keyword": keyword, "summary": summary_text} if __name__ == "__main__": keywords = ["茶树", "水稻", "小麦"] rows = [fetch_baike(k) for k in keywords] with open("my_datas.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["keyword", "summary"]) writer.writeheader() writer.writerows(rows)这段代码里,timeout别省,不然一个词条卡住整个脚本就挂在那里。encoding="utf-8-sig"是为了 Excel 打开不乱码,后面 Neo4j 读取时用 utf-8 也能兼容。关键词列表建议单独放一个 txt,方便替换成你自己的领域词。抓取频率要控制,加个time.sleep(1)是基本礼貌,也能降低被封的风险。
2.2 数据分类:结构化与非结构化的分水岭
抓回来的数据不是一种形态。基本信息栏那种“键:值”是结构化的,摘要和正文段落是非结构化的。data_parser.py和get_struct_data.py干的就是分类和清洗。结构化数据直接映射成三元组,比如“茶树 - 科 - 山茶科”;非结构化数据得先分句再抽关系。分类没做好,后面三元组就会混进一堆噪声。
我一般会先看一眼my_datas_tea.csv、my_datas_crops.csv这类文件的列名和空值比例。如果某一列超过一半是空的,说明抓取选择器可能失效了,得回去改getData_from_baike.py。分类阶段还会用到停用词表,项目里带了stopwords目录,里面有百度停用词表.txt、中文停用词表.txt、四川大学机器智能实验室停用词库.txt等,这些在分词和实体识别后过滤噪声时都会用到。
提示:停用词表不是越多越好,农业领域里“种植”“产量”这类词如果被当停用词滤掉,关系抽取会丢关键信息。建议先跑一遍,看抽出来的三元组里有没有明显该保留的词被删了。
3. 非结构化文本处理:LTP 分句、jieba 分词与命名实体识别
3.1 LTP 分句与 jieba 分词的分工
非结构化文本进myLTP.py和triple_ie.py之前,先要分句。LTP 的分句接口按标点切,但农业文本里常有“亩产 500 公斤,比去年增长 10%”这种带数字和百分号的句子,切的时候要留意别把数字和单位切开。分句之后用 jieba 分词,jiebadic.txt是自定义词典,把“杂交水稻”“有机茶园”这类领域词加进去,分词准确率会明显不一样。
# myLTP.py 分句与分词示意 import jieba from ltp import LTP ltp = LTP() # 默认加载小模型,显存不够可换 tiny jieba.load_userdict("jiebadic.txt") def split_and_seg(text): # LTP 分句 sents = ltp.sent_split([text]) results = [] for sent in sents: words = list(jieba.cut(sent)) results.append({"sentence": sent, "words": words}) return results if __name__ == "__main__": sample = "茶树喜欢温暖湿润气候。亩产茶叶约 100 公斤。" for item in split_and_seg(sample): print(item["sentence"], item["words"])ltp.sent_split返回的是句子列表,jieba.cut返回生成器,转成 list 方便后面做词性标注和实体识别。jiebadic.txt里一行一个词,可以带词频和词性,比如“杂交水稻 100 n”。如果你发现“茶树”被切成“茶”和“树”,就是词典没生效,检查文件路径和编码。
3.2 LTP 命名实体识别与依存句法分析抽三元组
命名实体识别用 LTP 的ner接口,能标出人名、地名、机构名等。农业领域更关心的是作物、病虫害、农药这类实体,LTP 通用模型不一定全认,所以项目里还配合了词典和规则。ner_results_tea.txt、ner_results_crops.txt这些文件就是识别结果落盘,方便你核对哪些实体被漏了。
依存句法分析是抽三元组的关键。主谓关系(SBV)、动宾关系(VOB)这些能帮你从“茶树 含有 茶多酚”里抽出(茶树,含有,茶多酚)。triple_ie.py里一般会遍历依存弧,找 SBV 和 VOB 配对。
# triple_ie.py 依存句法抽三元组示意 from ltp import LTP ltp = LTP() def extract_triples(sentence): seg, hidden = ltp.seg([sentence]) pos = ltp.pos(hidden) ner = ltp.ner(hidden) dep = ltp.dep(hidden) words = seg[0] triples = [] # dep 格式: (head_index, relation, dependent_index) for head, rel, dep_idx in dep[0]: if rel == "SBV": # 找主语和谓语,再找谓语后面的 VOB subject = words[dep_idx - 1] predicate = words[head - 1] for h2, r2, d2 in dep[0]: if r2 == "VOB" and h2 == head: obj = words[d2 - 1] triples.append((subject, predicate, obj)) return triples if __name__ == "__main__": print(extract_triples("茶树含有茶多酚。"))这里dep返回的索引是从 1 开始的,取词的时候要减 1。SBV是主谓关系,VOB是动宾关系,不同版本的 LTP 关系标签可能略有差异,跑之前先打印一条dep结果确认标签名。抽出来的三元组会写到triple_results_*.txt,格式一般是“主语 谓语 宾语”用制表符或逗号分隔,后面 Neo4j 导入脚本按这个格式解析。
注意:依存句法抽三元组对长句和复杂修饰语很敏感。“茶树在温暖湿润的气候下含有丰富的茶多酚”这种句子,SBV 和 VOB 可能跨了多个词,简单配对会抽错。常见做法是加一条规则:谓语和宾语之间如果隔了“的”字结构,先跳过或人工复核。
4. 三元组落库 Neo4j:createKG_neo4j.py 的参数与导入方式
4.1 连接配置与节点关系建模
createKG_neo4j.py是最后一步。它读triple_results_*.txt或最新三元组.txt,用 py2neo 或 neo4j 官方驱动往图数据库里写。节点标签一般按实体类型分,比如Crop、Disease、Chemical,关系类型用谓语。如果谓语太杂,可以先做一轮归一化,把“含有”“包含”“富含”统一成CONTAINS。
# createKG_neo4j.py 写入示意 from py2neo import Graph, Node, Relationship graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) def load_triples(path): triples = [] with open(path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split("\t") if len(parts) == 3: triples.append(parts) return triples def write_to_neo4j(triples): for subj, pred, obj in triples: s_node = Node("Entity", name=subj) o_node = Node("Entity", name=obj) graph.merge(s_node, "Entity", "name") graph.merge(o_node, "Entity", "name") rel = Relationship(s_node, pred.upper(), o_node) graph.merge(rel) if __name__ == "__main__": data = load_triples("最新三元组.txt") write_to_neo4j(data)graph.merge比create安全,重复节点不会报错。关系类型用pred.upper()是为了统一大小写,Neo4j 关系类型对大小写敏感。密码别写死在代码里,用环境变量或配置文件。如果三元组文件是逗号分隔,把split("\t")改成split(",")。
4.2 可视化查询与验证
写完之后在 Neo4j Browser 里跑一句MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 50,能看到图就说明链路通了。如果节点全是Entity标签,说明分类没做细,可以按实体识别结果给节点打不同标签。查询“茶树”相关的两跳关系:MATCH (n {name:"茶树"})-[r]->(m)-[r2]->(m2) RETURN n, r, m, r2, m2,能快速验证图谱有没有断链。
| 文件 | 作用 | 常见问题 |
|---|---|---|
| getData_from_baike.py | 爬取百科词条 | 选择器失效、编码乱码 |
| myLTP.py | 分句、分词、NER | 模型加载慢、词典未生效 |
| triple_ie.py | 依存句法抽三元组 | 关系标签不匹配、索引越界 |
| createKG_neo4j.py | 写入 Neo4j | 密码错误、关系类型大小写 |
5. 避坑与排查:跑这套源码最容易翻车的五个地方
5.1 现象:LTP 模型加载报错或卡住
原因:LTP 默认下载大模型,网络不稳或显存不足时会卡在加载阶段。 解决:换LTP("LTP/small")或 tiny 模型,或者提前把模型文件下到本地,用LTP(path="本地路径")加载。跑之前先单独执行python -c "from ltp import LTP; ltp=LTP()"确认模型能起来。
5.2 现象:三元组抽出来全是空或只有主语
原因:依存关系标签和代码里写的不一致,比如某些版本用nsubj而不是SBV。 解决:在triple_ie.py里加一行print(dep[0]),看实际返回的关系标签,再改判断条件。别照搬网上教程的标签名,版本差异很常见。
5.3 现象:Neo4j 写入报“关系类型不能为空”
原因:三元组文件里有空行或分隔符不对,split后长度不是 3。 解决:在load_triples里加if len(parts) != 3: continue,同时检查文件是不是用制表符分隔。用cat -A 最新三元组.txt | head能看到实际分隔符。
5.4 现象:分词把领域词切碎
原因:jiebadic.txt没加载或路径不对。 解决:确认jieba.load_userdict的路径是相对当前工作目录的,建议用绝对路径。加载后跑一句print(list(jieba.cut("杂交水稻"))),看是不是一个词。
5.5 现象:爬取的数据里混入无关词条
原因:关键词列表里有关联度低的词,或者百科页面跳转到了消歧义页。 解决:在getData_from_baike.py里加一层判断,如果页面标题和关键词不一致就跳过。关键词列表人工过一遍,别直接拿大词表跑。
6. 进阶技巧:把三元组质量再提一档的验证方法
跑通之后,真正决定图谱好不好用的是三元组质量。我一般会做两件事:一是抽样人工核对,从triple_results_*.txt里随机抽 50 条,看主语、谓语、宾语是不是合理;二是用 Neo4j 查询做一致性检查,比如MATCH (n)-[r]->(m) WHERE n.name = m.name RETURN n, r, m,把自环关系揪出来,这些多半是抽错了。
另一个技巧是给关系加权重。同一对实体在多个句子里出现同一种关系,可以在写入时累加一个count属性,查询时按count排序,高频关系优先展示。代码上就是在write_to_neo4j里先查有没有现成关系,有就更新属性,没有就新建。
# 关系权重累加示意 def write_with_weight(triples): for subj, pred, obj in triples: s = Node("Entity", name=subj) o = Node("Entity", name=obj) graph.merge(s, "Entity", "name") graph.merge(o, "Entity", "name") rel = Relationship(s, pred.upper(), o) existing = graph.match_one(nodes=(s, o), r_type=pred.upper()) if existing: existing["count"] = existing.get("count", 1) + 1 graph.push(existing) else: rel["count"] = 1 graph.create(rel)graph.match_one按节点和关系类型查,查到就更新count,查不到就新建。这样图谱里关系粗细可以按权重渲染,一眼看出哪些关系最密集。从那以后我每次跑完三元组,都会先抽 50 条人工过一遍,再跑自环检查,最后才写库。希望帮到你。
本文还有配套的精品资源,点击获取