简介这份资源面向计算机、数学、电子信息等专业的学生与知识图谱初学者提供一套基于Neo4j的农业领域知识图谱构建完整源码可用于课程设计、期末大作业、毕设或项目立项演示。项目覆盖从百度百科爬取农业数据、数据分类到结构化数据生成三元组再到非结构化数据的分句、分词、命名实体识别与依存句法分析主谓关系等抽取三元组的全流程并最终在Neo4j中完成可视化。压缩包共46个文件约21.41MB以23个txt语料与结果文件、8个py核心脚本、7个csv数据表为主另含xml配置、md说明及停用词表等辅助资料目录按茶叶、农作物、植物等主题分模块组织。目前已有87人学习。读者可据此掌握jieba分词、LTP实体识别与依存句法分析、三元组抽取及Neo4j建库的完整链路并参考现成脚本与中间结果快速复现与二次开发。1. 农业知识图谱落地从百度百科爬到 Neo4j 可视化一条链路走通农业领域的数据有个特点散、杂、口语化。同一句“小麦适宜在土层深厚、排水良好的壤土中种植”在百科词条里是散文在种植手册里是表格在问答社区里又变成半截话。想把这类信息塞进 Neo4j 做可视化知识图谱靠人工整理三元组基本是体力活几百条之后就会翻车。我这次要讲的就是一条从百度百科爬取农业词条、做数据分类、结构化数据直接抽三元组、非结构化文本用 LTP 分句加依存句法分析抽主谓宾关系的完整链路最后落到 Neo4j 里做可视化。适合有 Python 基础、想搭一个垂直领域知识图谱 Demo 的开发者也适合做农业信息化、想验证图谱能不能落地的同学。整条链路不依赖大模型纯规则加句法分析跑得动、看得懂、改得动。2. 数据爬取与分类把百科词条变成能用的原料2.1 为什么选百度百科做农业图谱的冷启动语料农业领域的公开结构化数据本来就少农业百科类站点要么字段残缺要么更新滞后。百度百科的词条结构相对规整基本信息栏是半结构化的键值对正文段落是自然语言词条之间还有内链。对知识图谱冷启动来说这三样东西刚好对应三种抽取策略——基本信息栏直接映射三元组正文段落走句法分析内链用来做实体对齐的候选。我一般会把农业词条按大类先圈定作物、病虫害、土壤、农机、农药、农艺措施。每类选 30 到 50 个种子词条比如“小麦”“稻飞虱”“红壤”“联合收割机”再用百科的内链和分类页做一轮扩展。扩展深度控制在两层再深就会混进大量无关词条比如从“小麦”跳到“面粉”再跳到“面包”图谱就偏了。爬取本身不复杂关键是控制频率和解析结构。下面这段是抓词条正文和基本信息栏的最小实现用 requests 加 BeautifulSoup不引入重型框架。import requests from bs4 import BeautifulSoup import time import re HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_baike_entry(entry_name): 抓取单个百科词条返回基本信息栏和正文段落 url fhttps://baike.baidu.com/item/{entry_name} resp requests.get(url, headersHEADERS, timeout10) if resp.status_code ! 200: return None soup BeautifulSoup(resp.text, html.parser) # 基本信息栏通常是 dl/dt/dd 结构 basic_info {} for dl in soup.select(dl.basicInfo-block): dts dl.select(dt) dds dl.select(dd) for dt, dd in zip(dts, dds): key dt.get_text(stripTrue) val dd.get_text(stripTrue) if key and val: basic_info[key] val # 正文段落取 lemma-summary 和 para 类 paragraphs [] for p in soup.select(div.lemma-summary p, div.para p): text p.get_text(stripTrue) if len(text) 10: paragraphs.append(text) return {entry: entry_name, basic_info: basic_info, paragraphs: paragraphs} if __name__ __main__: seeds [小麦, 稻飞虱, 红壤, 联合收割机] for name in seeds: data fetch_baike_entry(name) if data: print(data[entry], len(data[paragraphs]), 段) time.sleep(2) # 控制频率避免触发反爬这段代码里有两个参数值得调。time.sleep(2)是保守值实测 1 到 3 秒之间比较稳太快会拿到空页面。len(text) 10这个阈值用来过滤导航文字和短标签农业词条正文段落一般都在 30 字以上设 10 是留余量。基本信息栏的解析依赖dl.basicInfo-block这个选择器百科改版时这里最容易失效跑之前先用一个词条验证选择器命中数量。2.2 数据分类按结构化和非结构化分流爬下来的数据不能一锅端。基本信息栏是键值对属于结构化数据可以直接映射成三元组正文段落是自然语言属于非结构化数据得先分句再做依存句法分析。分类这一步我一般用规则加关键词兜底不训练模型因为农业词条的字段名相对固定。分类逻辑可以写成一张映射表字段名命中“分布区域”“生长环境”“形态特征”这类归到属性类正文段落里出现“是”“属于”“又称”这类判断词归到关系类。下面是一个分类函数的骨架。# 字段名到图谱关系的映射农业领域常见字段 FIELD_RELATION_MAP { 分布区域: 分布区域, 生长环境: 生长环境, 形态特征: 形态特征, 别名: 别名, 科: 所属科, 属: 所属属, 门: 所属门, } def classify_entry(data): 把词条拆成结构化三元组和非结构化段落 triples [] for key, val in data[basic_info].items(): rel FIELD_RELATION_MAP.get(key) if rel: # 值里可能有多个实体用顿号或逗号切分 for v in re.split(r[、,], val): v v.strip() if v: triples.append((data[entry], rel, v)) return { entry: data[entry], triples: triples, raw_paragraphs: data[paragraphs] }FIELD_RELATION_MAP是这张表的核心字段名对不上就抽不出关系。农业词条的字段名在不同词条里会有变体比如“分布区域”有时写成“分布范围”我一般会在映射表里加同义词或者在匹配前先做一次字段名归一化。切分符用正则[、,]覆盖了中文顿号和两种逗号实测能处理大部分多值字段。3. 结构化数据生成三元组从键值对到 Neo4j 节点3.1 三元组清洗去重、去噪、统一实体名结构化数据抽出来的三元组不能直接入库得先过三关。第一关是去重同一词条同一关系同一尾实体只留一条。第二关是去噪值里混进的“暂无”“未知”“见正文”这类要过滤。第三关是实体名统一比如“小麦”和“小麦植物”要归一成同一个实体名。NOISE_VALUES {暂无, 未知, 见正文, 无, 不详} def clean_triples(triples): 去重、去噪、实体名归一 seen set() cleaned [] for head, rel, tail in triples: # 去噪 if tail in NOISE_VALUES or len(tail) 2: continue # 实体名归一去掉括号补充说明 head re.sub(r[(].*?[)], , head).strip() tail re.sub(r[(].*?[)], , tail).strip() key (head, rel, tail) if key in seen: continue seen.add(key) cleaned.append(key) return cleanedNOISE_VALUES这个集合是血泪经验百科词条里“分布区域”写“暂无”的情况不少不滤掉会在图谱里生成一堆无意义节点。实体名归一用正则去掉括号内容农业词条里“小麦禾本科植物”这种写法很常见不去括号会导致同一实体分裂成两个节点。3.2 用 py2neo 批量写入 Neo4j三元组清洗完写入 Neo4j 用 py2neo 比较顺手。核心是 MERGE 而不是 CREATEMERGE 能保证实体不重复创建。下面这段是批量写入的实现用事务包起来避免逐条提交。from py2neo import Graph, Node, Relationship def write_to_neo4j(triples, uribolt://localhost:7687, userneo4j, passwordyour_password): 批量写入三元组到 Neo4j graph Graph(uri, auth(user, password)) tx graph.begin() node_cache {} def get_node(name): if name not in node_cache: node Node(Entity, namename) tx.merge(node, Entity, name) node_cache[name] node return node_cache[name] for head, rel, tail in triples: h_node get_node(head) t_node get_node(tail) # 关系类型做一次清洗Neo4j 关系类型不能有特殊字符 rel_type re.sub(r[^\w\u4e00-\u9fa5], _, rel) tx.merge(Relationship(h_node, rel_type, t_node)) tx.commit() print(f写入 {len(triples)} 条三元组)node_cache是性能关键同一实体在多个三元组里出现时只创建一次节点对象。tx.merge配合Entity, name做唯一性约束前提是你在 Neo4j 里先建了约束CREATE CONSTRAINT FOR (e:Entity) REQUIRE e.name IS UNIQUE。关系类型清洗用正则把非字母数字和中文的字符替换成下划线Neo4j 关系类型不支持空格和括号不清洗会直接报错。4. 非结构化数据分句与依存句法分析LTP 抽主谓宾4.1 LTP 分句与分词把段落切成可分析的句子非结构化段落第一步是分句。农业文本里句号、分号、感叹号都可能是句子边界但“小麦亩产 500 公斤比去年增长 10%”这种逗号连接的长句如果按逗号切会切碎语义。我一般用 LTP 的split_sentence做分句它按标点加长度双重判断比纯正则稳。from ltp import LTP ltp LTP(LTP/small) # 小模型够用速度快 def split_and_segment(paragraphs): 分句 分词 词性标注 results [] for para in paragraphs: sentences ltp.split_sentence(para) for sent in sentences: if len(sent) 8: # 太短的句子跳过 continue seg, hidden ltp.seg([sent]) pos ltp.pos(hidden) results.append({ sentence: sent, words: seg[0], pos: pos[0] }) return resultsLTP(LTP/small)加载的是小模型农业文本的句法结构不算复杂小模型足够。len(sent) 8这个阈值用来过滤“参见正文”这类短句。ltp.seg返回分词结果ltp.pos返回词性标注这两个结果后面依存句法分析要用。4.2 依存句法分析抽主谓宾SBV、VOB、ATT 三类关系依存句法分析的核心是看词与词之间的依存关系标签。农业文本里最有价值的是三类SBV主谓关系、VOB动宾关系、ATT定中关系。比如“小麦适宜在土层深厚的壤土中种植”依存分析后“小麦”是 SBV“种植”是核心谓词“壤土”通过 ATT 修饰“种植”的宾语。抽三元组的逻辑是找到谓词往上找 SBV 主语往下找 VOB 宾语ATT 用来补充修饰关系。def extract_triples_from_dep(sentence, words, pos): 基于依存句法分析抽三元组 seg, hidden ltp.seg([words]) dep ltp.dep(hidden) # 依存关系 triples [] # dep 格式[(head_idx, dep_idx, rel_type), ...] for head, dep_idx, rel in dep[0]: if rel SBV: subject words[dep_idx] predicate words[head] # 找谓词的 VOB 宾语 for h2, d2, r2 in dep[0]: if h2 head and r2 VOB: obj words[d2] triples.append((subject, predicate, obj)) return triples这段代码里dep[0]是依存关系列表每个元素是(中心词索引, 依存词索引, 关系类型)。SBV 关系里依存词是主语中心词是谓语。找到谓语后再扫一遍找 VOB宾语就出来了。农业文本里“小麦适宜种植”这种句子抽出来就是(小麦, 适宜种植, None)宾语缺失时我一般会跳过不硬凑。实际跑的时候LTP 的依存分析对长句效果会下降超过 40 字的句子建议先切分再分析。另外农业文本里“亩产”“株高”这类领域词LTP 分词可能会切错我一般会加一个自定义词典把领域词先塞进去。5. 避坑与排查这条链路最容易翻车的五个地方5.1 百科页面结构改版导致选择器失效现象爬取脚本跑出来basic_info是空字典paragraphs也是空列表。原因百科前端改版dl.basicInfo-block和div.para这两个选择器不再命中。解决跑之前先用一个词条打印soup.select的命中数量命中为 0 就换选择器。我一般会同时保留两套选择器用or连接比如soup.select(dl.basicInfo-block) or soup.select(div.basic-info)改版时至少有一套能兜住。5.2 LTP 模型加载报错或分词结果异常现象LTP(LTP/small)报模型文件找不到或者分词结果里全是单字。原因模型没下载到本地缓存目录或者 LTP 版本和模型版本不匹配。解决先确认 LTP 版本再按官方文档下载对应模型。分词全是单字通常是模型加载失败后走了兜底逻辑检查缓存目录里模型文件是否完整。我一般会在代码里加一句ltp.init_dict加载自定义词典把“稻飞虱”“红壤”这类领域词加进去分词准确率会明显提升。5.3 三元组写入 Neo4j 时报关系类型非法现象tx.merge(Relationship(...))抛异常提示关系类型包含非法字符。原因百科字段名里带空格、括号或特殊符号直接当关系类型用了。解决写入前用正则清洗关系类型把非中文非字母数字的字符替换成下划线。另外 Neo4j 关系类型区分大小写清洗后最好统一转成小写或保持原样别混用。5.4 依存句法分析抽出的三元组大量重复现象同一句话里“小麦适宜种植”被抽了多次或者不同句子抽出相同三元组。原因依存分析对同一谓词可能匹配到多个 SBV 或 VOB或者段落里重复表述。解决抽完三元组后统一走一遍去重用set包一下。另外可以在抽取阶段加约束一个谓词只取第一个 SBV 和第一个 VOB避免过度抽取。5.5 图谱可视化时节点过多导致布局卡死现象Neo4j Browser 里跑MATCH (n) RETURN n直接卡住节点几千个以上时布局算不过来。原因全量返回节点Browser 的前端布局扛不住。解决可视化时加LIMIT或者按关系类型分批查。我一般会先查某一类关系比如MATCH (a)-[r:分布区域]-(b) RETURN a, r, b LIMIT 100看清楚局部结构再逐步扩展。另外给实体加标签分类比如:作物、:病虫害查询时按标签过滤比全量查快得多。6. 进阶技巧用规则加句法分析把抽取准确率再提一档走到这里链路已经能跑通了但抽取准确率还有提升空间。我自己的习惯是在依存句法分析外面再包一层规则校验把明显不对的三元组拦掉。具体做法是维护一个谓词白名单和黑名单农业文本里“是”“属于”“又称”“分布于”这类谓词抽出来的三元组可信度高直接放行“有”“进行”“使用”这类谓词抽出来的三元组歧义大进人工复核队列。另一个技巧是用词性标注做二次过滤。依存分析抽出的主语如果是代词“它”“其”直接丢弃因为指代消解没做留着就是噪声。宾语如果是数量词“500 公斤”“10%”也丢弃这类数值更适合放在属性里而不是关系里。验证抽取效果我一般用抽样人工核对从每个大类里抽 20 条三元组人工判断对错算一个粗略准确率。农业文本的依存分析准确率实测在 60% 到 75% 之间加上规则过滤能提到 80% 左右。想再往上走就得引入领域词典和指代消解那是另一个工程量级了。最后说一个我踩过的坑一开始我图省事把结构化三元组和非结构化抽的三元组混在一起写入结果同一实体在两类数据里名字不一致图谱里出现两个“小麦”节点。后来改成先统一实体名再写入结构化数据里的实体名做归一化非结构化抽出的实体名也走同一套归一化规则图谱才干净。这个习惯我一直保留着入库前先跑一遍实体名一致性检查不一致的先合并再写。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?