简介:知识图谱是一种将实体与关系结构化表达的语义网络技术,其核心在于通过节点和边建模现实世界的复杂关联。在旅游推荐场景中,它突破传统关键词匹配局限,支持对‘老人+儿童+海边’等复合需求的逻辑推理与多维约束满足。依托Django框架的工程化能力、SQL数据库的可控性与Python的数据处理优势,该方案以轻量级技术栈实现高可解释、易维护、可演示的知识建模与路径推理。特别适合高校毕设落地,兼顾教学深度与工程实用性,是理解知识图谱本质而非堆砌工具链的典型实践。
1. 项目概述:一个能“读懂”旅游需求的Django系统长什么样?
你有没有试过在旅游平台搜“适合带老人和小孩的海边城市”,结果跳出一堆网红打卡地、潜水胜地,甚至还有冲浪教学?或者输入“雨天室内文化体验”,系统却推荐了露天古城墙和山顶观景台?这不是算法偷懒,而是传统推荐系统根本没能力理解“老人+小孩+海边”背后隐含的交通便利性、无障碍设施、医疗配套、儿童友好服务等多层语义关系——它只认关键词匹配,不认逻辑链条。
这个标题里的“Django框架多模态知识图谱智能旅游推荐系统”,说白了就是给旅游推荐装上了一套能“深度思考”的大脑。它不是简单把景点、酒店、天气、交通这些数据扔进数据库里存着,而是用知识图谱把它们像神经元一样连起来:青岛栈桥不仅是一个景点,它关联着“步行可达地铁站3号口”“附近有三级甲等医院”“设有母婴室和轮椅坡道”“夏季平均湿度72%”“周边500米内3家连锁药店”……这些信息不是孤立字段,而是以节点(Node)和关系(Edge)构成的网状结构。当用户输入“带6岁孩子和75岁父母的4日青岛行程”,系统不是查“青岛”+“亲子”+“老年”三个标签,而是沿着知识图谱推理:找同时满足“无障碍通行”“儿童托管服务”“慢节奏游览动线”“医疗应急半径≤1km”的景点组合,并动态计算路线时间、体力消耗、天气适配度——这才是真正的“智能”。
我带学生做毕设时发现,90%的所谓“智能推荐”项目,本质还是基于用户历史行为的协同过滤或内容相似度计算,底层数据模型仍是扁平的SQL表结构。而这个项目用Django做骨架,却把SQL数据库当作知识图谱的“持久化底座”而非核心引擎——它用Django ORM管理业务逻辑,用原生SQL精细控制图谱数据的导入、更新与查询优化,再通过Python脚本构建图谱拓扑,最后用Django视图层把推理结果渲染成可交互的行程单。整个流程里,Django不是万能胶,而是指挥官;SQL不是老古董,而是高精度手术刀;Python不是胶水语言,而是知识建模的画笔。如果你正在找毕设选题,别被“知识图谱”四个字吓退——它在这里不是要你从零造轮子,而是教你如何用最熟悉的Django+SQL+Python,把旅游推荐这件事做得真正“懂人”。
2. 系统架构设计与技术选型逻辑
2.1 为什么用Django而不是Flask或FastAPI?
很多人看到“知识图谱”就默认要上Neo4j、GraphQL、微服务,但这个毕设项目反其道而行之,坚持用Django作为主框架,背后有三重现实考量:
第一是开发效率与教学适配性。Django自带Admin后台、用户认证、ORM、模板引擎、URL路由,毕设周期通常只有3-6个月,学生需要快速搭建可演示的完整系统。我试过用Flask从零配JWT认证+分页+文件上传+邮件通知,光配置就耗掉两周;而Django一条命令python manage.py startapp tourism,再注册到settings.py,Admin后台立刻能增删景点数据——这对毕设答辩时现场演示“后台录入新景点并实时影响推荐结果”至关重要。更关键的是,高校课程普遍教Django ORM,学生写Tourist.objects.filter(age__range=(60,80))比写SQL更顺手,降低学习曲线。
第二是SQL数据库的深度掌控需求。知识图谱的构建阶段需要大量复杂JOIN、窗口函数、递归CTE(Common Table Expressions)来清洗和关联数据。比如,从携程爬取的景点数据含“门票价格”字段,但不同平台标价单位不同(元/人、元/家庭、含导览费),需用SQL的CASE WHEN统一转换;又如计算“景点间步行可达性”,需对地理坐标做Haversine距离计算,MySQL 5.7+原生支持ST_Distance_Sphere()函数,Django ORM虽能调用,但写原生SQL更直观可控。Flask虽灵活,但学生容易陷入“自己造轮子”的陷阱;而Django明确区分“ORM用于业务逻辑”“原生SQL用于数据工程”,边界清晰。
第三是部署与维护的确定性。毕设系统最终要部署到学校服务器或阿里云学生机,Django+uWSGI+Nginx的组合经过十年验证,出问题有海量中文文档可查。去年有学生用FastAPI+PostgreSQL+Redis做类似项目,本地跑得好好的,一上云就报asyncpg.exceptions.TooManyConnectionsError,折腾三天才发现是学生机内存不足导致连接池溢出——而Django同步模式天然规避这类异步并发陷阱。
提示:Django的“重”恰恰是毕设的优势。它的约定大于配置、全栈一体化,让开发者聚焦在“知识图谱怎么建”“推荐逻辑怎么写”这些核心问题上,而不是反复调试跨域、鉴权、缓存策略。
2.2 为什么知识图谱不用Neo4j而用SQL数据库?
热搜词里“neo4j构建知识图谱”出现频率很高,但这个项目刻意避开Neo4j,选择用MySQL/PostgreSQL存储图谱数据,原因很实在:
成本与运维门槛。Neo4j社区版限制数据库大小(2GB)和并发连接数,毕设数据量虽不大,但学生常因测试导入全量POI(兴趣点)数据超限;企业版需付费,学生项目不可能采购。而MySQL学生机免费、监控工具成熟(phpMyAdmin一键查看慢查询)、备份恢复方案标准(mysqldump)。我让学生对比过:用Neo4j导入10万景点数据,需配置dbms.memory.heap.initial_size=4g,学生机8GB内存直接卡死;而MySQL用分区表+索引优化,同样数据量查询响应<200ms。
技术栈统一性。项目中90%的数据操作是CRUD(增删改查),仅10%涉及图遍历。例如,“查找所有含‘亲子’标签且评分≥4.5的景点”是标准WHERE查询;“推荐与‘故宫’有‘文化关联’且交通时间≤30分钟的博物馆”才需图遍历。若为10%场景引入Neo4j,就得维护两套数据库、两套连接池、两套数据同步逻辑——学生极易在“景点数据更新后忘记同步到Neo4j”导致推荐结果错误。而用SQL模拟图谱,所有节点存node_table(id, name, type, properties_json),所有关系存edge_table(from_id, to_id, relation_type, weight),用WITH RECURSIVE实现深度优先搜索,既保持技术栈纯净,又避免数据不一致风险。
教学价值最大化。毕设不是工业级产品,核心是让学生理解知识图谱的本质——不是某种特定数据库,而是一种数据建模思想。用SQL建模强制学生思考:什么该作为节点(景点、城市、季节)?什么该作为关系(位于、适合、气候影响)?权重如何量化(用户点击率、停留时长、评论情感分)?当学生亲手写INSERT INTO edge_table SELECT a.id, b.id, '气候影响' FROM weather_forecast a JOIN tourist_preference b ON a.city = b.destination WHERE a.humidity > 80 AND b.preference = '舒适干燥',比直接调用Neo4j的MATCH (n:City)-[r:CLIMATE_IMPACT]->(m:Tourist) WHERE r.humidity > 80 RETURN n,m更能体会语义建模的严谨性。
2.3 Python在其中扮演什么不可替代的角色?
Python不是简单充当“胶水”,而是承担三大核心职能:
第一,知识抽取与清洗的主力引擎。项目源码中data_pipeline/目录下,Python脚本负责从多个来源提取结构化知识:
- 用
requests+BeautifulSoup爬取马蜂窝景点详情页,提取“开放时间”“建议游玩时长”“是否允许携带宠物”等非标准化字段,通过正则匹配和规则引擎(如if '轮椅' in text: accessibility = True)转化为结构化属性; - 用
pandas读取文旅局发布的Excel《无障碍旅游设施名录》,清洗地址歧义(“北京路店” vs “北京路123号”),通过高德API地理编码统一为经纬度; - 用
jieba分词+sklearn.feature_extraction.text.TfidfVectorizer计算景点描述文本相似度,自动生成“文化类”“自然类”“亲子类”等隐式标签。这些操作若用SQL纯实现,代码冗长且难以调试;而Python的生态库让数据预处理效率提升5倍以上。
第二,推荐算法的实验沙盒。Django视图层只负责接收请求、调用推荐函数、返回JSON,真正的算法逻辑在recommender/模块中:
- 基于知识图谱的路径推理:用
networkx构建内存图,实现A*算法寻找“老人友好→交通便利→文化体验”最优路径; - 多模态融合:将景点图片用
torchvision.models.resnet18提取视觉特征向量,与文本TF-IDF向量拼接,输入轻量级MLP模型预测用户偏好得分; - 实时反馈闭环:用户点击“收藏”按钮后,Python脚本触发
UPDATE edge_table SET weight = weight * 1.2 WHERE from_id = %s AND to_id = %s,动态强化关系权重。这些算法需频繁迭代调试,Python的交互式环境(Jupyter Notebook)和丰富机器学习库是不可替代的。
第三,SQL优化的智能助手。源码中sql_optimizer.py脚本会自动分析慢查询:
- 解析Django生成的SQL,识别缺失索引(如
WHERE city = '青岛' AND season = '夏季'未建联合索引); - 模拟数据分布,推荐最优索引策略(
CREATE INDEX idx_city_season ON attraction (city, season)); - 对复杂图遍历查询,生成执行计划对比报告(
EXPLAIN FORMAT=JSON)。这让学生直观理解“为什么加这个索引能让查询从3秒降到200毫秒”,远比背诵数据库理论更深刻。
3. 核心模块拆解与实操细节
3.1 知识图谱构建:从原始数据到语义网络的四步转化
知识图谱不是数据库表的简单堆砌,而是对旅游领域知识的结构化重表达。项目源码中knowledge_graph/目录实现了完整的构建流水线,分为四个不可跳过的阶段:
第一步:实体识别与标准化(Entity Recognition & Normalization)
原始数据来自三个渠道:爬虫抓取的景点网页、文旅局发布的设施名录、用户评论文本。每条数据都存在命名歧义——“西湖”可能指杭州西湖、惠州西湖、甚至某家餐厅名。Python脚本entity_normalizer.py采用分层消歧策略:
- 规则层:预置地名词典(如
{"西湖": "杭州西湖", "瘦西湖": "扬州瘦西湖"}),覆盖80%高频歧义; - 上下文层:对评论“在西湖边喝龙井,风景绝了”,用spaCy识别“西湖”与“龙井”(杭州特产)的共现关系,置信度>0.9时判定为杭州西湖;
- 地理层:调用高德API对地址“西湖区南山路”进行逆地理编码,返回精确坐标(30.227,120.135),再与已知景点坐标库比对(欧氏距离<500米即匹配)。
注意:这一步必须人工校验!我让学生抽样检查100条,发现爬虫把“西湖醋鱼”误识别为景点,立即在规则层加入
if '菜' in text or '鱼' in text: skip_entity = True。知识图谱质量取决于源头,宁可少建10个节点,也不建1个错误节点。
第二步:关系抽取与权重量化(Relation Extraction & Weighting)
关系不是凭空定义,而是从数据中挖掘。relation_extractor.py实现三种抽取方式:
- 显式关系:从结构化数据直接提取。如景点详情页的“附近景点”列表,直接生成
(西湖, 附近, 雷峰塔)三元组; - 隐式关系:从文本中推理。用依存句法分析“雷峰塔位于西湖南岸”,提取
(雷峰塔, 位于, 西湖); - 统计关系:从用户行为推断。统计10万条订单数据,发现预订“西湖”后3小时内又订“灵隐寺”的用户占比32%,则生成
(西湖, 高频连游, 灵隐寺),权重=32。
权重设计遵循“可解释性”原则:显式关系权重固定为1.0,隐式关系按置信度(0.6~0.95)赋值,统计关系用实际占比(0.01~0.99)。所有权重存入edge_table.weight字段,为后续推荐提供量化依据。
第三步:图谱存储与索引优化(Storage & Indexing)
SQL表结构设计是性能关键:
-- 节点表:支持动态属性扩展 CREATE TABLE node_table ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, type ENUM('attraction','city','season','facility') NOT NULL, properties JSON, -- 存储{'open_time': '08:00-17:00', 'wheelchair_access': true} created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 关系表:联合索引覆盖高频查询 CREATE TABLE edge_table ( from_id BIGINT NOT NULL, to_id BIGINT NOT NULL, relation_type VARCHAR(50) NOT NULL, weight DECIMAL(3,2) DEFAULT 1.0, PRIMARY KEY (from_id, to_id, relation_type), -- 防止重复关系 INDEX idx_from_type (from_id, relation_type), -- 查询某节点的所有出边 INDEX idx_to_type (to_id, relation_type) -- 查询某节点的所有入边 );实操心得:
properties JSON字段看似灵活,但Django ORM查询JSON字段需用__contains,性能较差。因此源码中对高频查询属性(如wheelchair_access)单独建列,并用generated column保持同步:ALTER TABLE node_table ADD COLUMN wheelchair_access TINYINT GENERATED ALWAYS AS (JSON_EXTRACT(properties, '$.wheelchair_access')) STORED;这样WHERE wheelchair_access = 1走索引,速度提升10倍。
第四步:图谱验证与质量评估(Validation & QA)
构建完成后必须验证。graph_validator.py执行三项检查:
- 连通性检查:用BFS算法验证“北京”节点能否到达所有5A级景点,发现“八达岭长城”因地址写成“北京市延庆县”未匹配,立即修正;
- 一致性检查:扫描所有
(景点, 位于, 城市)关系,确认城市名在node_table.type='city'中存在,揪出3个错别字(“杭州市”写成“抗州市”); - 覆盖率检查:对比文旅局公布的127个无障碍景点,图谱中仅覆盖118个,缺失9个——定位到爬虫未抓取小众场馆,补抓后重新入库。
这一步耗时占总构建时间40%,但能避免后期推荐逻辑因数据缺陷而失效。
3.2 智能推荐引擎:三层过滤与动态加权的实战逻辑
推荐不是“猜你喜欢”,而是基于知识图谱的精准推理。源码中recommender/core.py实现三级过滤机制,每层都可独立开关调试:
第一层:硬性约束过滤(Hard Constraint Filtering)
这是安全底线,必须100%满足。用户输入“带婴儿的三亚3日游”,系统首先执行:
# Django ORM查询,生成高效SQL constraints = Q(city='三亚') & Q(season='夏季') if user.has_infant: constraints &= Q(wheelchair_access=True) & Q(nearby_hospital_distance__lte=1) # 1km内有医院 if user.budget == 'low': constraints &= Q(ticket_price__lte=100) # 执行:SELECT * FROM attraction WHERE city='三亚' AND season='夏季' AND wheelchair_access=1 AND nearby_hospital_distance <= 1;注意:所有硬性条件必须对应数据库字段,不能依赖图谱关系。因为
nearby_hospital_distance是节点属性,查询走索引;而“是否有儿科门诊”需遍历(医院, 提供, 儿科)关系,延迟高。毕设阶段优先保障基础可用性。
第二层:知识图谱路径推理(Knowledge Path Reasoning)
硬性过滤后剩200个景点,需从中找出逻辑最优组合。path_reasoner.py用改进的A*算法:
- 启发式函数h(n):不是简单估算地理距离,而是综合
user_preference_score(用户历史偏好) +season_compatibility(季节适配分) +crowd_level(人流热度); - 边权重g(n):不是固定值,而是动态计算。例如“从亚龙湾到蜈支洲岛”的交通时间,根据实时路况API调整(晴天30分钟,暴雨90分钟);
- 路径约束:强制包含“至少1个亲子互动项目”“每日步行≤8000步”“午休点距景点≤500米”。
实测:对“三亚亲子游”,算法输出路径亚龙湾热带天堂森林公园 → 亚龙湾海底世界 → 三亚湾椰梦长廊,全程步行1.2km,含2个母婴室、3家药店,完全符合约束。
第三层:多模态特征融合排序(Multimodal Fusion Ranking)
最终对候选景点打分排序。fusion_ranker.py融合三类特征:
- 结构化特征:来自SQL表的
rating(评分)、review_count(评论数)、ticket_price(价格); - 文本特征:景点描述TF-IDF向量,用余弦相似度匹配用户搜索词“亲子”“轻松”;
- 视觉特征:用ResNet18提取景点主图特征,计算与用户历史收藏图的相似度(如用户常收藏蓝天碧海图,则偏好高饱和度图片)。
融合公式:final_score = 0.4*structured_score + 0.3*text_score + 0.3*vision_score。权重经网格搜索确定,structured_score权重最高,因毕设数据中结构化信息最可靠。
实操心得:多模态融合易陷入“为融合而融合”。我让学生先关闭视觉特征,仅用结构化+文本特征,准确率82%;加入视觉特征后升至85%,但训练时间增加3倍。最终决定保留,因毕设需体现“多模态”创新点,且Django部署时用Celery异步预提特征,不影响实时响应。
3.3 Django集成:如何让知识图谱“活”在Web界面
Django不是知识图谱的容器,而是它的翻译器和展示台。views.py中的TourRecommendView是核心枢纽:
请求解析与意图识别
用户输入“冬天去哈尔滨看雪,要便宜”,视图层不做NLP,而是用规则引擎快速分类:
def parse_intent(query): if '冬天' in query or '雪' in query: season = '冬季' elif '夏天' in query: season = '夏季' else: season = '全年' budget_keywords = {'便宜': 'low', '经济': 'low', '省钱': 'low', '豪华': 'high'} budget = budget_keywords.get(next((k for k in budget_keywords if k in query), None), 'medium') return {'season': season, 'budget': budget, 'query_raw': query}注意:毕设不追求完美NLP,规则引擎快、准、易调试。学生可在此基础上加简单BERT微调,但非必需。
图谱查询与结果组装
调用推荐引擎后,需将冷冰冰的ID列表转为前端可渲染的富媒体数据:
# 推荐引擎返回 [101, 102, 103](景点ID) attractions = Attraction.objects.filter(id__in=[101,102,103]).select_related('city').prefetch_related( Prefetch('edges_from', queryset=Edge.objects.select_related('to_node')), Prefetch('edges_to', queryset=Edge.objects.select_related('from_node')) ) # 一次查询获取景点详情+所有关联节点(如“附近地铁站”“推荐酒店”),避免N+1查询前端交互增强
Django模板recommendation.html不只是列表展示:
- 点击景点卡片,弹出Modal显示知识图谱关系图(用
vis.js渲染,节点大小=权重,连线粗细=关系强度); - 拖拽行程条调整天数,后端实时重算路径并返回新方案;
- “收藏”按钮触发AJAX请求,Python脚本更新
edge_table权重并刷新缓存。
所有交互逻辑在Django完成,无需额外框架,降低复杂度。
4. SQL数据库设计与性能优化实战
4.1 表结构设计:平衡范式与查询效率的取舍
毕设数据库不是学术范式教科书,而是为推荐场景定制的高性能存储。models.py中关键表设计体现三大妥协:
attraction表:反范式化存储高频查询字段
class Attraction(models.Model): name = models.CharField(max_length=200) city = models.CharField(max_length=100) # 反范式:不关联city表,避免JOIN season_compatibility = models.JSONField() # {'winter': 0.8, 'summer': 0.3},预计算避免运行时计算 wheelchair_access = models.BooleanField(default=False) # 单独列,便于WHERE索引 # ... 其他字段为什么反范式?因为推荐查询90%是
WHERE city='三亚' AND wheelchair_access=1,若city存外键,每次查询需JOINcity_table,增加I/O开销。学生机SSD随机读取延迟约0.1ms,JOIN一次多0.1ms,100并发就是10ms延迟——对实时推荐不可接受。牺牲一点存储空间(city名称重复存储),换取查询速度,值得。
edge_table:用复合主键替代自增ID
CREATE TABLE edge_table ( from_id BIGINT NOT NULL, to_id BIGINT NOT NULL, relation_type VARCHAR(50) NOT NULL, weight DECIMAL(3,2) DEFAULT 1.0, PRIMARY KEY (from_id, to_id, relation_type), -- 无自增ID INDEX idx_from_type (from_id, relation_type), INDEX idx_to_type (to_id, relation_type) );优势:1)主键即索引,查询
SELECT * FROM edge_table WHERE from_id=101 AND relation_type='附近'走聚簇索引,速度最快;2)防止重复关系(同一对节点不能有两条“附近”关系);3)节省存储(省去8字节BIGINT ID)。缺点:插入时需确保from_id/to_id存在,但Django信号post_save可拦截验证。
user_profile表:JSON字段存储动态偏好
class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) preferences = models.JSONField(default=dict) # {'climate': 'dry', 'pace': 'slow', 'interests': ['history', 'food']} # 不拆分为多张表,因偏好项极少变化,且查询时只需整体读取为什么用JSON?用户偏好字段少(<10个)、更新频次低(月级)、查询模式固定(全量读取)。若拆表,
SELECT p.* FROM user_profile p JOIN preference_item i ON p.id=i.profile_id WHERE i.key='climate',JOIN开销大于JSON解析。实测:JSON字段查询+json.loads()耗时0.5ms,JOIN查询耗时1.2ms。
4.2 索引策略:让慢查询从3秒降到200毫秒
索引不是越多越好,而是针对真实查询负载设计。sql_optimizer.py分析了毕设典型慢查询,针对性创建索引:
高频查询1:按城市+季节筛选景点
原始查询:SELECT * FROM attraction WHERE city='三亚' AND season='冬季';
执行计划显示type: ALL(全表扫描)。优化:
-- 创建联合索引,顺序按选择性高低:city选择性高(全国300+城市),season选择性低(仅四季) CREATE INDEX idx_city_season ON attraction (city, season);效果:查询时间从2800ms → 180ms。原理:B+树先按city排序,再在每个city子树内按season排序,定位极快。
高频查询2:查找某景点的所有关联节点
原始查询:SELECT e.*, n2.name FROM edge_table e JOIN node_table n2 ON e.to_id=n2.id WHERE e.from_id=101;
执行计划显示e表走全表扫描。优化:
-- 在edge_table上建(from_id, to_id)联合索引,覆盖查询所需字段 CREATE INDEX idx_from_to ON edge_table (from_id, to_id); -- 同时在node_table的id上确保有主键索引(默认存在)效果:从1200ms → 85ms。注意:idx_from_to索引已包含to_id,SELECT e.*, n2.name中e.*的其他字段(如relation_type)需回表,但因to_id是主键,回表代价低。
高频查询3:按权重排序取Top10
原始查询:SELECT * FROM edge_table WHERE relation_type='适合' ORDER BY weight DESC LIMIT 10;
执行计划显示Using filesort。优化:
-- 创建(relation_type, weight)联合索引,weight倒序存储 CREATE INDEX idx_relation_weight ON edge_table (relation_type, weight DESC);效果:从950ms → 45ms。关键点:DESC必须显式声明,否则MySQL按升序存储,ORDER BY weight DESC仍需排序。
实操心得:索引创建后必须验证。我让学生用
EXPLAIN FORMAT=JSON对比优化前后执行计划,重点关注key(使用索引)、rows(扫描行数)、Extra(是否Using filesort)。曾有学生建了idx_city单列索引,但查询是WHERE city='三亚' AND budget='low',因未覆盖budget字段,索引失效——务必用真实查询测试。
4.3 数据库迁移与版本控制:毕设协作不翻车
多人协作时,数据库变更极易冲突。项目采用Django原生迁移+SQL脚本双轨制:
Django迁移管理
- 所有模型变更(如新增字段)用
python manage.py makemigrations生成.py文件; - 迁移文件提交Git,团队成员
python manage.py migrate自动执行; - 关键迁移(如
Add wheelchair_access field)在migrations/0003_add_wheelchair_access.py中添加RunPython操作,填充默认值:
def set_default_access(apps, schema_editor): Attraction = apps.get_model('tourism', 'Attraction') Attraction.objects.filter(wheelchair_access__isnull=True).update(wheelchair_access=False) class Migration(migrations.Migration): operations = [ migrations.AddField(...), migrations.RunPython(set_default_access), ]SQL脚本补充
- 复杂数据操作(如知识图谱初始化)写
.sql文件存sql_scripts/目录; init_knowledge_graph.sql包含:创建node_table/edge_table、导入基础城市数据、设置初始关系;- 执行命令:
mysql -u root -p tourism < sql_scripts/init_knowledge_graph.sql。
注意:Django迁移不管理
node_table,因它是知识图谱专用表,与Django模型无关。SQL脚本由setup_database.sh统一调用,确保环境一致性。
5. 毕设落地常见问题与独家避坑指南
5.1 数据获取难题:没有公开API怎么办?
毕设最大痛点不是算法,而是数据。项目源码中data_collection/目录提供了三套零成本方案:
方案1:结构化数据爬取(推荐)
- 目标:马蜂窝、携程景点页(反爬较弱);
- 技巧:用
requests.Session()维持会话,time.sleep(random.uniform(1,3))模拟人工; - 关键:绕过JS渲染。马蜂窝景点页数据在HTML源码中,用
BeautifulSoup直接解析<script>标签内的window.__INITIAL_STATE__变量,提取JSON数据,比Selenium快10倍; - 风险:IP被封。对策:用
fake-useragent随机UA,配合rotating-proxies轮换免费代理(如https://free-proxy-list.net/),学生机足够用。
方案2:政府开放数据(权威)
- 目标:各省市文旅局官网“数据开放平台”;
- 实例:浙江省文旅厅发布《全省A级景区名录》Excel,含坐标、等级、开放时间;
- 技巧:
pandas.read_excel()读取后,用geopy批量地理编码,生成标准经纬度; - 优势:数据权威、免费、无版权风险,毕设答辩时加分项。
方案3:人工标注+规则生成(保底)
- 当爬取失败时,用
django-import-export导入Excel模板; - 模板含
name(景点名)、city(城市)、tags(逗号分隔,如“亲子,文化,免费”); - Python脚本
tag_to_relations.py自动转换:tags="亲子,免费"→ 生成(景点, 适合, 亲子)、(景点, 门票, 免费)关系; - 学生3小时可录入100个景点,足够毕设演示。
避坑:严禁用“Python爬虫源码大全”里的通用爬虫,那些代码针对旧版网站,90%已失效。必须针对目标网站写专属解析器。
5.2 推荐结果不理想?先检查这五个致命点
学生常抱怨“推荐结果很随机”,90%问题出在以下环节:
问题1:知识图谱关系权重为0或NULL
- 现象:所有景点推荐得分相同;
- 检查:
SELECT COUNT(*) FROM edge_table WHERE weight IS NULL OR weight = 0; - 原因:关系抽取脚本未处理空值,如
weight = float(row['confidence'])但row['confidence']为空字符串; - 解决:在
relation_extractor.py中加weight = float(row['confidence']) if row['confidence'] else 0.5。
问题2:Django ORM查询未使用select_related/prefetch_related
- 现象:页面加载慢,数据库连接数飙升;
- 检查:Django Debug Toolbar显示N+1查询;
- 原因:
for a in attractions: print(a.city.name),每次循环查一次city_table; - 解决:
Attraction.objects.select_related('city'),一次JOIN获取全部。
问题3:SQL索引未生效
- 现象:
WHERE city='三亚'查询慢; - 检查:
EXPLAIN SELECT * FROM attraction WHERE city='三亚';看key是否为NULL; - 原因:索引列类型不匹配,如
city字段是VARCHAR(100)但查询用WHERE city=123(数字),触发隐式转换,索引失效; - 解决:确保查询参数类型一致,Django中用
filter(city__exact='三亚')。
问题4:图谱路径推理未设深度限制
- 现象:推荐接口超时(>30秒);
- 检查:
path_reasoner.py中A*算法未设max_depth=3; - 原因:知识图谱中“景点→城市→省份→国家→大洲”链路过长,算法穷举;
- 解决:在
find_path函数开头加if depth > 3: return []。
问题5:缓存未清除导致结果陈旧
- 现象:修改景点数据后,推荐结果不变;
- 检查:Django默认缓存
get_or_404结果; - 解决:在视图中禁用缓存
@never_cache,或手动cache.delete('recommendation_123')。
5.3 答辩演示技巧:让老师眼前一亮的三个细节
毕设答辩不是代码朗诵,而是讲故事。我指导的学生用以下技巧拿高分:
细节1:演示“数据如何驱动推荐”
- 不直接点“推荐”按钮,而是打开Admin后台,找到“西湖”景点,修改
wheelchair_access=False→ 保存; - 再切回前台,输入“带老人游杭州”,展示推荐列表中“西湖”消失,“灵隐寺”(
wheelchair_access=True)顶替出现; - 说明:“老师,您看到的不是算法神奇,而是知识图谱中一个属性的改变,实时传导到推荐结果——这就是数据驱动的价值。”
细节2:对比传统推荐与本系统
- 准备两个查询:
- 传统系统:“杭州景点” → 返回西湖、千岛湖、
本文还有配套的精品资源,点击获取