用Python构建个人电影推荐系统的全流程实战

最近几年,电影推荐系统变得越来越受欢迎。作为一个电影爱好者,你是否想过自己动手搭建一个个性化的电影推荐平台?本文将带你从零开始,使用Python和豆瓣API构建一个完整的电影推荐系统。不同于简单的API调用教程,我们会涵盖数据获取、处理、存储到可视化展示的全流程,最终打造一个可以实际使用的个人电影看板。

1. 准备工作与环境搭建

在开始之前,我们需要准备好开发环境。推荐使用Python 3.8或更高版本,并安装以下必要的库:

pip install requests pandas flask flask-sqlalchemy
  • requests :用于发送HTTP请求获取API数据
  • pandas :数据处理和分析
  • flask :构建简单的Web应用
  • flask-sqlalchemy :数据库操作

提示:建议使用虚拟环境来管理项目依赖,避免与其他项目产生冲突。

1.1 获取豆瓣API访问权限

豆瓣提供了开放的API接口,但需要申请API Key才能使用。目前豆瓣对新申请的API Key有较严格的审核,建议:

  1. 访问豆瓣开发者平台注册账号
  2. 仔细阅读API使用条款
  3. 申请API Key时详细说明使用目的

如果暂时无法获取个人API Key,可以使用公开的测试Key(但可能有调用频率限制)。

2. 豆瓣API数据获取与解析

2.1 基础API调用

让我们从最基本的API调用开始。豆瓣电影API提供了多种数据接口,包括正在热映、Top250、即将上映等。下面是一个获取正在热映电影的示例:

import requests

def get_in_theaters_movies(city="北京", start=0, count=10):
    url = "https://api.douban.com/v2/movie/in_theaters"
    params = {
        "apikey": "你的API_KEY",
        "city": city,
        "start": start,
        "count": count
    }
    response = requests.get(url, params=params)
    if response.status_code == 200:
        return response.json()
    else:
        print(f"请求失败,状态码:{response.status_code}")
        return None

2.2 数据结构解析

豆瓣API返回的是JSON格式的数据,我们需要了解其结构才能有效提取信息。以热映电影接口为例,返回数据主要包含以下字段:

字段名 类型 描述
count int 当前返回的电影数量
start int 起始位置
total int 总电影数量
subjects list 电影列表

每个电影对象又包含丰富的信息:

{
    "rating": {
        "max": 10,
        "average": 8.5,
        "min": 0
    },
    "genres": ["剧情", "犯罪"],
    "title": "电影名",
    "casts": [{"name": "演员1"}, {"name": "演员2"}],
    "directors": [{"name": "导演"}],
    "year": "2023",
    "images": {"large": "海报URL"},
    "id": "电影ID"
}

2.3 多接口数据获取

除了热映电影,我们还可以获取其他类型的电影数据:

def get_top250(start=0, count=10):
    url = "https://api.douban.com/v2/movie/top250"
    params = {
        "apikey": "你的API_KEY",
        "start": start,
        "count": count
    }
    return requests.get(url, params=params).json()

def get_coming_soon(start=0, count=10):
    url = "https://api.douban.com/v2/movie/coming_soon"
    params = {
        "apikey": "你的API_KEY",
        "start": start,
        "count": count
    }
    return requests.get(url, params=params).json()

3. 数据存储与管理

获取到数据后,我们需要将其存储起来以便后续使用。这里我们使用SQLite数据库,它轻量且无需额外配置。

3.1 数据库设计

设计一个简单的电影数据库表结构:

from flask_sqlalchemy import SQLAlchemy
from flask import Flask

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///movies.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Movie(db.Model):
    id = db.Column(db.String(20), primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    original_title = db.Column(db.String(100))
    year = db.Column(db.String(10))
    genres = db.Column(db.String(200))
    rating = db.Column(db.Float)
    directors = db.Column(db.String(200))
    casts = db.Column(db.String(500))
    image_url = db.Column(db.String(200))
    summary = db.Column(db.Text)
    type = db.Column(db.String(20))  # in_theaters/top250/coming_soon
    
    def __repr__(self):
        return f'<Movie {self.title}>'

3.2 数据存储函数

编写将API数据存储到数据库的函数:

def save_movies_to_db(movies_data, movie_type):
    for movie in movies_data['subjects']:
        existing = Movie.query.get(movie['id'])
        if existing:
            continue
            
        new_movie = Movie(
            id=movie['id'],
            title=movie['title'],
            original_title=movie.get('original_title', ''),
            year=movie.get('year', ''),
            genres=','.join(movie.get('genres', [])),
            rating=movie.get('rating', {}).get('average', 0),
            directors=','.join([d['name'] for d in movie.get('directors', [])]),
            casts=','.join([c['name'] for c in movie.get('casts', [])]),
            image_url=movie.get('images', {}).get('large', ''),
            type=movie_type
        )
        db.session.add(new_movie)
    db.session.commit()

4. 构建电影推荐Web应用

有了数据基础,现在我们可以构建一个简单的Web应用来展示和推荐电影。

4.1 Flask基础框架

创建一个基本的Flask应用结构:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html')

@app.route('/movies/<type>')
def show_movies(type):
    movies = Movie.query.filter_by(type=type).all()
    return render_template('movies.html', movies=movies, type=type)

if __name__ == '__main__':
    app.run(debug=True)

4.2 前端模板设计

使用Bootstrap快速搭建界面。创建 templates/index.html

<!DOCTYPE html>
<html>
<head>
    <title>个人电影推荐</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h1 class="text-center">我的电影推荐</h1>
        <div class="row mt-4">
            <div class="col-md-4">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">正在热映</h5>
                        <a href="/movies/in_theaters" class="btn btn-primary">查看</a>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">Top250</h5>
                        <a href="/movies/top250" class="btn btn-primary">查看</a>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">即将上映</h5>
                        <a href="/movies/coming_soon" class="btn btn-primary">查看</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

4.3 电影列表页面

创建 templates/movies.html 展示具体电影列表:

<!DOCTYPE html>
<html>
<head>
    <title>{{ type }}电影</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h1 class="text-center">{{ type }}电影</h1>
        <div class="row row-cols-1 row-cols-md-3 g-4 mt-4">
            {% for movie in movies %}
            <div class="col">
                <div class="card h-100">
                    <img src="{{ movie.image_url }}" class="card-img-top" alt="{{ movie.title }}">
                    <div class="card-body">
                        <h5 class="card-title">{{ movie.title }}</h5>
                        <p class="card-text">
                            <small class="text-muted">{{ movie.year }} | {{ movie.genres }}</small><br>
                            评分: {{ movie.rating }}
                        </p>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
</body>
</html>

5. 进阶功能与优化

5.1 实现简单的推荐算法

基于用户评分数据,我们可以实现一个简单的推荐系统。这里使用基于内容的推荐方法:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel

def get_recommendations(movie_id, num_recommendations=5):
    # 获取所有电影数据
    movies = Movie.query.all()
    
    # 准备数据
    movie_data = []
    for m in movies:
        features = f"{m.genres} {m.directors} {m.casts}"
        movie_data.append({
            'id': m.id,
            'features': features,
            'title': m.title
        })
    
    # 创建TF-IDF矩阵
    tfidf = TfidfVectorizer(stop_words='english')
    tfidf_matrix = tfidf.fit_transform([m['features'] for m in movie_data])
    
    # 计算相似度
    cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
    
    # 找到目标电影的索引
    indices = {m['id']: i for i, m in enumerate(movie_data)}
    idx = indices.get(movie_id)
    
    if idx is None:
        return []
    
    # 获取相似度分数
    sim_scores = list(enumerate(cosine_sim[idx]))
    
    # 按分数排序
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
    
    # 获取最相似的电影
    sim_scores = sim_scores[1:num_recommendations+1]
    movie_indices = [i[0] for i in sim_scores]
    
    return [movie_data[i]['title'] for i in movie_indices]

5.2 添加搜索功能

扩展Flask应用,添加电影搜索功能:

@app.route('/search')
def search():
    query = request.args.get('q', '')
    if query:
        movies = Movie.query.filter(
            Movie.title.contains(query) | 
            Movie.original_title.contains(query) |
            Movie.directors.contains(query) |
            Movie.casts.contains(query)
        ).all()
    else:
        movies = []
    return render_template('search.html', movies=movies, query=query)

对应的搜索页面模板 templates/search.html

<!DOCTYPE html>
<html>
<head>
    <title>电影搜索</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h1 class="text-center">电影搜索</h1>
        <form class="d-flex mt-4" action="/search">
            <input class="form-control me-2" type="search" name="q" placeholder="搜索电影..." value="{{ query }}">
            <button class="btn btn-outline-success" type="submit">搜索</button>
        </form>
        
        {% if movies %}
        <div class="row row-cols-1 row-cols-md-3 g-4 mt-4">
            {% for movie in movies %}
            <div class="col">
                <div class="card h-100">
                    <img src="{{ movie.image_url }}" class="card-img-top" alt="{{ movie.title }}">
                    <div class="card-body">
                        <h5 class="card-title">{{ movie.title }}</h5>
                        <p class="card-text">
                            <small class="text-muted">{{ movie.year }} | {{ movie.genres }}</small><br>
                            评分: {{ movie.rating }}
                        </p>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        {% elif query %}
        <div class="alert alert-warning mt-4">没有找到相关电影</div>
        {% endif %}
    </div>
</body>
</html>

5.3 性能优化与缓存

为了提高响应速度,我们可以添加缓存机制:

from flask_caching import Cache

cache = Cache(app, config={'CACHE_TYPE': 'simple'})

@app.route('/movies/<type>')
@cache.cached(timeout=3600)  # 缓存1小时
def show_movies(type):
    movies = Movie.query.filter_by(type=type).all()
    return render_template('movies.html', movies=movies, type=type)

6. 部署与后续扩展

6.1 项目部署

完成开发后,我们可以将项目部署到云服务器。推荐使用Gunicorn作为WSGI服务器:

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app

对于生产环境,建议使用Nginx作为反向代理,并配置SSL证书。

6.2 可能的扩展方向

这个基础项目还有很多可以扩展的地方:

  • 用户系统 :添加用户注册登录功能,记录用户偏好
  • 评分功能 :允许用户对电影评分,实现个性化推荐
  • 定时任务 :设置定时任务自动更新电影数据
  • 移动端适配 :优化界面适应移动设备
  • 数据分析 :使用pandas和matplotlib分析电影数据趋势

在实际项目中,我发现最耗时的部分往往是数据的清洗和格式化。豆瓣API返回的数据结构虽然丰富,但某些字段需要特别注意处理,比如导演和演员信息是嵌套的JSON结构,需要提取出我们需要的信息。另外,API调用频率限制也是一个需要考虑的因素,建议在代码中添加适当的延迟和错误处理。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐