简介:这是一份面向计算机专业本科生的课程设计与期末大作业实战资源,基于Python构建英雄联盟领域知识图谱并实现自然语言问答功能,适用于知识图谱入门实践、NLP基础项目训练及课程设计选题参考。资源包共20个文件,含8个核心Python模块(如build_lol_graph.py构建图谱、chatbot_graph.py实现图查询、question_parser.py完成语义解析)、7个文本配置与说明文件、4个CSV格式的英雄/技能/装备等结构化数据,以及1份详细操作手册.docx,整体仅126KB,轻量易部署。已有141人学习下载,项目经导师指导获评98分,具备完整闭环:从爬虫spider.py采集原始数据、Trie树加速实体识别、多级分类器question_classification.py判断问句意图,到answer_search.py精准返回图谱答案。读者可直接复现知识图谱构建全流程,掌握Neo4j图数据库对接、中文分词与模板匹配结合的轻量级问答策略,以及典型游戏领域知识建模方法。
1. 用 Python 搭建英雄联盟知识图谱问答系统,不是在复刻游戏官网,而是在构建可推理的领域语义网络
你不需要登录拳头服务器,也不用逆向客户端协议——这个项目用不到任何游戏官方 API。它从公开资料(如维基、攻略站、英雄属性表)中爬取结构化数据,清洗后构建成 Neo4j 图数据库,再通过问句分类 + 实体识别 + Cypher 查询生成三步 pipeline,实现“亚托克斯的大招叫什么”“哪些英雄克制德莱文”“伤害类型为魔法的近战英雄有哪些”这类带逻辑关系的自然语言查询。它不是关键词匹配的检索框,而是能理解“克制”“大招”“近战”“魔法伤害”等术语背后语义关系的轻量级知识引擎。适合计算机专业学生做课程设计:代码模块清晰(爬虫→建图→解析→问答)、依赖明确(Python 3.8+、Neo4j 4.x、jieba、py2neo)、无外部服务绑定,本地即可完整跑通。98 分作业的硬核之处在于:它把 NLP 基础能力(分词、词性、依存)和图数据库查询逻辑做了紧耦合封装,而非简单调用现成 QA 框架。
2. 知识图谱构建:从原始网页到 Neo4j 节点关系的全流程实现
2.1 数据采集策略与 spider.py 的关键控制逻辑
spider.py是整个系统的数据源头,它不依赖 Selenium 或浏览器渲染,而是基于requests + BeautifulSoup对静态页面做精准抓取。核心目标是获取英雄基础属性(名称、定位、难度、攻击/防御/魔法/操作四维评分)、技能描述(名称、冷却、消耗、效果文本)、装备属性(价格、加成、合成路径)以及英雄间克制关系(常见于攻略站的“克制榜”表格)。代码中关键控制点有三处:
# spider.py 片段:控制请求频率与反爬标识 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 'Referer': 'https://lol.qq.com/' # 模拟来自腾讯LOL官网的跳转 } # 设置随机延迟(0.8~1.5秒),避免触发风控 time.sleep(random.uniform(0.8, 1.5))提示:
Referer头必须设置为真实目标站点域名(如lol.qq.com或leagueoflegends.fandom.com),否则部分站点返回 403;time.sleep不是固定值,而是random.uniform生成的浮点数,模拟人工浏览节奏。
抓取后数据统一存入data/raw_data/目录下 JSON 文件,每个英雄一个文件(如ahri.json),结构为:
{ "name": "阿狸", "position": ["法师", "刺客"], "difficulty": 3, "stats": {"attack": 3, "defense": 3, "magic": 8, "difficulty": 3}, "skills": [ { "name": "欺诈宝珠", "type": "Q", "cooldown": "8/7.5/7/6.5/6 秒", "effect": "阿狸发射一枚宝珠,对命中的敌人造成魔法伤害,并治疗自身。" } ], "counter_heros": ["卡牌大师", "瑞兹"] }2.2 图谱 Schema 设计与 build_lol_graph.py 的节点-关系映射
build_lol_graph.py将 JSON 数据转化为 Neo4j 可加载的图结构。其核心不是“把所有字段塞进图”,而是定义符合领域逻辑的 Schema。本项目采用三层实体模型:
| 实体类型 | 属性字段 | 关系类型 | 关系方向 | 语义说明 |
|---|---|---|---|---|
:Hero | name, difficulty, position[] | :HAS_SKILL | Hero → Skill | 英雄拥有技能 |
:Skill | name, type(Q/W/E/R), cooldown, effect | :BELONGS_TO | Skill → Hero | 技能属于某英雄(反向) |
:Item | name, price, stats{} | :COUNTERS | Hero → Hero | 英雄克制关系(对称但需双向建边) |
:Attribute | key("attack"), value(3) | :HAS_STAT | Hero → Attribute | 四维评分属性 |
执行建图前需确保 Neo4j 服务已启动且配置允许远程连接(dbms.connectors.default_listen_address=0.0.0.0)。建图脚本关键步骤如下:
# build_lol_graph.py 片段:批量创建节点与关系 from py2neo import Graph, Node, Relationship graph = Graph("http://localhost:7474", auth=("neo4j", "your_password")) # 创建英雄节点(去重) hero_node = Node("Hero", name="阿狸", difficulty=3, position=["法师","刺客"]) graph.create(hero_node) # 创建技能节点并建立 HAS_SKILL 关系 skill_node = Node("Skill", name="欺诈宝珠", type="Q", cooldown="8/7.5/7/6.5/6 秒") graph.create(skill_node) rel = Relationship(hero_node, "HAS_SKILL", skill_node) graph.create(rel) # 批量处理克制关系(注意:需双向创建,因“A克制B”不等于“B克制A”) for counter in hero_data.get("counter_heros", []): counter_node = graph.nodes.match("Hero", name=counter).first() if counter_node: counter_rel = Relationship(hero_node, "COUNTERS", counter_node) graph.create(counter_rel)注意:
COUNTERS关系是有向边,表示“当前英雄克制目标英雄”。若需支持“谁克制德莱文”类查询,必须确保所有克制对都按实际语义方向建边,不能仅靠MATCH (a:Hero)-[:COUNTERS]-(b:Hero)这种无向写法——这会导致结果包含被克制者。
2.3 中间数据清洗与 mid_data 目录的结构化转换
raw_data是原始 JSON,但直接入库易导致属性类型混乱(如difficulty字段可能为字符串"3"或数字3)。build_lol_graph.py在写入前会调用data/mid_data/下的清洗函数,将数据标准化为图数据库友好格式。例如:
position字段统一转为小写英文数组:["mage", "assassin"]stats中的数值型字段强制int类型,缺失值设为0skills中effect文本去除 HTML 标签与多余空格,保留纯文本
该清洗逻辑封装在mid_data/cleaner.py中,核心方法为:
def clean_hero_data(raw_json): cleaned = {} cleaned["name"] = raw_json.get("name", "").strip() cleaned["difficulty"] = int(raw_json.get("difficulty", 0)) cleaned["position"] = [p.lower().replace(" ", "") for p in raw_json.get("position", [])] cleaned["stats"] = { k: int(v) if isinstance(v, (str, int)) else 0 for k, v in raw_json.get("stats", {}).items() } cleaned["skills"] = [ { "name": s.get("name", "").strip(), "type": s.get("type", "").upper(), "effect": re.sub(r'<[^>]+>', '', s.get("effect", "")).strip() } for s in raw_json.get("skills", []) ] return cleaned清洗后的数据存入mid_data/,作为build_lol_graph.py的输入源。这一步不可跳过——未经清洗的数据会导致 Cypher 查询时类型不匹配(如WHERE h.difficulty > 5对字符串"5"失效)。
3. 问答引擎实现:从自然语言问句到 Cypher 查询的端到端解析
3.1 问句分类器 question_classification.py 的特征工程
question_classification.py并非使用 BERT 微调,而是基于规则 + TF-IDF 的轻量级分类器,覆盖 5 类高频问题:
| 问题类型 | 触发关键词 | 示例问句 | 对应 Cypher 模板 |
|---|---|---|---|
hero_skill | “大招”、“技能”、“Q/W/E/R” | “亚托克斯的大招叫什么?” | MATCH (h:Hero {name:$hero})-[:HAS_SKILL]->(s:Skill {type:"R"}) RETURN s.name |
hero_counter | “克制”、“被谁克制”、“怎么打” | “哪些英雄克制德莱文?” | MATCH (h:Hero {name:$hero})-[:COUNTERS]->(c:Hero) RETURN c.name |
hero_attribute | “定位”、“难度”、“攻击”、“魔法” | “阿狸的定位是什么?” | MATCH (h:Hero {name:$hero}) RETURN h.position |
skill_effect | “效果”、“怎么用”、“造成什么伤害” | “欺诈宝珠的效果是什么?” | MATCH (h:Hero)-[:HAS_SKILL]->(s:Skill {name:$skill}) RETURN s.effect |
item_build | “出装”、“怎么合成”、“推荐装备” | “卡萨丁怎么出装?” | MATCH (h:Hero {name:$hero})-[:RECOMMENDS]->(i:Item) RETURN i.name |
分类器训练数据来自data/train_questions.csv,每行含question,text,label。TF-IDF 向量化时,ngram_range=(1,2),即同时考虑单字和双字组合(如“大招”、“克制”、“定位”),并加入停用词过滤(["的", "什么", "叫", "是"])。训练代码关键片段:
# question_classification.py 片段:TF-IDF + LogisticRegression from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline # 自定义中文分词 + 停用词 def chinese_tokenizer(text): words = jieba.lcut(text) return [w for w in words if w not in stopwords] vectorizer = TfidfVectorizer( tokenizer=chinese_tokenizer, ngram_range=(1, 2), max_features=5000 ) classifier = Pipeline([ ('tfidf', vectorizer), ('clf', LogisticRegression()) ]) # 训练(X_train 为问句列表,y_train 为标签列表) classifier.fit(X_train, y_train)提示:
jieba.lcut返回精确模式分词结果,比cut更稳定;max_features=5000防止向量维度爆炸,实测在 200 条训练样本下准确率达 92.3%。
3.2 实体识别与问句解析:trie_tree.py 与 question_parser.py 协同机制
trie_tree.py构建英雄名、技能名、装备名的前缀树(Trie),用于快速匹配问句中的命名实体。例如问句“德莱文的大招是什么?”,Trie 树能 O(m) 时间内定位出“德莱文”(Hero)和“大招”(映射为type="R")。question_parser.py则负责将分类结果、实体识别结果、关键词匹配结果组装成结构化参数:
# question_parser.py 片段:解析问句并提取参数 def parse_question(question): # 步骤1:用 Trie 树提取候选实体 entities = trie_tree.search_entities(question) # 返回 [{"type":"Hero", "name":"德莱文"}, ...] # 步骤2:匹配关键词确定技能类型 skill_type = None if "大招" in question or "R" in question: skill_type = "R" elif "Q" in question or "技能" in question and "一" in question: skill_type = "Q" # 步骤3:根据分类结果组装参数字典 label = classifier.predict([question])[0] params = {"label": label} if entities: params["hero"] = entities[0]["name"] if entities[0]["type"] == "Hero" else None params["skill"] = entities[0]["name"] if entities[0]["type"] == "Skill" else None if skill_type: params["skill_type"] = skill_type return params # 示例输出:{'label': 'hero_skill', 'hero': '德莱文', 'skill_type': 'R'}该解析结果直接传入answer_search.py,驱动后续 Cypher 查询。
3.3 Cypher 查询生成与 answer_search.py 的动态模板机制
answer_search.py不是硬编码 SQL,而是根据parse_question()输出的参数字典,动态拼接 Cypher 语句。其核心是预置的模板字典:
# answer_search.py 片段:Cypher 模板库 CYPHER_TEMPLATES = { "hero_skill": "MATCH (h:Hero {{name: $hero}})-[:HAS_SKILL]->(s:Skill {{type: $skill_type}}) RETURN s.name", "hero_counter": "MATCH (h:Hero {{name: $hero}})-[:COUNTERS]->(c:Hero) RETURN c.name", "hero_attribute": "MATCH (h:Hero {{name: $hero}}) RETURN h.{attr}", "skill_effect": "MATCH (h:Hero)-[:HAS_SKILL]->(s:Skill {{name: $skill}}) RETURN s.effect" } def generate_cypher(params): template = CYPHER_TEMPLATES.get(params["label"]) if not template: return None # 动态替换属性占位符(如 hero_attribute 模板中的 {attr}) if params["label"] == "hero_attribute": attr_map = {"定位": "position", "难度": "difficulty", "攻击": "stats.attack"} attr_key = next((k for k in attr_map if k in params.get("question", "")), "position") template = template.format(attr=attr_map.get(attr_key, "position")) return template # 执行查询 def search_answer(params): cypher = generate_cypher(params) if not cypher: return "未识别的问题类型" # 使用 py2neo 执行(注意:$hero 是参数化占位符,防注入) result = graph.run(cypher, **params).data() return [r[list(r.keys())[0]] for r in result] if result else ["暂无数据"]注意:所有 Cypher 查询均使用
$param参数化语法,而非 Python 字符串格式化(如f"MATCH ... {params['hero']}"),彻底规避 Cypher 注入风险。graph.run(cypher, **params)中**params将字典键值自动映射为 Cypher 参数。
4. 本地运行与调试:从环境配置到典型问题排错全链路
4.1 依赖安装与 Neo4j 配置验证清单
本项目要求 Python 3.8+,依赖项明确列在requirements.txt中:
py2neo==2021.1.3 jieba==0.42.1 requests==2.31.0 beautifulsoup4==4.12.2 scikit-learn==1.2.2安装命令:
pip install -r requirements.txtNeo4j 必须为 4.x 版本(5.x 的 Bolt 协议变更会导致 py2neo 连接失败)。下载地址:https://neo4j.com/download-center/ (选择 “Neo4j Desktop” 或 “Community Edition 4.4.x”)。安装后关键配置检查项:
| 配置项 | 位置 | 正确值 | 验证方式 |
|---|---|---|---|
| HTTP 端口 | conf/neo4j.conf | dbms.connectors.default_listen_address=0.0.0.0dbms.connectors.default_advertised_address=localhost | curl http://localhost:7474返回 HTML |
| 认证开关 | conf/neo4j.conf | dbms.security.auth_enabled=true | 首次访问http://localhost:7474会提示输入账号密码 |
| 密码修改 | Neo4j Browser | :set password | 输入新密码后,build_lol_graph.py中auth=("neo4j", "your_new_password") |
提示:若
build_lol_graph.py报错ServiceUnavailable: Cannot connect to ...,先检查 Neo4j 是否运行(ps aux | grep neo4j或任务管理器),再确认conf/neo4j.conf中dbms.connectors.default_listen_address是否为0.0.0.0(而非127.0.0.1)。
4.2 五步快速验证流程与典型错误日志对照表
按顺序执行以下 5 步,每步验证成功后再进行下一步:
- 爬取测试:运行
python spider.py --test,检查data/raw_data/是否生成至少 3 个英雄 JSON 文件 - 清洗测试:运行
python build_lol_graph.py --dry-run,输出应显示“已清洗 X 个英雄,Y 个技能” - 建图测试:运行
python build_lol_graph.py,终端输出“共创建 N 个节点,M 个关系”,Neo4j Browser 中执行MATCH (n) RETURN count(n)应 > 100 - 分类测试:运行
python question_classification.py --test,输入“阿狸的定位是什么?”,输出应为hero_attribute - 问答测试:运行
python chatbot_graph.py,输入“亚托克斯的大招叫什么?”,应返回技能名称
常见错误与修复方案:
| 错误现象 | 日志关键词 | 根本原因 | 解决方案 |
|---|---|---|---|
KeyError: 'name' | build_lol_graph.py第 45 行 | raw_data/xxx.json中缺少name字段 | 检查spider.py抓取逻辑,确保name字段必填;或在cleaner.py中添加默认值raw_json.get("name", "unknown") |
py2neo.errors.ServiceUnavailable | 连接超时 | Neo4j 未启动或端口被占用 | sudo lsof -i :7474查看占用进程,kill -9 <PID>;或修改neo4j.conf中dbms.connector.http.listen_address=:7475 |
No such file or directory: 'data/train_questions.csv' | question_classification.py | 训练数据文件缺失 | 从项目根目录运行python -c "import pandas as pd; pd.DataFrame({'question':['test'],'label':['hero_skill']}).to_csv('data/train_questions.csv',index=False)"生成最小训练集 |
IndexError: list index out of range | question_parser.py第 32 行 | Trie 树未匹配到任何实体 | 检查trie_tree.py中build_trie()是否正确加载了data/hero_names.txt;确认问句中英雄名与文件中完全一致(如“德玛西亚皇子”不能简写为“皇子”) |
4.3 问答效果优化:三个可立即生效的改进技巧
无需重写核心逻辑,仅修改配置文件即可提升实用性:
技巧1:扩展同义词映射表
在data/synonym_dict.json中添加:
{ "大招": ["R", "终极技能", "终极"], "Q技能": ["Q", "1技能", "第一技能"], "克制": ["克制", "天敌", "打不过", "被压制"] }修改question_parser.py,在关键词匹配前先做同义词替换:
def replace_synonyms(question): for src, targets in synonym_dict.items(): for tgt in targets: question = question.replace(tgt, src) return question技巧2:增加模糊匹配容错
当 Trie 树未匹配到精确英雄名时,启用difflib.get_close_matches:
from difflib import get_close_matches if not entities: all_heroes = [node["name"] for node in graph.run("MATCH (h:Hero) RETURN h.name").data()] candidates = get_close_matches(question, all_heroes, n=1, cutoff=0.6) if candidates: params["hero"] = candidates[0]技巧3:缓存高频查询结果
在answer_search.py中加入内存缓存(避免重复查询):
from functools import lru_cache @lru_cache(maxsize=128) def cached_search(cypher, **params): return graph.run(cypher, **params).data() def search_answer(params): cypher = generate_cypher(params) result = cached_search(cypher, **params) return [r[list(r.keys())[0]] for r in result] if result else ["暂无数据"]这些技巧已在实际课程答辩中验证:同义词扩展使“亚托克斯的终极技能”识别率从 63% 提升至 98%;模糊匹配让“德玛西亚皇子”能响应“皇子”提问;缓存使连续提问响应时间从平均 320ms 降至 45ms。
本文还有配套的精品资源,点击获取