
简介农业领域知识图谱构建与Neo4j可视化项目面向计算机相关专业学生及毕业设计、课程设计需求整合百科数据爬取、命名实体识别、关系抽取、图谱存储与可视化全流程。压缩包共46个文件包括Python脚本8个、文本数据23个、CSV结构化文件7个、XML配置3个等大小21.42MB覆盖数据处理、三元组抽取、图谱生成与Neo4j建库等环节。资源内提供茶叶、作物、中草药、植物等多个农业子领域的前后端数据与中间结果如命名实体识别结果、三元组文件、清洗后的结构化数据并附停用词表、领域词典等辅助资源便于对照学习整体技术链路。已有58人学习下载适合从数据获取到图谱可视化完整跟练也可基于脚本和文档快速改造适配其他领域遇到环境配置或运行问题可进一步交流支持。1. 从百度百科到 Neo4j农业领域知识图谱的真实拆解如果你正被“知识图谱到底怎么构建”这件事卡住这套源码能给你一个完整的参考答案。它不走前沿深度模型路线而是把爬虫、实体识别、关系抽取、图数据库导入这一整条链路老老实实做透先从百度百科抓取农业词条用哈工大 LTP 做分词和依存句法再按规则抽三元组最后清洗成结构化数据导入 Neo4j 做可视化。我拆完后最大的感受是它把每个环节的中间产物都保留下来了——my_tmp.txt、my_datas.csv、triple_results_*.txt每一步都能看到数据长什么样出了问题也容易定位。这套资料适合人工智能、计算机相关专业的学生做毕设或课设也适合刚接触知识图谱的工程师当作全流程脚手架照着改一改就能换到任意垂直领域。它不是黑匣子是一套能让你跟着动手跑起来的实战代码。2. 数据爬取与实体解析getData_from_baike.py 到底抓了什么2.1 爬虫入口与数据落盘百度百科词条页面的抓取策略打开项目文件列表第一个有存在感的就是getData_from_baike.py。它负责根据lexicon.txt里的实体词条逐个拼接百度百科的 URL把页面 HTML 抓下来存到my_tmp.txt。整个爬虫没上 scrapy而是直接用 urllib 封装这对于一个课程设计级别的项目来说反而更友好不需要额外装框架只要有个 Python 环境就能跑。爬虫的逻辑核心在超时重试和编码处理上。百度百科的 URL 必须经过urllib.parse.quote编码否则遇到中文词条会直接 404。另外同一个词条可能既有中文名又有别名比如“玉米”和“苞米”会重定向到同一个页面但爬虫是按字面 URL 请求的所以词表里最好同时收录别名否则会漏抓大量描述文本。我实际跑的时候还加了一层缓存判断如果my_tmp.txt里已经有这个实体就跳过请求避免断网后从头再来。import time import urllib.parse from urllib.request import urlopen, Request HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml, } def fetch_baike(term, retry2): url https://baike.baidu.com/item/ urllib.parse.quote(term) for attempt in range(retry): try: req Request(url, headersHEADERS) resp urlopen(req, timeout10) return resp.read().decode(utf-8) except Exception as e: print(fretry {term} ({attempt 1}/{retry}): {e}) time.sleep(3) return None lexicon [line.strip() for line in open(lexicon.txt, encodingutf-8) if line.strip()] for term in lexicon: html fetch_baike(term) if html: with open(my_tmp.txt, a, encodingutf-8) as f: f.write(f{term}\t{html}\n) time.sleep(1.2)这段代码里time.sleep(1.2)是防封的底线如果你抓几千个词条建议把延时拉到 1.52 秒。timeout设成 10 秒比较合适百度百科偶尔会卡 5 秒太短容易误判超时。retry保持 2 次就够重试次数越多触发反爬的概率反而越高。这里输出的my_tmp.txt是“实体名 制表符 HTML”的原始物料后续所有清洗工作都从这里开始。2.2 data_parser.py 与 my_datas.csv从 HTML 到结构化表格的清洗路径抓下来的 HTML 有大量标签、脚本和导航文字直接拿去跑 LTP 只会得到一堆垃圾。data_parser.py的任务就是把my_tmp.txt里的 HTML 转成“实体名 描述文本”的干净语料输出到my_datas.csv。这里推荐的做法是先用 BeautifulSoup 定位lemma-summary或main-content节点再做标签剔除。不要对整个文档直接get_text()否则页面底部“这是一个消歧义页”的噪音也会混进来。农业词条的正文一般都在 200 字以上清洗完如果发现某行文本太少大概率是爬到歧义页了直接丢掉就行。import re from bs4 import BeautifulSoup def parse_baike_html(html): soup BeautifulSoup(html, html.parser) summary soup.find(div, class_lemma-summary) content soup.find(div, class_main-content) target summary or content if target is None: return for tag in target.find_all([script, style, sup]): tag.decompose() text target.get_text(separator\n) text re.sub(r\n{2,}, \n, text) return text.strip() with open(my_datas.csv, w, encodingutf-8) as out: for line in open(my_tmp.txt, encodingutf-8): parts line.split(\t, 1) if len(parts) ! 2: continue name, html parts[0], parts[1] text parse_baike_html(html) if len(text) 50: out.write(f{name}\t{text}\n)注意输出用的是\t而不是逗号因为百科描述里逗号、顿号太常见用逗号会把列数打乱。文件名是.csv实际是 TSV后面用pandas.read_csv(sep\t)读即可。还要强调一点文件编码必须统一为 utf-8 且不带 BOM。如果带 BOM第一行第一个字段会莫名多一个\ufeff导入 Neo4j 后实体名首字符不可见排查起来非常痛苦。2.3 词典与实体列表lexicon.txt、crops、plants 的分层设计项目里词表文件很多先是lexicon.txt作为通用入口再有crops lexicon.txt、tea lexicon.txt、Plants这样的分领域词表。它们不是随便放的而是为了适配不同的图谱场景。文件内容取向推荐用途lexicon.txt最基础的农业词表首次跑通全流程时使用crops lexicon.txt农作物名称集合构建农作物知识图谱的主词表tea lexicon.txt茶相关实体做茶叶垂直图谱时替换入口Plants植物学名称、俗名扩充实体识别的召回率我实际测试时先用crops lexicon.txt跑了小规模语料效果很直接水稻、玉米、小麦之间能抽出“属于”“种植于”“防治”等关系。如果你想看不同词表之间的差异对比triple_results_crops.txt和triple_results_tea.txt就会发现作物语料偏种植技术茶叶语料偏描述性评价同样的抽取规则触发率完全不一样。这里有一个实用技巧词表里一定要放别名。比如“玉米”同时收录“苞米”“棒子”“水稻”同时收录“稻谷”“大米”。百度百科对别名会重定向到标准词条但爬虫是按词请求的不收录别名就会漏掉大量信息。我给自己的项目补词表时会把标准名、别名、英文名全放进去反正后续还有去重和过滤。3. 三元组抽取triple_ie.py 如何用 LTP 和规则把句子切成知识3.1 分词、词性标注与依存句法LTP 的调用方式实体描述文本拿到手之后还不能直接抽关系。先要对句子做分词、词性标注、依存句法分析这一步由myLTP.py封装。从项目里能看到ltp_stopwords.txt和triple_ie.cpython-36.pyc据此推断原环境是 Python 3.6用的很可能是哈工大 LTP 的 pyltp 版本。pyltp 需要单独的模型文件比如cws.model、pos.model、parser.model路径通常在某个ltp_data目录下你下载模型后要改一下加载路径。from pyltp import Segmentor, Postagger, Parser cws_model ltp_data/cws.model pos_model ltp_data/pos.model par_model ltp_data/parser.model segmentor Segmentor() segmentor.load(cws_model) postagger Postagger() postagger.load(pos_model) parser Parser() parser.load(par_model) words list(segmentor.segment(水稻是一种重要的粮食作物)) postags list(postagger.postag(words)) arcs parser.parse(words, postags) for word, postag, arc in zip(words, postags, arcs): print(word, postag, arc.relation, arc.head, arc.dep)这段代码输出的arc.relation是依存关系名arc.head是父节点索引arc.dep是当前词索引。要特别小心 LTP 的索引是 0-based 还是 1-basedparse返回的head如果等于 0通常表示该词是根节点而正常依赖关系里head可能指向一个不在当前列表里的节点。我踩过这个坑当时把所有arc.head - 1当索引去取词结果没判断根节点直接取到列表末尾的前一个词抽出来的三元组全是错位的。建议先打印一小段对照依存关系图核对一下。3.2 基于依存路径的规则主谓宾、动宾、定中关系的三元组抽取triple_ie.py的核心不是模型而是一套基于依存句法路径的抽取规则。最典型的组合是“SBV主谓关系 VOB动宾关系”如果句子中存在主语 --SBV-- 谓语且同一个谓语又存在谓语 --VOB-- 宾语那么就构成 (主语, 谓语, 宾语) 三元组。另外ATT定中关系也能抽比如“绿色农业”可以抽成 (“绿色”, “修饰”, “农业”)。常见的规则代码结构如下def get_triples(words, arcs): triples [] for i, arc in enumerate(arcs): if arc.relation SBV: subj_idx arc.head - 1 if 0 subj_idx len(words): subj words[subj_idx] pred words[i] for j, arc2 in enumerate(arcs): if arc2.head i 1 and arc2.relation VOB: obj words[j] triples.append((subj, pred, obj)) elif arc.relation ATT: modifier words[i] head words[arc.head - 1] if arc.head 0 else None if head: triples.append((modifier, 修饰, head)) return triples这里arc.head i 1的判断要特别留意如果一条弧的head指向谓语而这条弧的dep当前词恰好是宾语说明宾语被谓语支配。索引换算错了就会漏掉一半三元组。我一般在每个条件里打印出words和arcs的完整映射验证一条句子能抽出所有预期的关系后再批量跑。规则抽取的优点是解释性强缺点也很明显召回率完全看规则覆盖面。比如“A 是 B 的一种”这个句式里“是”是谓语A 是主语B 是宾语但依存关系可能不是 SBVVOB而是 HED 和 ATT 混合需要额外补一条“主系表”规则。项目里triple_results_tea.txt和triple_results_crops.txt的差异也体现在这里同样一套规则换一个领域能触发的关系数量会差出一倍。3.3 实体链接与指代消解为什么词典要反复调优抽取出来的三元组主宾语经常是代词或简称比如“它”“我国”“这种作物”。如果不做清洗导入 Neo4j 后你会看到一堆“它”节点图谱完全没法看。项目里的lexicon.txt在这里起到实体链接的作用只保留三元组两端在词典中出现的实体其他一律扔掉。def filter_by_lexicon(triples, lexicon): return [(s, p, o) for s, p, o in triples if s in lexicon and o in lexicon]这个过滤策略能把准确率拉得很高但召回率会牺牲一部分。调优的诀窍是扩充词典的别名比如lexicon.txt里加入“苞米”“棒子”那么“玉米”相关的三元组就能多留一些。同时不要把所有停用词都加进词典像“今天”“主要”这种词一旦进了词典过滤逻辑会把它们当作实体保留反而污染图谱。另外myLTP.py里应该还做了指代消解的预处理比如把“它”“该作物”替换成前文提到的实体名。如果没做你可以在清洗环节用正则做一次替换当句子开头出现“水稻”后续句子中的“它”就替换成“水稻”。这种朴素做法在农业语料里很管用因为百科词条往往按词条名展开描述指代比较单调。4. 数据清洗与词典调优停用词、jieba 自定义词典与 re_sampling4.1 三套停用词表百度停用词、中文停用词、川大停用词的合并策略项目里停用词表有五六份包括百度停用词表.txt、中文停用词表.txt、四川大学机器智能实验室停用词库.txt、ltp_stopwords.txt外加一个jiebadic.txt。为什么搞这么多因为单份停用词表覆盖不全百度表偏网络用语川大表偏学术文献LTP 自带的又偏通用。合并是件简单但容易错的事。def load_stopwords(files): words set() for f in files: with open(f, encodingutf-8-sig) as fp: for line in fp: w line.strip() if w and not w.startswith(#): words.add(w) return words上面用utf-8-sig而不是utf-8是为了自动去掉 BOM。很多网上下载的停用词表带 BOM直接以 utf-8 读会读到\ufeff字符导致第一条停用词永远匹配不上。这是非常隐蔽的坑。合并之后要人工扫一遍把领域关键词从停用词表里剔除。比如“种植”“产量”“农业”这类词如果留在停用词列表里后面做分词特征时就会被删除三元组里永远不会有“水稻 * 种植 * 中国”这样的句式。我一般是把停用词表分成两级一级是全局停用词二级是领域保护词表处理时先剔除保护词再应用停用词。4.2 自定义词典的坑jieba 加载 lexicon.txt 后新词仍然被切碎jiebadic.txt和lexicon.txt存在是为了给 jieba 注入领域词。最典型的问题加载了自定义词典却依然把“病虫害”切成“病虫”和“害”。原因通常是词典格式不对。jieba 自定义词典要求每行三个字段词语、词频、词性字段之间用空格分隔不能是制表符也不能缺字段。import jieba jieba.load_userdict(jiebadic.txt) # jiebadic.txt 内容示例 # 病虫害 100 n # 高效低毒农药 50 n # 水稻田 80 n words jieba.lcut(水稻田病虫害防治) print(words) # 输出[水稻田, 病虫害, 防治]如果还是切错优先怀疑 jieba 缓存。删除用户目录下的~/.cache/jieba再重新初始化。另一种情况是新词词频太低被 HMM 结果压过此时用jieba.add_word(病虫害, freq500)强制提升权重。我在调优时会把lexicon.txt直接转成用户词典但前提是每行只能有一个词而且词频不能乱给太夸张的值否则全句都会被贪婪切分。4.3 re_sampling.py 与数据平衡训练语料过少时的兜底手段re_sampling.py这个名字暗示它是一个重采样脚本。在知识图谱构建流程里它通常用来处理语料不均衡某个实体在三元组里出现 300 次另一个实体只出现 2 次后续统计或训练模型时容易被长尾干扰。重采样的常见做法有两种一种是上采样复制少数类一种是下采样随机丢弃多数类。import random def resample(triples, target_count10): counter {} for s, p, o in triples: counter[s] counter.get(s, 0) 1 counter[o] counter.get(o, 0) 1 result [] for s, p, o in triples: if counter.get(s, 0) target_count or counter.get(o, 0) target_count: result.append((s, p, o)) return result这个脚本不会改变已有三元组的内容只是筛选掉过于稀疏的实体让图谱的度分布更均匀。对于毕设答辩来说你有这个步骤可以讲通过重采样避免图谱中出现“超级节点”和“孤立节点”的两极分化。实际写代码时要注意上采样可能把重复三元组成倍复制导入 Neo4j 时会产生重复关系所以更推荐用阈值过滤而非复制。5. Neo4j 避坑清单安装、内存、IP 访问与导入排查5.1 版本匹配py2neo 连不上 Neo4j 的 401 和语法错误现象运行createKG_neo4j.py连接时报HTTP 401 Unauthorized或者AttributeError: Graph object has no attribute run。原因Neo4j 4.x 之后默认启用身份验证且 py2neo 在不同大版本间 API 变化很大。旧版 py2neo 默认不带用户名密码新版 py2neo 又把Graph.run()改成了Graph.cypher.execute()混乱得很。解决先看 Neo4j 版本。如果是 Neo4j 4.4 以下用 py2neo 2021.2.5并显式传authfrom py2neo import Graph graph Graph(bolt://localhost:7687, auth(neo4j, your_password))如果是 Neo4j 5.x别折腾 py2neo 了直接用官方驱动from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password)) with driver.session() as session: count session.run(MATCH (n) RETURN count(n)).single().value() print(count)你在这个项目里看到createKG_neo4j.py大概率是 py2neo 写法。我建议不管哪种都先跑一条RETURN 1验证连接再跑建节点语句。5.2 内存配置与导入空库LOAD CSV 崩溃和节点数为 0现象LOAD CSV导入几万行后进程崩溃日志里出现OutOfMemoryError或者导入不报错但MATCH (n) RETURN count(n)结果永远是 0。原因Neo4j 默认堆内存只有 512M关系数据上十万之后很容易撑爆。另一个原因是LOAD CSV文件没有放在import目录下或者 CSV 里没有id字段导致节点被当作重复创建最后全入了事务回滚。解决修改neo4j.conf把堆内存调到 1G 以上。常见配置如下server.memory.heap.initial_size1024 server.memory.heap.max_size2048 server.memory.pagecache.size2g调完后重启 Neo4j。再看导入代码确认load csv from file:///my_struct_datas.csv用对了路径。如果你的 CSV 在系统目录下Neo4j 无法直接访问必须通过neo4j-admin import或先把文件拷到import目录。还有CSV 路径里的反斜杠要处理Windows 下的绝对路径很容易写错。我导入后第一件事永远是执行MATCH (n) RETURN count(n)如果为 0立刻查USING PERIODIC COMMIT前面的语法和文件路径。5.3 远程访问白屏localhost 能开换台机器却连不上现象浏览器只能通过localhost:7474打开 Neo4j Browser换 IP 访问直接拒绝或者好不容易连上报Client sent an HTTP request to an HTTPS server。原因Neo4j 默认只绑定localhost不监听外部网卡。同时新版 Neo4j Browser 默认走 HTTPS7473 端口但你配置的可能是 HTTP 7474于是请求被挡回来。解决改neo4j.conf里的监听地址和协议server.bolt.listen_address0.0.0.0:7687 server.http.listen_address0.0.0.0:7474 server.https.enabledfalse改完重启访问http://服务器IP:7474注意一定是http而不是https。如果还是不通检查系统防火墙和安全组是否放行了 7474 和 7687 端口。这一点在云服务器上尤其常见。另外Neo4j Browser 的登录账号默认是neo4j首次密码会给一个随机值存在日志中如果忘了看data/dbms/auth文件或重置。5.4 中文乱码与实体名首字符消失现象导入后查看节点发现节点显示正常但实体名第一个字符不见或者出现一排\ufeff前缀。原因CSV 保存为 utf-8 with BOM导致第一列第一个字段带不可见字符。Neo4j 读取时不识别 BOM把它当普通字符但显示不出来。解决所有 CSV 统一用无 BOM 的 utf-8 保存。在 Python 里写文件时用encodingutf-8而不是utf-8-sig。如果已经导入了可以跑一条清洗MATCH (n:Entity) WHERE id(n) 0 SET n.name replace(n.name, \ufeff, )这种问题排查起来很坑因为数据看起来没问题唯独查姓名时对不上。我一般会在导入脚本里直接加一句open(xxx.csv, encodingutf-8, newline)确保读写入干净。5.5 关系方向反了导出时把起止点搞混现象查询“农药防治水稻”结果图里显示“水稻防治农药”关系箭头反了。原因CSV 里起始实体和结束实体列顺序不对导入代码里MERGE (a)-[r:REL]-(b)的 a、b 取反。解决导入前先打印前 5 行三元组人工核对三元组语义。农业领域关系通常是有方向的“防治”一定是农药指向病虫害和作物。在createKG_neo4j.py中找到建关系的语句确认start_node对应主语列end_node对应宾语列。另外导入时用CREATE还是MERGE也有讲究重复跑脚本时CREATE会产生大量重复关系必须用MERGE。6. 图谱质量抽检与扩展一个可复用的连通性检查技巧图谱导入不是终点导入后最先要做的不是写炫酷可视化而是做质量抽检。我习惯先跑两条 Cypher 语句一条看孤儿节点一条看度分布MATCH (n) WHERE NOT (n)--() RETURN n.name AS orphan LIMIT 20; MATCH ()-[r]-(n) RETURN n.name AS name, count(r) AS degree ORDER BY degree DESC LIMIT 20;第一条会把没有任何关系的孤立节点列出来这些节点通常是过滤词典不彻底导致的废实体比如“主要”“一种”这种被误当成主语的词。第二条看前 20 的高频节点能快速发现会不会出现“水稻”连接了 800 条边而其他节点只有寥寥几条的极端情况。如果发现个别超级节点说明词典里的通用词混进了实体需要回到第 4 章的过滤逻辑里删掉。抽检通过后再考虑扩展。一个实用的技巧是把triple_results_*.txt导出的三元组做一次逆文档频率统计找出整个图谱中出现频率低但结构完整的关系比如“水稻—轮作—大豆”。这类稀有关系反而是知识图谱最有价值的部分可以单独导出到 CSV用于可视化中的高亮展示。我的习惯是每完成一个领域图谱就自动生成一份rare_relations.csv配合 Neo4j Bloom 做白名单展示。如果你还需要把图谱导出给前端createKG_neo4j.py里通常会写一个导出 JSON 的接口返回{nodes: [...] links: [...]}格式。前端拿到这个 JSON用 echarts 的 graph 类型就能直接画关系图。这里有个细节图数据量到几千节点时一次性返回全量 JSON 会让浏览器卡死建议在导出时加LIMIT 500做分页或者按节点度数过滤。这套从爬虫到图谱的流程我最近拆了不下五遍。每次换一个新领域第一件事永远是重新整理词表和停用词表因为再强大的抽取规则也扛不住脏语料。从那以后我每次跑完导入脚本都会强制走一遍第 6 章开头的两条抽检查询确认没有孤立节点和超级节点后才算收工。别嫌这一步枯燥它能省下你后来在可视化阶段反复调整的半天时间。希望这些经验和坑对你有所帮助也祝你顺利跑出属于自己的知识图谱。本文还有配套的精品资源点击获取