简介:这是一套基于Python与Vue.js构建的知识图谱驱动型智能旅游推荐系统完整代码工程,面向计算机专业本科生、毕业设计学生及知识图谱初学者,解决旅游场景下个性化景点、餐饮等多维度智能推荐问题。资源包共237个文件,涵盖22个核心Python后端模块(含Neo4j图数据库交互、推荐算法逻辑)、24个Vue前端组件(含交互式知识图谱可视化)、25个JS业务脚本、18个CSS样式文件及90张界面截图与流程图,整体压缩包大小为39.62MB,结构清晰、模块解耦明确。已有266人下载学习,项目经导师高度认可,获评98分高分实践成果。代码全程手写并附详尽中文注释,包含可直接运行的前后端分离部署方案、数据集示例(xls/csv)、知识图谱构建脚本(py)及推荐结果可视化页面(vue+interactive-graph.min.css),新手可快速上手调试与二次开发。
1. 这不是又一个“景点列表页”,而是一个能理解“川西小众但适合带老人自驾”需求的知识驱动推荐系统
你有没有试过在旅游平台搜“适合带父母的云南行程”,结果首页全是网红打卡点和年轻人徒步路线?这个基于 Python + Vue 的知识图谱旅游推荐系统,恰恰解决了这个断层——它不靠关键词匹配,而是把景点、交通、餐饮、气候、海拔、适老设施、历史背景等上百个维度建模成结构化三元组,让系统真正“理解”什么是“适合带老人”。后端用 Neo4j 存储图谱关系,Flask 提供语义查询 API;前端用 Vue 实现可交互的图谱可视化(注意:不是静态图,是支持点击节点展开关联、拖拽缩放、高亮路径的 real-time graph),并融合协同过滤与图神经网络(GNN)打分逻辑。项目已预置四川、云南、陕西三地 2000+ 实体及 8000+ 关系数据,部署后输入自然语言如“想吃地道川菜、避开人挤人、有无障碍通道”,系统会返回带推理路径的推荐结果(例如:推荐「钟水饺总店」→ 因其位于「春熙路地铁站」→ 该站有垂直电梯 → 且该店在知识图谱中标注了「适老餐位」「无台阶入口」)。适合课程设计、毕设答辩、或作为知识图谱工程落地的最小可行案例。
2. 知识图谱构建:从原始旅游数据到 Neo4j 可查询三元组的完整链路
2.1 为什么选 Neo4j 而非 MySQL 或 Elasticsearch?
旅游推荐的核心瓶颈不在存储容量,而在关系遍历效率与语义表达能力。MySQL 的 JOIN 操作在 5 层以上关联时性能断崖式下降;Elasticsearch 擅长全文检索,但无法原生表达“景点A→被某旅行社包含在→亲子游线路→该线路要求游客年龄≤12岁→而用户孩子今年10岁”这类多跳推理链。Neo4j 的 Cypher 查询语言天然支持MATCH (p:Person)-[:HAS_CHILD]->(c:Child)-[:AGE]->(a:Age {value:10})这类模式匹配,且单次查询毫秒级响应。本项目中,我们定义了 7 类核心节点(Place、Cuisine、Transport、Facility、Season、TourGroup、UserProfile)和 12 种关系(LOCATED_IN、SERVES_CUISINE、ACCESSIBLE_BY、HAS_FACILITY、BEST_IN_SEASON、INCLUDES_PLACE等),所有实体属性均标注source(来源:携程/马蜂窝/文旅局公开数据)和confidence(置信度,人工校验后赋值 0.7~0.95),为后续 GNN 微调留出可信度权重接口。
2.2 数据清洗与三元组抽取:Python 脚本实操
原始数据来自三类渠道:① 文旅局发布的《四川省无障碍旅游设施名录》(PDF 表格);② 携程评论中提取的“带老人体验”高频句式(如“电梯直达”“轮椅友好”“有休息区”);③ 马蜂窝游记中人工标注的“美食-景点”共现关系。清洗脚本data_pipeline/extract_triples.py核心逻辑如下:
# data_pipeline/extract_triples.py import re import pandas as pd from py2neo import Graph # 1. 解析 PDF 表格(使用 tabula-py) df = tabula.read_pdf("data/accessible_facilities.pdf", pages="all") # 清洗:统一“无障碍坡道”“无障碍通道”为标准标签 "RAMP_ACCESS" df["facility_type"] = df["facility_type"].str.replace(r"无障碍[坡道|通道|电梯]", "RAMP_ACCESS", regex=True) # 2. 从评论提取关系(正则 + 规则模板) def extract_from_review(text): patterns = [ (r"电梯直达", "HAS_FACILITY", "ELEVATOR"), (r"轮椅友好", "HAS_FACILITY", "WHEELCHAIR_ACCESS"), (r"有休息区", "HAS_FACILITY", "REST_AREA"), (r"离地铁站(\d+)米", "ACCESSIBLE_BY", "SUBWAY_{group1}m") # group1 为捕获的距离数字 ] triples = [] for pattern, rel, obj in patterns: if re.search(pattern, text): # 从上下文提取景点名(假设评论前10字含景点名) place = re.match(r"^.{0,10}", text).group().strip() triples.append((place, rel, obj)) return triples # 3. 写入 Neo4j(批量提交,避免逐条事务开销) graph = Graph("bolt://localhost:7687", auth=("neo4j", "password")) tx = graph.begin() for place, rel, obj in all_triples: tx.run( "MERGE (p:Place {name: $place}) " "MERGE (o:Facility {type: $obj}) " "CREATE (p)-[:HAS_FACILITY {source: 'travel_comment', confidence: 0.85}]->(o)", place=place, obj=obj ) graph.commit(tx)提示:
MERGE保证节点不重复创建;CREATE强制新建关系(因同一景点可能多次被不同用户标注“轮椅友好”,需保留多条关系以统计置信度);confidence值在后续 GNN 推理中作为边权重,直接影响推荐排序。
2.3 图谱验证:用 Cypher 快速定位数据质量问题
部署 Neo4j 后,必须验证三元组质量。以下 3 条 Cypher 是日常巡检必查项:
// 检查是否存在孤立节点(无任何关系的景点) MATCH (p:Place) WHERE NOT (p)--() RETURN p.name, count(*) AS isolated_count // 检查“适老设施”关系是否全部指向标准 Facility 节点(避免拼写错误如 "wheelchair" vs "wheel_chair") MATCH (p:Place)-[r:HAS_FACILITY]->(f) WHERE NOT f.type IN ['ELEVATOR','WHEELCHAIR_ACCESS','REST_AREA','RAMP_ACCESS'] RETURN p.name, f.type, r.source // 检查跨省关系合理性(如“西安兵马俑”被标记为“位于云南”,明显错误) MATCH (p:Place)-[:LOCATED_IN]->(prov:Province) WHERE p.name CONTAINS "兵马俑" AND prov.name = "云南" RETURN p.name, prov.name运行结果若返回非空,说明数据清洗规则需补充。例如第二条查出f.type="wheel_chair",则需在extract_triples.py中增加.replace("wheel_chair", "WHEELCHAIR_ACCESS")。
3. 后端服务:Flask API 设计与语义查询引擎实现
3.1 API 分层设计:从 HTTP 请求到图谱推理的四层映射
本系统 API 不是简单 CRUD,而是将用户自然语言请求映射为图谱查询路径。app.py定义了/api/recommend入口,其处理流程严格分为四层:
| 层级 | 职责 | 关键代码位置 | 示例输入输出 |
|---|---|---|---|
| 1. 意图解析层 | 识别用户核心诉求(如“适老”“美食”“避暑”) | nlu/intent_classifier.py | 输入:“带爸妈去云南,要凉快点的” → 输出:{"intent": "climate_preference", "target_climate": "cool", "user_profile": "elderly_accompany"} |
| 2. 实体链接层 | 将模糊词(如“凉快点”)绑定到图谱节点(Season {type: "summer_cool"}) | nlu/entity_linker.py | “凉快点” →MATCH (s:Season) WHERE s.description CONTAINS "夏季凉爽" |
| 3. 查询生成层 | 根据意图+实体,动态拼装 Cypher 模板 | graph/query_builder.py | 生成:MATCH (p:Place)-[:BEST_IN_SEASON]->(s:Season {type:"summer_cool"})-[:LOCATED_IN]->(prov:Province {name:"云南"}) RETURN p.name, p.rating |
| 4. 结果重排层 | 对原始图谱结果融合 GNN 打分(gnn/scorer.py)与协同过滤(cf/recommender.py) | service/recommender.py | 最终排序:[{"name":"大理古城","score":0.92,"reason":"GNN路径:古城→有电瓶车→电瓶车→适老→用户画像匹配"}] |
3.2 核心推荐 API 实现:支持多条件组合与解释性返回
/api/recommend的完整实现(含错误处理与日志):
# app.py from flask import Flask, request, jsonify from service.recommender import HybridRecommender from nlu.intent_classifier import classify_intent import logging app = Flask(__name__) recommender = HybridRecommender() # 初始化混合推荐器 @app.route('/api/recommend', methods=['POST']) def get_recommendation(): try: data = request.get_json() user_query = data.get('query', '').strip() if not user_query: return jsonify({"error": "query is required"}), 400 # 1. 意图识别(耗时<50ms,本地模型) intent_result = classify_intent(user_query) # 2. 执行混合推荐(图谱+GNN+CF) results = recommender.recommend( query=user_query, intent=intent_result['intent'], constraints=intent_result.get('constraints', {}), top_k=5 ) # 3. 构建可解释返回(关键!毕业答辩加分项) response = { "query": user_query, "recommendations": [ { "name": r['name'], "score": round(r['score'], 3), "explanation": r['explanation'], # 如:"因您关注'适老',该景点有无障碍电梯(置信度0.92)且距地铁站<100m" "details": { "rating": r.get('rating', 0), "address": r.get('address', ''), "image_url": r.get('image_url', '') } } for r in results ], "debug": { "intent": intent_result['intent'], "graph_query_time_ms": results[0].get('graph_time', 0) if results else 0, "gnn_score_weight": 0.6 # 当前权重配置 } } return jsonify(response) except Exception as e: logging.error(f"Recommendation failed: {str(e)}") return jsonify({"error": "Internal server error"}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) # 生产环境禁用 debug注意:
explanation字段不是硬编码,而是从 GNN 的注意力权重反向追踪路径生成。例如 GNN 对边HAS_FACILITY的注意力值最高,则解释为“因该设施匹配您的需求”。
3.3 GNN 打分模块:PyTorch Geometric 实现轻量级图卷积
gnn/scorer.py使用 PyTorch Geometric 实现 2 层 GCN,输入为子图(用户查询触发的 100 个节点内拓扑),输出节点嵌入用于相似度计算:
# gnn/scorer.py import torch from torch_geometric.nn import GCNConv from torch_geometric.data import Data class GCN(torch.nn.Module): def __init__(self, num_features, hidden_channels, num_classes): super().__init__() self.conv1 = GCNConv(num_features, hidden_channels) self.conv2 = GCNConv(hidden_channels, num_classes) def forward(self, x, edge_index): x = self.conv1(x, edge_index).relu() x = self.conv2(x, edge_index) return x # 加载预训练权重(项目已提供 model/gcn_weights.pth) gcn_model = GCN(num_features=128, hidden_channels=64, num_classes=32) gcn_model.load_state_dict(torch.load("model/gcn_weights.pth")) gcn_model.eval() def score_places(subgraph_data: Data) -> torch.Tensor: """对子图中所有 Place 节点打分""" with torch.no_grad(): embeddings = gcn_model(subgraph_data.x, subgraph_data.edge_index) # 计算与用户画像向量的余弦相似度 user_emb = get_user_embedding(subgraph_data.user_profile) # 从 intent_result 提取 scores = torch.cosine_similarity(embeddings, user_emb, dim=1) return scores参数说明:
num_features=128是节点初始特征维度(由 BERT 提取的文本描述向量);hidden_channels=64平衡效果与速度;num_classes=32是最终嵌入维度,足够区分旅游场景语义。
4. 前端交互:Vue 组件化实现知识图谱可视化与推荐卡片联动
4.1 图谱可视化组件:基于 vis.js 的可交互 force-directed 图
src/components/InteractiveGraph.vue封装了 vis.js 的力导向图,关键特性包括:
- 双击节点展开关联:点击“成都”节点,自动加载其
LOCATED_IN、SERVES_CUISINE、ACCESSIBLE_BY相关节点; - 路径高亮:当推荐结果包含“钟水饺→春熙路地铁站→无障碍电梯”路径时,自动用红色箭头高亮此链路;
- 右键菜单:对任意节点右键,可选择“查看详情”(弹出 Neo4j 原始数据)、“排除此节点”(临时过滤)、“添加到对比列表”。
核心初始化代码:
<!-- src/components/InteractiveGraph.vue --> <template> <div id="graph-container" style="height: 600px; border: 1px solid #eee;"></div> </template> <script> import * as vis from 'vis-network' export default { name: 'InteractiveGraph', props: { initialNodes: Array, // 从 API 获取的初始节点数组 initialEdges: Array // 初始边数组 }, data() { return { network: null, nodes: new vis.DataSet(this.initialNodes), edges: new vis.DataSet(this.initialEdges) } }, mounted() { this.initNetwork() }, methods: { initNetwork() { const container = document.getElementById('graph-container') const data = { nodes: this.nodes, edges: this.edges } const options = { physics: { stabilization: { iterations: 50 } }, interaction: { dragNodes: true, zoomView: true, multiselect: true }, nodes: { shape: 'dot', size: 20, font: { size: 12 } }, edges: { width: 2, arrows: { to: { enabled: true, scaleFactor: 0.8 } } } } this.network = new vis.Network(container, data, options) // 双击节点事件:加载其邻居 this.network.on('doubleClick', (params) => { if (params.nodes.length === 1) { this.loadNeighbors(params.nodes[0]) } }) }, loadNeighbors(nodeId) { // 调用后端 API 获取 nodeId 的直接邻居 this.$http.get(`/api/graph/neighbors/${nodeId}`).then(res => { this.nodes.add(res.data.nodes) this.edges.add(res.data.edges) }) } } } </script>4.2 推荐卡片组件:Vue 响应式渲染与状态同步
src/components/RecommendationCard.vue实现推荐结果的卡片式展示,并与图谱组件双向联动:
<!-- src/components/RecommendationCard.vue --> <template> <div class="recommendation-list"> <div v-for="(item, index) in recommendations" :key="item.name" class="card" @click="highlightOnGraph(item.name)" :class="{ 'active': highlightedName === item.name }" > <h3>{{ item.name }} <span class="score">★{{ item.score }}</span></h3> <p class="explanation">{{ item.explanation }}</p> <div class="details"> <span>📍 {{ item.details.address || '暂无地址' }}</span> <span v-if="item.details.rating">⭐ {{ item.details.rating }}/5</span> </div> </div> </div> </template> <script> export default { name: 'RecommendationCard', props: { recommendations: Array, onHighlight: Function // 父组件传入的高亮回调 }, data() { return { highlightedName: '' } }, methods: { highlightOnGraph(name) { this.highlightedName = name if (this.onHighlight) { this.onHighlight(name) // 通知图谱组件高亮该节点 } } } } </script> <style scoped> .card { padding: 12px; margin: 8px 0; border-radius: 6px; background: #fff; cursor: pointer; transition: all 0.2s; border-left: 4px solid #42b883; } .card:hover { background: #f9f9f9; transform: translateX(4px); } .card.active { background: #e8f5e9; border-left-color: #42b883; } .score { font-size: 14px; color: #ff9800; margin-left: 8px; } </style>提示:
@click="highlightOnGraph(item.name)"触发图谱高亮,onHighlight由父组件App.vue注入,实现组件间通信。这种解耦设计使图谱与推荐列表可独立测试。
4.3 环境配置与依赖管理:Vue CLI 4.5 + Python 3.8 最小兼容方案
项目采用 Vue CLI 4.5(非 Vite)以确保vis-network兼容性,package.json关键依赖:
{ "dependencies": { "axios": "^0.21.1", "core-js": "^3.6.5", "vis-network": "^9.1.2", // 注意:Vite 对 vis-network 支持不佳,故选 Vue CLI "vue": "^2.6.11" }, "devDependencies": { "@vue/cli-plugin-babel": "^4.5.0", "@vue/cli-plugin-eslint": "^4.5.0", "@vue/cli-service": "^4.5.0" } }Python 环境要求明确写入requirements.txt:
# requirements.txt Flask==2.0.3 py2neo==2021.2.3 torch==1.10.2+cpu # 项目已预编译 CPU 版本,避免 CUDA 冲突 torch-geometric==2.0.3 numpy==1.21.6 pandas==1.3.5 tabula-py==2.5.0注意:
torch-geometric安装需严格匹配torch版本,执行pip install torch-geometric -f https://data.pyg.org/whl/torch-1.10.2+cpu.html(项目文档已提供此命令)。
5. 部署与调试:从本地启动到生产环境的平滑过渡技巧
5.1 一键启动脚本:解决端口冲突与服务依赖顺序
新手常卡在“启动顺序”上:Neo4j 未就绪时 Flask 就去连接,导致报错。scripts/start_all.sh通过健康检查确保服务按序启动:
#!/bin/bash # scripts/start_all.sh echo "✅ 启动 Neo4j..." sudo systemctl start neo4j # 等待 Neo4j 就绪(检查 bolt 端口) while ! nc -z localhost 7687; do echo "⏳ 等待 Neo4j 启动..." sleep 3 done echo "✅ Neo4j 已就绪" echo "✅ 启动 Flask 后端..." cd backend && nohup python app.py > flask.log 2>&1 & FLASK_PID=$! sleep 2 if ! kill -0 $FLASK_PID 2>/dev/null; then echo "❌ Flask 启动失败,请检查 flask.log" exit 1 fi echo "✅ Flask 已就绪(PID: $FLASK_PID)" echo "✅ 启动 Vue 前端..." cd ../frontend && npm run serve技巧:
nc -z localhost 7687比curl http://localhost:7474更可靠,因 Neo4j 的 HTTP 服务可能先于 Bolt 服务就绪。
5.2 常见部署问题与精准修复方案
| 现象 | 根本原因 | 修复命令/操作 |
|---|---|---|
Vue 页面空白,控制台报vis_network_1 is not defined | vis-network未正确全局注入 | 在main.js顶部添加import * as vis from 'vis-network'; window.vis = vis; |
Flask 报错Connection refused连接 Neo4j | Neo4j 未启用远程访问 | 编辑/etc/neo4j/neo4j.conf,取消注释dbms.connectors.default_listen_address=0.0.0.0并重启sudo systemctl restart neo4j |
| 推荐结果为空,但图谱查询正常 | GNN 模型权重文件路径错误 | 检查gnn/scorer.py中torch.load("model/gcn_weights.pth")的相对路径,应为os.path.join(os.path.dirname(__file__), "../model/gcn_weights.pth") |
中文乱码(如景点名显示为?) | Neo4j 默认字符集非 UTF-8 | 在/etc/neo4j/neo4j.conf添加dbms.directories.import=/var/lib/neo4j/import,并确保导入 CSV 文件为 UTF-8 BOM 编码 |
5.3 毕业答辩演示技巧:3 分钟讲清技术亮点
答辩时评委最关注“你做了什么独特工作”。建议按此结构陈述(配屏幕共享):
第一分钟:直击痛点
“传统旅游推荐只做关键词匹配,比如搜‘带老人’,返回的是标题含‘老人’的景点。而我们的系统,通过知识图谱将‘轮椅通道’‘电梯直达’‘休息区’等设施与景点关联,并用 GNN 计算这些设施与用户画像的语义距离——这才是真正的‘理解需求’。”第二分钟:展示核心证据
切换到 Neo4j Browser,执行MATCH (p:Place {name:"钟水饺总店"})-[:HAS_FACILITY]->(f) RETURN p.name, f.type, f.confidence,展示三条设施关系及置信度;再切到前端,输入“带爸妈吃川菜”,演示推荐卡片中“解释性文本”如何引用这些置信度数据。第三分钟:强调工程价值
“代码全部模块化:nlu/下意图识别可替换为你们学校的 NLP 课设模型;gnn/下打分模块可接入你们实验室的图神经网络论文;graph/下 Cypher 查询可直接复用于文旅局的数据治理项目——这不是玩具,是可演进的知识图谱工程骨架。”
最后一步:在答辩 PPT 末页放上项目 GitHub 仓库二维码(即使私有,也生成临时公开链接),并注明“代码含详细中文注释,
README.md提供从零部署的每一步截图”。这比说“我写了 10000 行代码”有力得多。
本文还有配套的精品资源,点击获取