1. 项目概述:基于知识图谱的电影智能问答系统
最近完成了一个很有意思的项目——基于知识图谱的电影智能问答系统。这个系统能够理解用户用自然语言提出的电影相关问题,比如"诺兰导演了哪些科幻电影"、"推荐几部评分9分以上的剧情片",然后从知识图谱中查询并返回结构化的答案。
这个项目的核心价值在于:
- 实现了从原始数据采集到知识图谱构建,再到智能问答的完整流程
- 采用双重问题理解机制(规则匹配+分词兜底),提高问答准确率
- 通过可视化界面直观展示电影知识图谱中的实体关系
- 整套系统可以完全离线运行,不依赖第三方API服务
我在开发过程中特别注重系统的实用性,即使没有图数据库(Neo4j)环境,系统也能基于JSON文件正常运行。下面我会详细拆解每个模块的实现细节和关键技术点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 整体架构
系统采用模块化设计,主要分为四个核心组件:
code复制用户提问 → [问答引擎] → [知识图谱] → 返回答案
↑ ↑
jieba 分词 数据来源
规则匹配 ↑
[爬虫模块]
豆瓣 Top250
这种分层架构使得各模块职责清晰,便于单独开发和测试。数据流向也很明确:从豆瓣采集原始数据 → 构建知识图谱 → 问答引擎处理查询 → 返回结果给用户。
2.2 技术选型分析
在选择技术栈时,我主要考虑了以下因素:
-
爬虫模块:
- 选用Requests+BeautifulSoup组合而非Scrapy,因为数据量不大(250部电影),轻量级方案更合适
- 添加随机延迟和User-Agent防止被封,实测2-5秒间隔很稳定
- 使用openpyxl导出Excel,方便非技术人员查看数据
-
知识图谱存储:
- Neo4j是最适合存储图结构数据的数据库,查询效率高
- 同时保留JSON文件方案,降低部署门槛
- py2neo库提供了Python操作Neo4j的友好接口
-
问答引擎:
- 优先采用正则模板匹配,准确率高
- jieba分词作为兜底方案,通过自定义词典提升分词准确率
- 设计同义词映射处理"科幻片"vs"科幻"这类表达差异
-
Web界面:
- Flask轻量灵活,适合快速开发小型Web应用
- ECharts的力导向图完美呈现知识图谱结构
- Bootstrap确保界面响应式布局,适配各种设备
2.3 知识图谱设计
知识图谱的核心是实体和关系的设计。经过多次迭代,我最终确定了以下结构:
实体类型:
- Movie(电影):包含title、rating、year等属性
- Person(人物):导演和演员共用此类型
- Genre(类型):如科幻、动作等
- Country(国家):电影产地
关系类型:
- DIRECTED:人物→电影,表示导演关系
- ACTED_IN:人物→电影,表示出演关系
- HAS_GENRE:电影→类型,表示分类关系
- FROM_COUNTRY:电影→国家,表示产地关系
这种设计既覆盖了常见查询需求,又保持了足够的扩展性。例如要新增"编剧"关系,只需添加新的关系类型即可。
3. 数据采集与处理
3.1 爬虫实现细节
爬虫模块需要从豆瓣Top250页面提取完整的电影信息。这里有几个关键技术点:
页面解析策略:
python复制def parse_movie(self, item):
movie = {}
# 电影名(可能有中文名/英文名两种情况)
title_span = item.find('span', class_='title')
movie['title'] = title_span.text.strip() if title_span else ''
# 评分(转换为float类型)
rating_span = item.find('span', class_='rating_num')
movie['rating'] = float(rating_span.text.strip()) if rating_span else 0.0
# 导演和演员信息提取
info_div = item.find('div', class_='bd').find('p', class_='')
if info_div:
info_text = info_div.get_text(strip=True)
# 使用正则提取导演和演员
director_match = re.search(r'导演:\s*([^主]+)', info_text)
actor_match = re.search(r'主演:\s*(.+)', info_text)
movie['directors'] = [d.strip() for d in director_match.group(1).split('/')] if director_match else []
movie['actors'] = [a.strip() for a in actor_match.group(1).split('/')] if actor_match else []
# 年份、国家、类型信息
line2 = info_div.find_next_sibling('p').get_text(strip=True)
parts = [p.strip() for p in line2.split('/')]
movie['year'] = parts[0] if len(parts) > 0 else ''
movie['country'] = parts[1] if len(parts) > 1 else ''
movie['genres'] = parts[2].split() if len(parts) > 2 else []
return movie
反爬策略:
- 设置合理的User-Agent,模拟浏览器访问
- 每页请求间隔2-5秒随机延迟
- 捕获异常并重试,保证数据完整性
- 使用IP代理池(可选,本项目未使用)
注意事项:豆瓣对爬虫比较敏感,建议在非高峰时段运行爬虫,并控制请求频率。如果被封IP,可以尝试更换User-Agent或增加延迟时间。
3.2 数据清洗与转换
原始爬取的数据需要经过清洗才能使用:
-
字段标准化:
- 统一日期格式(如"1994"→1994)
- 处理空值(如没有quote的电影)
- 拆分组合字段(如"美国/中国大陆"拆分为列表)
-
名称规范化:
- 去除特殊字符和空格
- 统一人名格式(如"克里斯托弗·诺兰"和"诺兰"视为同一人)
-
类型合并:
- 将相似类型合并(如"科幻"和"科幻片")
- 建立类型同义词表
清洗后的数据会输出为JSON和Excel两种格式,JSON用于系统加载,Excel便于人工检查。
4. 知识图谱构建
4.1 Neo4j数据库操作
使用py2neo库操作Neo4j的核心流程:
python复制from py2neo import Graph, Node, Relationship
# 连接数据库
graph = Graph("bolt://localhost:7687", auth=("neo4j", "password"))
# 创建节点(使用merge避免重复)
movie_node = Node("Movie",
title="肖申克的救赎",
rating=9.7,
year=1994)
graph.merge(movie_node, "Movie", "title")
# 创建关系
director_node = Node("Person", name="弗兰克·德拉邦特")
graph.merge(director_node, "Person", "name")
directed_rel = Relationship(director_node, "DIRECTED", movie_node)
graph.merge(directed_rel)
4.2 批量导入优化
当导入250部电影数据时,直接逐个创建节点效率很低。我采用了以下优化措施:
- 事务批量提交:每50个操作作为一个事务提交
- 并行处理:使用多线程处理不同类型的数据
- 预检查:导入前检查数据有效性,避免中途失败
python复制from py2neo import Transaction
def batch_import(movies):
tx = graph.begin()
for i, movie in enumerate(movies):
# 创建节点和关系
if i % 50 == 0:
graph.commit(tx)
tx = graph.begin()
graph.commit(tx)
4.3 无Neo4j的备用方案
考虑到Neo4j的安装门槛,系统设计了基于JSON的备用方案:
- 将电影数据加载到内存中的字典结构
- 实现基于字典的图遍历算法
- 对简单查询,性能差异不大
这种设计使得系统可以灵活部署,降低了使用门槛。
5. 问答引擎实现
5.1 问题理解机制
问答引擎采用双重理解策略:
策略一:正则模板匹配(优先)
python复制self.patterns = [
{
'pattern': r'(.+?)导演了?(哪些|什么)(电影|作品)[??]?',
'type': 'director_movies',
'extract': lambda m: m.group(1).strip()
},
{
'pattern': r'(.+?)的评分(是多少|怎么样)[??]?',
'type': 'movie_rating',
'extract': lambda m: m.group(1).strip()
},
# 更多模式...
]
def match_pattern(self, question):
for p in self.patterns:
match = re.search(p['pattern'], question)
if match:
return p['type'], p['extract'](match)
return None, None
策略二:jieba分词+实体识别(兜底)
python复制def jieba_parse(self, question):
words = list(jieba.cut(question))
entities = []
for word in words:
# 检查是否是已知的电影、人物或类型
if self.is_movie(word):
entities.append(('movie', word))
elif self.is_person(word):
entities.append(('person', word))
# 其他类型...
return entities
5.2 查询逻辑实现
针对不同类型的查询,实现专门的查询方法:
python复制def get_director_movies(self, director_name):
"""查询导演的所有电影"""
movies = []
for movie in self.data['movies']:
for d in movie.get('directors', []):
if director_name in d or d in director_name:
movies.append(movie)
break
if not movies:
return f"没有找到{director_name}导演的电影", []
# 按评分排序
movies.sort(key=lambda x: x['rating'], reverse=True)
# 生成自然语言回答
answer = f"{director_name}导演的电影共有{len(movies)}部,评分最高的三部是:\n"
for m in movies[:3]:
answer += f" • {m['title']} ({m['year']}) 评分:{m['rating']}\n"
return answer, movies
5.3 同义词与模糊匹配
为了提高系统的容错能力,实现了以下特性:
- 类型同义词:
python复制genre_map = {
'科幻片': '科幻',
'动作片': '动作',
'喜剧片': '喜剧',
# ...
}
- 名称模糊匹配:
python复制def fuzzy_match(name, candidates):
# 简单实现:包含关系
for c in candidates:
if name in c or c in name:
return c
return None
- 拼音支持(可选扩展):
python复制from pypinyin import lazy_pinyin
def is_pinyin_match(name, candidate):
return ''.join(lazy_pinyin(name)) in ''.join(lazy_pinyin(candidate))
6. Web界面开发
6.1 Flask后端设计
后端采用经典的MVC模式:
python复制from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/ask', methods=['POST'])
def ask_question():
data = request.get_json()
question = data.get('question', '').strip()
# 初始化问答引擎(单例模式)
if 'qa_engine' not in g:
g.qa_engine = QAEngine()
# 获取答案
answer, results = g.qa_engine.answer(question)
return jsonify({
'success': True,
'answer': answer,
'results': results
})
6.2 前端交互实现
前端使用纯JavaScript实现动态交互:
javascript复制document.getElementById('ask-form').addEventListener('submit', async (e) => {
e.preventDefault();
const question = document.getElementById('question-input').value;
const response = await fetch('/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const data = await response.json();
// 显示答案
document.getElementById('answer').innerHTML =
`<p>${data.answer.replace(/\n/g, '<br>')}</p>`;
// 显示电影卡片
if(data.results.length > 0) {
renderMovies(data.results);
}
});
6.3 知识图谱可视化
使用ECharts实现力导向图:
javascript复制function initGraph() {
fetch('/api/graph-data')
.then(res => res.json())
.then(data => {
const chart = echarts.init(document.getElementById('graph'));
const option = {
series: [{
type: 'graph',
layout: 'force',
data: data.nodes,
links: data.links,
categories: data.categories,
force: {
repulsion: 100,
edgeLength: 100
},
// 更多配置...
}]
};
chart.setOption(option);
});
}
7. 部署与优化
7.1 系统部署方案
提供多种部署方式以适应不同场景:
-
开发模式:
bash复制
python web/app.py -
生产部署:
- 使用Gunicorn+Gevent提高并发能力
- Nginx反向代理处理静态文件
- Supervisor管理进程
-
Docker部署(可选):
dockerfile复制FROM python:3.8 WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["gunicorn", "-w 4", "-k gevent", "web.app:app"]
7.2 性能优化措施
-
缓存机制:
- 对频繁查询的结果进行缓存
- 使用LRU策略控制缓存大小
-
数据预加载:
- 启动时预加载常用数据到内存
- 建立内存索引加速查询
-
异步处理:
- 对耗时操作使用Celery异步任务
- 前端轮询获取结果
8. 常见问题与解决方案
8.1 爬虫相关问题
Q:爬虫被封IP怎么办?
A:可以尝试以下方法:
- 增加请求间隔时间(5-10秒)
- 轮换User-Agent
- 使用代理IP池
- 改用项目提供的示例数据
Q:部分电影信息缺失怎么处理?
A:系统设计了健壮的数据处理逻辑:
- 关键字段缺失时会跳过该电影
- 非关键字段缺失使用默认值
- 记录错误日志便于后续修复
8.2 知识图谱问题
Q:Neo4j导入速度慢怎么办?
A:优化建议:
- 使用批量导入API而非单条插入
- 增加JVM内存分配
- 关闭自动索引创建,导入后再建索引
Q:如何扩展新的实体和关系?
A:只需三步:
- 在数据模型中定义新实体/关系
- 修改爬虫收集相应数据
- 更新问答引擎的支持逻辑
8.3 问答引擎问题
Q:系统无法理解某些问法怎么办?
A:可以通过以下方式改进:
- 在patterns中添加新的正则模板
- 扩充jieba的自定义词典
- 增加同义词映射规则
Q:模糊匹配准确率不高怎么优化?
A:可以考虑:
- 引入编辑距离算法
- 使用词向量计算语义相似度
- 添加用户反馈机制修正错误
9. 项目扩展方向
这个基础系统还有很大的扩展空间:
-
多数据源融合:
- 整合IMDB、烂番茄等数据源
- 解决不同来源的数据冲突
-
增强NLP能力:
- 引入意图识别模型
- 实现多轮对话
- 支持更复杂的查询(如"推荐类似《盗梦空间》的电影")
-
推荐系统集成:
- 基于用户历史查询做个性化推荐
- 结合协同过滤和内容推荐
-
移动端适配:
- 开发微信小程序版本
- 实现语音输入输出
-
知识图谱自动更新:
- 定期自动爬取新电影
- 增量更新知识图谱
在实际开发过程中,我发现知识图谱的构建质量直接影响问答效果。特别是实体对齐(如不同来源的同一导演)和关系定义(如"合作"关系)需要精心设计。另外,问答模板的覆盖范围决定了系统的实用程度,需要不断积累和优化。
