
简介这是一套面向自然语言处理初学者与关系抽取实践者的Python工具源码基于HanLP完成实体识别、语义角色标注与依存句法分析最终输出关系三元组。工具支持多种抽取模式包括施事者—谓语—受事者三元组、主谓宾三元组、关键词、高频词、实体词、实体共现词以及实体与关键词关联词可满足文本结构化与知识图谱构建的基础需求。资源包共66个文件以35个py源码和16个md说明文档为主另含txt、png、json、yaml、js、css等配置与示例文件压缩包约1.76MB目录涵盖utils、tests、examples、docs等模块结构清晰便于按功能查阅。目前已有431人学习下载。读者可从中获取完整的实体识别与关系抽取实现代码、测试用例、示例脚本及配套文档适合用于课程设计、科研原型或工程二次开发快速理解三元组抽取流程与HanLP调用方式。1. 从一段合同文本里抠出三元组这套 Python 关系抽取工具到底解决什么问题手里拿到一份 3000 字的合同、一段医疗病历或者一批新闻稿老板要你「把里面的人、公司、金额、时间、地点之间的关系全部结构化出来」你第一反应可能是上大模型。但真到落地环节成本、延迟、私有化部署、结果可解释性这几座大山压下来很多团队最后还是回到传统 NLP 流水线先做命名实体识别NER再做关系分类最后拼成(头实体, 关系, 尾实体)这样的三元组。这套 Python 实现的文本关系抽取工具走的就是这条路线实体识别这一层交给 HanLP关系判定这一层自己写规则或接分类模型输出统一的三元组结构。它适合谁适合需要把非结构化中文文本转成知识图谱边、做合同要素抽取、做舆情主体关系梳理的工程师也适合刚学完 python 基础语法、想找一个能跑通全流程的 NLP 小项目练手的人。不适合指望开箱即用、零配置就能达到 GPT 级别抽取精度的场景——传统流水线的天花板就在那里但它的可控性和可调试性恰恰是很多生产环境更看重的。下面我把这套方案从环境搭建、实体识别、关系判定到三元组组装按我自己踩过的顺序讲一遍。2. 环境与 HanLP 落地从 python 安装到第一次跑出实体2.1 为什么实体识别这一层选 HanLP 而不是自己训 BERT关系抽取的上游是实体识别实体识别的质量直接决定三元组能不能拼对。选 HanLP 的理由很实际它开箱就带中文预训练模型hanlp这个 pip 包封装了分词、词性标注、命名实体识别、依存句法一整条流水线调用接口统一不需要你先去 huggingface 下载权重再写 tokenizer。对于「文本关系抽取」这种以工程落地为目标的场景HanLP 的性价比很高——你花在环境上的时间少花在关系逻辑上的时间就多。当然它也有边界。HanLP 的默认 NER 模型覆盖的是通用领域实体类型人名、地名、机构名等如果你做的是医疗、法律、金融这种垂直领域通用模型对专有实体召回会明显下降。这时候常见做法是用 HanLP 做基础分词和词性标注实体识别换成自己微调的模型或者用 HanLP 的自定义词典功能把领域词先兜住。我一般会先用默认模型跑一遍看效果再决定要不要换。2.2 从零把环境跑起来python 安装与依赖配置不管你用的是 python 官网下载的安装包还是 ubuntu 下 apt 装的第一步都是确认版本。HanLP 对 python 版本有要求太老的 3.6 以下会出问题建议 3.8 及以上。装完 python 之后vscode python 环境配置或者 pycharm 配置 python 环境都行选一个顺手的。下面这套命令是我在 linux 和 windows 上都验证过的# 建议先建虚拟环境避免和系统里的包打架 python -m venv hanlp_env # linux / mac 激活 source hanlp_env/bin/activate # windows 激活 # hanlp_env\Scripts\activate # 安装核心依赖hanlp 会自动拉取 torch 等 pip install hanlp # 如果网络慢可以指定国内镜像 pip install hanlp -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后验证一下HanLP 第一次调用会自动下载预训练模型这一步会占几百 MB 磁盘网络不好容易卡住import hanlp # 加载多任务模型包含分词、词性、NER # 第一次运行会自动下载耐心等 tagger hanlp.load(hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH) text 张三于2023年5月加入了北京字节跳动科技有限公司担任算法工程师。 result tagger(text) # 打印命名实体识别结果 print(result[ner/ontonotes])这段代码的逻辑是hanlp.load加载一个多任务预训练模型tagger(text)一次性返回分词、词性、NER、依存等多个结果我们只取ner/ontonotes这一项。参数说明CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH是模型标识SMALL表示轻量版速度快但精度略低如果对精度要求高可以换成LARGE版本代价是显存和推理时间上升。跑通这一步你就拿到了实体列表这是三元组的原料。2.3 实体识别结果长什么样怎么读上面那段代码的输出大致是这样[(张三, PERSON), (2023年5月, DATE), (北京字节跳动科技有限公司, ORG), (算法工程师, TITLE)]每个元素是(实体文本, 实体类型)。注意 HanLP 的实体类型标签是英文的PERSON是人名ORG是机构DATE是日期TITLE是职位。这些标签就是你后面写关系规则时的锚点。比如「加入」这个动作通常连接的是PERSON和ORG「担任」连接的是PERSON和TITLE。把实体类型和触发词对上关系抽取的骨架就出来了。这里有个容易翻车的点HanLP 返回的实体位置信息默认不在这个简化输出里如果你需要知道实体在原文中的起止位置做关系触发词定位时会用到得用result[ner/ontonotes]配合result[tok/fine]自己对齐或者直接调底层 API 拿 offset。我一开始没注意这个写关系规则时全靠字符串匹配遇到重复实体就抓瞎。3. 关系判定与三元组组装规则、模型两条路怎么选3.1 关系抽取的两种主流做法触发词规则 vs 关系分类模型实体识别出来之后下一步是判断两个实体之间到底有没有关系、是什么关系。这里分两条路。第一条是触发词规则法。核心思想是句子里的某些动词或关键词是关系的「信号」。比如「就职于」「加入」「担任」指向任职关系「收购」「并购」指向资本关系「出生于」指向籍贯关系。做法是先在句子里找到触发词再看触发词左右最近的实体按预设的模板拼成三元组。这条路实现简单、可解释性强、调试直观缺点是覆盖不了触发词不明显的句子召回率有限。第二条是关系分类模型法。把「实体1 句子 实体2」拼成一个分类样本训练一个模型输出关系类别。这条路召回高但需要标注数据训练和部署成本都上去了。对于大多数刚起步的项目我的建议是先用规则跑通闭环把三元组结构、存储、下游消费都打通等规则覆盖率不够了再上模型。下面重点讲规则法因为它才是这套工具能快速落地的关键。3.2 用触发词规则把实体拼成三元组先定义关系模板。每个模板包含关系名、触发词列表、头实体类型、尾实体类型。然后遍历句子找到触发词再在触发词附近找符合类型的实体。# 关系模板定义 # 每个关系关系名 - (触发词列表, 头实体类型, 尾实体类型) RELATION_PATTERNS { 任职于: ([加入, 就职于, 任职于, 入职], PERSON, ORG), 担任: ([担任, 出任, 就任], PERSON, TITLE), 成立于: ([成立于, 创立于, 创办于], ORG, DATE), 收购: ([收购, 并购, 兼并], ORG, ORG), } def extract_triples(text, entities, tokens): text: 原始句子 entities: [(实体文本, 类型), ...] tokens: 分词结果用于定位触发词位置 triples [] for rel_name, (triggers, head_type, tail_type) in RELATION_PATTERNS.items(): for trigger in triggers: if trigger not in text: continue # 找到触发词在句中的位置 trigger_pos text.find(trigger) # 在触发词左侧找头实体右侧找尾实体 head None tail None for ent_text, ent_type in entities: ent_pos text.find(ent_text) if ent_pos trigger_pos and ent_type head_type and head is None: head ent_text if ent_pos trigger_pos and ent_type tail_type and tail is None: tail ent_text if head and tail: triples.append((head, rel_name, tail)) return triples # 测试 text 张三于2023年5月加入了北京字节跳动科技有限公司担任算法工程师。 entities [(张三, PERSON), (2023年5月, DATE), (北京字节跳动科技有限公司, ORG), (算法工程师, TITLE)] tokens [张三, 于, 2023年5月, 加入, 了, 北京字节跳动科技有限公司, , 担任, 算法工程师, 。] print(extract_triples(text, entities, tokens)) # 输出: [(张三, 任职于, 北京字节跳动科技有限公司), (张三, 担任, 算法工程师)]逻辑说明外层遍历每个关系模板内层遍历触发词。找到触发词位置后在它左边找类型匹配的头实体右边找类型匹配的尾实体都找到就产出一个三元组。参数说明RELATION_PATTERNS是核心配置你可以按业务往里面加关系head_type和tail_type决定了实体类型约束防止把「日期」错配成「机构」。这段代码故意写得朴素是为了让你看清逻辑实际用的时候要处理一个实体被多个关系复用、触发词重叠、否定句「没有加入」这些情况。3.3 三元组去重、过滤与结构化输出规则法跑出来的三元组会有重复和噪声。比如同一句话里「加入」和「入职」都命中会产出两条一样的。再比如「张三没有加入字节」这种否定句规则法会误抽。所以组装完必须过一遍清洗。def clean_triples(triples): # 去重 triples list(set(triples)) # 过滤头尾实体相同的自环 triples [t for t in triples if t[0] ! t[2]] # 过滤空值 triples [t for t in triples if all(t)] return triples # 否定句简单处理触发词前 3 个字内有否定词就丢弃 NEG_WORDS [不, 没, 未, 无, 非] def filter_negation(text, triples): result [] for h, r, t in triples: # 找到关系对应触发词的位置做判断这里简化处理 neg False for w in NEG_WORDS: if w in text: neg True break if not neg: result.append((h, r, t)) return result去重和自环过滤是基本操作否定句处理是规则法的老大难上面这段只是最粗的版本实际项目里我一般会结合依存句法看触发词和否定词是不是在同一个依存子树里这样准确率高很多。输出格式上三元组可以直接存成 JSON Lines一行一条方便下游灌进图数据库或者做数据分析import json triples [(张三, 任职于, 北京字节跳动科技有限公司), (张三, 担任, 算法工程师)] with open(triples.jsonl, w, encodingutf-8) as f: for h, r, t in triples: f.write(json.dumps({head: h, relation: r, tail: t}, ensure_asciiFalse) \n)这种一行一个 JSON 的格式比一次性 dump 一个大 JSON 数组更友好追加写入方便读取时也能流式处理文本量大时不会爆内存。4. 避坑与排查关系抽取流水线里最容易翻车的 5 个地方4.1 实体边界切错导致三元组头尾对不上现象抽出来的三元组里头实体是「北京字节跳动」尾实体是「科技有限公司」明显被切断了。原因HanLP 默认模型对长机构名的边界识别不稳定尤其是带地名前缀的公司名。解决给 HanLP 加自定义词典把业务里高频的机构全称、简称都塞进去或者在后处理阶段用规则把相邻的同类实体合并。我一般会维护一个领域词典文件启动时加载。4.2 触发词误命中把无关句子抽成关系现象「他加入了讨论」被抽成「他 任职于 讨论」。原因触发词「加入」太泛不区分宾语类型。解决在规则里加实体类型约束只是第一层还要加宾语语义约束比如尾实体必须是 ORG 类型且不能是动词短语。更稳的做法是结合依存句法要求触发词和尾实体之间存在动宾关系。4.3 一个实体被多个关系重复消费现象同一个「张三」在「加入字节并担任工程师」里被「加入」和「担任」两个模板各抽一次如果模板设计不当可能产出「张三 任职于 工程师」这种错误三元组。原因头实体查找时没有做关系隔离多个模板共享了实体列表。解决每个模板独立查找且尾实体类型严格匹配TITLE 类型不能被 ORG 模板消费。4.4 长文本里实体位置漂移现象处理超过 500 字的段落时三元组头尾实体张冠李戴。原因HanLP 对超长文本会截断或分块实体 offset 和原文对不上。解决先按句号、分号切句逐句做实体识别和关系抽取再在句子级别合并结果。切句这一步别偷懒它是长文本抽取稳定的前提。4.5 模型首次加载慢被误判为卡死现象第一次运行hanlp.load时程序几分钟没反应以为挂了。原因HanLP 在后台下载预训练模型没有进度条。解决提前手动下载模型放到缓存目录或者用hanlp.pretrained里的下载函数单独拉。生产环境一定要把模型文件打进镜像别让线上服务第一次请求时去下载。5. 进阶技巧把规则法和模型法串起来让三元组召回再上一个台阶规则法跑通之后你会发现有些句子明明有关系但就是没有触发词比如「张三字节跳动算法工程师」这种同位语结构规则法完全抓不到。这时候可以引入一个轻量的关系分类模型做补充。我的做法是规则法先跑一遍产出高置信度三元组剩下的句子用「实体对 句子」构造样本喂给一个基于 HanLP 或 transformers 微调的分类模型输出关系类别。两路结果合并去重召回率能明显提升。验证这套流水线好不好别只看准确率。我一般会准备一个小规模的人工标注测试集至少 200 条句子覆盖各种句式和否定情况然后算三个指标精确率抽出来的三元组有多少是对的、召回率人工标注的三元组有多少被抽出来了、以及三元组级别的 F1。规则法的精确率通常高但召回低加了模型之后召回上去精确率会掉一点这个权衡要看业务能不能接受。还有一个实用技巧把关系模板配置化。别把RELATION_PATTERNS写死在代码里抽成 YAML 或 JSON 文件业务方改关系不用动代码重启服务就生效。我吃过这个亏早期模板写死在 py 文件里运营要加一个「投资」关系还得找我改代码发版后来全部外置省了大量沟通成本。最后说个我自己的习惯每加一条新规则先拿历史数据回跑一遍看新规则有没有把老结果带偏。规则之间会互相干扰尤其是触发词有重叠的时候。这个回跑机制帮我拦下过好几次线上事故。希望帮到你。本文还有配套的精品资源点击获取