引言:老片经典台词的魅力与价值

老片经典台词是电影艺术的精华,它们往往浓缩了时代精神、人物情感和文化内涵。这些台词不仅在当时引起共鸣,更在岁月的洗礼下历久弥新,成为流行文化的重要组成部分。例如,《乱世佳人》中的”明天又是新的一天”(Tomorrow is another day)传递出坚韧不拔的乐观精神;《教父》中的”我将给他一个他无法拒绝的条件”(I’m gonna make him an offer he can’t refuse)则展现了权力与威胁的艺术。这些台词之所以经典,是因为它们简洁有力、富有哲理,能够跨越时空与观众产生情感连接。

对于电影爱好者、研究者或普通观众而言,查询老片经典台词的需求日益增长。无论是为了重温经典、撰写影评,还是寻找特定场景的对话,一个高效的台词查询系统都显得尤为重要。本文将详细介绍如何构建一个老片经典台词查询系统,包括数据收集、存储、检索和展示等完整流程,并提供详尽的代码示例,帮助读者从零开始打造属于自己的台词库。

系统架构概述

一个完整的老片经典台词查询系统通常包含以下几个核心模块:

  1. 数据收集模块:负责从各种来源获取电影台词数据,包括电影基本信息、台词文本、出现时间等。
  2. 数据存储模块:设计合理的数据库结构来存储海量台词数据,确保查询效率。
  3. 检索引擎模块:实现高效的搜索算法,支持关键词、演员、电影名等多维度查询。
  4. 用户界面模块:提供友好的交互界面,方便用户输入查询条件并展示结果。
  5. API接口模块:为其他应用提供数据接口,实现系统集成。

我们将采用Python作为主要开发语言,结合SQLite数据库和Flask框架构建一个轻量级但功能完整的查询系统。这种技术栈易于上手,适合个人项目和中小型应用。

数据收集与处理

数据来源

收集老片经典台词的第一步是确定可靠的数据来源。以下是几个常用渠道:

  1. 公开API:如OMDb API(The Open Movie Database)提供电影基本信息,但台词数据有限。
  2. 电影字幕文件:SRT格式的字幕文件包含完整的对话内容,是台词数据的主要来源。
  3. 电影剧本网站:如Scripts.com、IMSDB等网站提供电影剧本的文本版本。
  4. 手动整理:对于特别经典的电影,可以通过观看影片并手动记录的方式收集。

数据格式标准化

为了便于存储和查询,我们需要将收集到的台词数据标准化为统一的格式。一个典型的台词记录应包含以下字段:

  • movie_id:电影唯一标识
  • movie_title:电影标题
  • year:上映年份
  • character:说话角色
  • dialogue:台词文本
  • timestamp:出现时间(可选)
  • scene:场景描述(可选)

数据收集示例代码

以下是一个从SRT字幕文件提取台词的Python脚本示例。该脚本读取SRT文件,解析时间戳和对话内容,并将其转换为结构化数据。

import re
from datetime import timedelta

def parse_srt(file_path):
    """
    解析SRT字幕文件,提取台词数据
    :param file_path: SRT文件路径
    :return: 包含台词数据的列表,每个元素为字典
    """
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()
    
    # SRT格式正则表达式:序号 + 时间戳 + 内容
    pattern = re.compile(r'(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n(.*?)(?=\n\n|\Z)', re.DOTALL)
    matches = pattern.findall(content)
    
    dialogues = []
    for match in matches:
        index, start_time, end_time, text = match
        # 清理文本:移除换行符和多余空格
        cleaned_text = re.sub(r'\n', ' ', text).strip()
        
        # 尝试识别角色(简单规则:大写字母开头的短句可能是角色名)
        # 实际应用中可能需要更复杂的NLP处理或人工标注
        character = "Unknown"
        if cleaned_text and cleaned_text[0].isupper() and len(cleaned_text.split()) <= 3:
            character = cleaned_text
            cleaned_text = ""
        
        dialogues.append({
            'index': int(index),
            'start_time': start_time,
            'end_time': end_time,
            'character': character,
            'dialogue': cleaned_text
        })
    
    return dialogues

# 示例使用
if __name__ == "__main__":
    srt_file = "example_movie.srt"
    try:
        data = parse_srt(srt_file)
        print(f"成功解析 {len(data)} 条台词")
        # 打印前3条
        for item in data[:3]:
            print(f"时间: {item['start_time']} - {item['end_time']}")
            print(f"角色: {item['character']}")
            print(f"台词: {item['dialogue']}\n")
    except FileNotFoundError:
        print(f"错误:文件 {srt_file} 不存在")
    except Exception as e:
        print(f"解析出错: {e}")

代码说明:

  1. 使用正则表达式匹配SRT文件的结构(序号、时间戳、内容)。
  2. 清理文本中的换行符和多余空格。
  3. 尝试通过简单规则识别角色名(实际应用中可能需要更复杂的NLP技术)。
  4. 返回结构化的台词数据列表。

数据清洗与增强

原始数据往往需要进一步清洗和增强才能用于查询系统:

  1. 文本标准化:统一大小写、去除标点、处理特殊字符。
  2. 角色识别:通过脚本或人工方式补充角色信息。
  3. 情感标注:为台词添加情感标签(如幽默、悲伤、愤怒)。
  4. 关键词提取:自动提取台词中的关键词,便于检索。

以下是一个简单的文本标准化函数:

import string

def standardize_text(text):
    """
    标准化文本:转小写、去标点、去多余空格
    :param text: 原始文本
    :return: 标准化后的文本
    """
    # 转小写
    text = text.lower()
    # 去除标点
    text = text.translate(str.maketrans('', '', string.punctuation))
    # 去除多余空格
    text = ' '.join(text.split())
    return text

# 示例
original = "I'm gonna make him an offer he can't refuse."
standardized = standardize_text(original)
print(f"原始: {original}")
print(f"标准化: {standardized}")

数据库设计与实现

数据库选择

对于老片经典台词查询系统,SQLite是一个理想的选择,因为它轻量级、无需服务器、易于部署。对于更大规模的应用,可以考虑PostgreSQL或MySQL。

数据表结构

我们需要设计两个主要表:movies(电影信息)和dialogues(台词内容)。

-- 电影信息表
CREATE TABLE movies (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    year INTEGER,
    director TEXT,
    genre TEXT,
    imdb_rating REAL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 台词表
CREATE TABLE dialogues (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    movie_id INTEGER NOT NULL,
    character_name TEXT,
    dialogue_text TEXT NOT NULL,
    start_time TEXT,
    end_time TEXT,
    scene_description TEXT,
    FOREIGN KEY (movie_id) REFERENCES movies(id)
);

-- 为常用查询字段创建索引
CREATE INDEX idx_dialogues_text ON dialogues(dialogue_text);
CREATE INDEX idx_dialogues_movie ON dialogues(movie_id);
CREATE INDEX idx_dialogues_character ON dialogues(character_name);
CREATE INDEX idx_movies_title ON movies(title);

使用Python操作SQLite

以下代码展示了如何使用Python的sqlite3模块创建数据库、插入数据和执行查询。

import sqlite3
from contextlib import closing

class MovieQuoteDB:
    def __init__(self, db_path='movie_quotes.db'):
        self.db_path = db_path
        self.init_database()
    
    def init_database(self):
        """初始化数据库,创建表"""
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            # 创建电影表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS movies (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT NOT NULL,
                    year INTEGER,
                    director TEXT,
                    genre TEXT,
                    imdb_rating REAL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            # 创建台词表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS dialogues (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    movie_id INTEGER NOT NULL,
                    character_name TEXT,
                    dialogue_text TEXT NOT NULL,
                    start_time TEXT,
                    end_time TEXT,
                    scene_description TEXT,
                    FOREIGN KEY (movie_id) REFERENCES movies(id)
                )
            ''')
            # 创建索引
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_dialogues_text ON dialogues(dialogue_text)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_dialogues_movie ON dialogues(movie_id)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_movies_title ON movies(title)')
            conn.commit()
    
    def add_movie(self, title, year=None, director=None, genre=None, imdb_rating=None):
        """添加电影信息,返回电影ID"""
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            cursor.execute('''
                INSERT INTO movies (title, year, director, genre, imdb_rating)
                VALUES (?, ?, ?, ?, ?)
            ''', (title, year, director, genre, imdb_rating))
            conn.commit()
            return cursor.lastrowid
    
    def add_dialogue(self, movie_id, character_name, dialogue_text, start_time=None, end_time=None, scene_description=None):
        """添加台词"""
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            cursor.execute('''
                INSERT INTO dialogues (movie_id, character_name, dialogue_text, start_time, end_time, scene_description)
                VALUES (?, ?, ?, ?, ?, ?)
            ''', (movie_id, character_name, dialogue_text, start_time, end_time, scene_description))
            conn.commit()
    
    def search_quotes(self, keyword, movie_title=None, character_name=None, limit=20):
        """
        搜索台词
        :param keyword: 搜索关键词
        :param movie_title: 可选,电影标题过滤
        :param character_name: 可选,角色名过滤
        :param limit: 返回结果数量限制
        :return: 包含台词信息的列表
        """
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            # 构建查询
            query = '''
                SELECT d.id, m.title, m.year, d.character_name, d.dialogue_text, d.start_time
                FROM dialogues d
                JOIN movies m ON d.movie_id = m.id
                WHERE d.dialogue_text LIKE ?
            '''
            params = [f'%{keyword}%']
            
            if movie_title:
                query += ' AND m.title LIKE ?'
                params.append(f'%{movie_title}%')
            
            if character_name:
                query += ' AND d.character_name LIKE ?'
                params.append(f'%{character_name}%')
            
            query += ' LIMIT ?'
            params.append(limit)
            
            cursor.execute(query, params)
            results = cursor.fetchall()
            
            # 格式化结果
            formatted_results = []
            for row in results:
                formatted_results.append({
                    'id': row[0],
                    'movie_title': row[1],
                    'year': row[2],
                    'character': row[3],
                    'dialogue': row[4],
                    'timestamp': row[5]
                })
            return formatted_results

# 示例使用
if __name__ == "__main__":
    db = MovieQuoteDB()
    
    # 添加电影《教父》
    godfather_id = db.add_movie("The Godfather", 1972, "Francis Ford Coppola", "Crime,Drama", 9.2)
    
    # 添加经典台词
    db.add_dialogue(godfather_id, "Don Corleone", "I'm gonna make him an offer he can't refuse.", "00:45:30,000", "00:45:32,500", "办公室谈判场景")
    db.add_dialogue(godfather_id, "Michael Corleone", "It's not personal, it's strictly business.", "01:15:10,000", "01:15:12,500", "餐厅复仇场景")
    
    # 搜索台词
    results = db.search_quotes("offer")
    for result in results:
        print(f"电影: {result['movie_title']} ({result['year']})")
        print(f"角色: {result['character']}")
        print(f"台词: {result['dialogue']}\n")

代码说明:

  1. MovieQuoteDB类封装了所有数据库操作。
  2. init_database方法创建必要的表和索引。
  3. add_movie和add_dialogue方法用于添加数据。
  4. search_quotes方法实现核心搜索功能,支持多条件过滤。
  5. 使用contextlib.closing确保数据库连接正确关闭。

检索引擎优化

基础搜索 vs 模糊搜索

基础的LIKE查询在处理大量数据时可能效率低下,且对拼写错误不友好。我们可以引入模糊搜索技术:

  1. 全文索引:SQLite的FTS5扩展提供高效的全文搜索。
  2. 模糊匹配算法:如Levenshtein距离、Jaro-Winkler距离。
  3. 同义词扩展:将”love”扩展为”adore, affection, fondness”等。

使用SQLite FTS5实现全文搜索

以下代码演示如何使用FTS5创建虚拟表并实现高效搜索。

class FTSQuoteDB(MovieQuoteDB):
    """扩展基础数据库类,支持全文搜索"""
    
    def init_database(self):
        super().init_database()
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            # 创建FTS5虚拟表
            cursor.execute('''
                CREATE VIRTUAL TABLE IF NOT EXISTS dialogues_fts 
                USING fts5(
                    dialogue_text,
                    character_name,
                    movie_title,
                    content=dialogues,
                    content_rowid=id
                )
            ''')
            # 创建触发器,保持FTS表与主表同步
            cursor.execute('''
                CREATE TRIGGER IF NOT EXISTS dialogues_ai AFTER INSERT ON dialogues BEGIN
                    INSERT INTO dialogues_fts(rowid, dialogue_text, character_name, movie_title)
                    SELECT id, dialogue_text, character_name, (SELECT title FROM movies WHERE id = dialogues.movie_id)
                    FROM dialogues WHERE id = new.id;
                END;
            ''')
            cursor.execute('''
                CREATE TRIGGER IF NOT EXISTS dialogues_ad AFTER DELETE ON dialogues BEGIN
                    DELETE FROM dialogues_fts WHERE rowid = old.id;
                END;
            ''')
            conn.commit()
    
    def add_dialogue(self, movie_id, character_name, dialogue_text, start_time=None, end_time=None, scene_description=None):
        """重写添加方法,确保FTS表同步"""
        # 先调用父类方法
        super().add_dialogue(movie_id, character_name, dialogue_text, start_time, end_time, scene_description)
        # FTS表通过触发器自动更新,无需手动操作
    
    def search_quotes_fts(self, keyword, limit=20):
        """
        使用FTS5进行全文搜索
        :param keyword: 搜索关键词
        :param limit: 结果限制
        :return: 搜索结果列表
        """
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            # 使用MATCH操作符进行全文搜索
            query = '''
                SELECT d.id, m.title, m.year, d.character_name, d.dialogue_text, d.start_time
                FROM dialogues_fts fts
                JOIN dialogues d ON fts.rowid = d.id
                JOIN movies m ON d.movie_id = m.id
                WHERE dialogues_fts MATCH ?
                ORDER BY rank
                LIMIT ?
            '''
            cursor.execute(query, (keyword, limit))
            results = cursor.fetchall()
            
            formatted_results = []
            for row in results:
                formatted_results.append({
                    'id': row[0],
                    'movie_title': row[1],
                    'year': row[2],
                    'character': row[3],
                    'dialogue': row[4],
                    'timestamp': row[5]
                })
            return formatted_results

# 示例使用
if __name__ == "__main__":
    db = FTSQuoteDB()
    
    # 添加更多数据
    casablanca_id = db.add_movie("Casablanca", 1942, "Michael Curtiz", "Romance,Drama", 8.5)
    db.add_dialogue(casablanca_id, "Rick Blaine", "Here's looking at you, kid.", "01:30:45,000", "01:30:47,500", "机场告别场景")
    db.add_dialogue(casablanca_id, "Rick Blaine", "We'll always have Paris.", "01:35:20,000", "01:35:22,500", "酒吧回忆场景")
    
    # 全文搜索
    results = db.search_quotes_fts("looking")
    for result in results:
        print(f"电影: {result['movie_title']} ({result['year']})")
        print(f"角色: {result['character']}")
        print(f"台词: {result['dialogue']}\n")

代码说明:

  1. FTSQuoteDB继承自MovieQuoteDB,扩展全文搜索功能。
  2. 创建FTS5虚拟表dialogues_fts,包含台词文本、角色名和电影标题。
  3. 使用触发器自动同步主表和FTS表的数据。
  4. search_quotes_fts方法使用MATCH操作符进行高效搜索,支持词干提取和排名。

模糊匹配算法

当用户输入可能有拼写错误时,模糊匹配非常有用。以下是一个基于Levenshtein距离的简单实现。

def levenshtein_distance(s1, s2):
    """
    计算两个字符串之间的Levenshtein距离
    :param s1: 字符串1
    :param s2: 字符串2
    :return: 编辑距离
    """
    if len(s1) < len(s2):
        return levenshtein_distance(s2, s1)
    
    if len(s2) == 0:
        return len(s1)
    
    previous_row = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            substitutions = previous_row[j] + (c1 != c2)
            current_row.append(min(insertions, deletions, substitutions))
        previous_row = current_row
    
    return previous_row[-1]

def fuzzy_search(keyword, dialogue_list, max_distance=2):
    """
    模糊搜索函数
    :param keyword: 搜索关键词
    :param dialogue_list: 台词列表(从数据库获取)
    :param max_distance: 最大允许距离
    :return: 匹配的台词列表
    """
    results = []
    for dialogue in dialogue_list:
        # 计算关键词与台词的编辑距离
        distance = levenshtein_distance(keyword.lower(), dialogue['dialogue'].lower())
        if distance <= max_distance:
            # 计算相似度分数(0-1)
            similarity = 1 - (distance / max(len(keyword), len(dialogue['dialogue'])))
            dialogue['similarity'] = similarity
            results.append(dialogue)
    
    # 按相似度排序
    results.sort(key=lambda x: x['similarity'], reverse=True)
    return results

# 示例使用
if __name__ == "__main__":
    # 模拟数据库查询结果
    sample_dialogues = [
        {'dialogue': "I'm gonna make him an offer he can't refuse.", 'movie_title': 'The Godfather'},
        {'dialogue': "Here's looking at you, kid.", 'movie_title': 'Casablanca'},
        {'dialogue': "We'll always have Paris.", 'movie_title': 'Casablanca'}
    ]
    
    # 模糊搜索"offr"(故意拼写错误)
    results = fuzzy_search("offr", sample_dialogues)
    for result in results:
        print(f"匹配度: {result['similarity']:.2f} - {result['movie_title']}: {result['dialogue']}")

代码说明:

  1. levenshtein_distance函数计算两个字符串的编辑距离。
  2. fuzzy_search函数将每个台词与关键词比较,计算相似度。
  3. 结果按相似度排序,返回最可能的匹配项。
  4. 这种方法适合在数据库查询结果基础上进行后处理,或在小规模数据集上使用。

Web API接口开发

使用Flask构建RESTful API

为了让系统更易用,我们可以通过Web API提供服务。Flask是一个轻量级的Python Web框架,适合快速开发。

from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
from contextlib import closing

app = Flask(__name__)
CORS(app)  # 允许跨域请求

class QuoteAPI:
    def __init__(self, db_path='movie_quotes.db'):
        self.db_path = db_path
    
    def search_quotes(self, keyword, movie=None, character=None, limit=20):
        """API使用的搜索方法"""
        with closing(sqlite3.connect(self.db_path)) as conn:
            cursor = conn.cursor()
            # 使用FTS5搜索
            query = '''
                SELECT d.id, m.title, m.year, d.character_name, d.dialogue_text, d.start_time
                FROM dialogues_fts fts
                JOIN dialogues d ON fts.rowid = d.id
                JOIN movies m ON d.movie_id = m.id
                WHERE dialogues_fts MATCH ?
            '''
            params = [keyword]
            
            # 添加过滤条件
            if movie:
                query += ' AND m.title LIKE ?'
                params.append(f'%{movie}%')
            if character:
                query += ' AND d.character_name LIKE ?'
                params.append(f'%{character}%')
            
            query += ' LIMIT ?'
            params.append(limit)
            
            cursor.execute(query, params)
            results = cursor.fetchall()
            
            return [{
                'id': row[0],
                'movie_title': row[1],
                'year': row[2],
                'character': row[3],
                'dialogue': row[4],
                'timestamp': row[5]
            } for row in results]

# 初始化API
api = QuoteAPI()

@app.route('/api/search', methods=['GET'])
def search():
    """
    搜索台词API
    参数:
        keyword: 搜索关键词(必填)
        movie: 电影标题过滤(可选)
        character: 角色名过滤(可选)
        limit: 结果数量限制(可选,默认20)
    """
    keyword = request.args.get('keyword')
    if not keyword:
        return jsonify({'error': 'keyword parameter is required'}), 400
    
    movie = request.args.get('movie')
    character = request.args.get('character')
    limit = int(request.args.get('limit', 20))
    
    try:
        results = api.search_quotes(keyword, movie, character, limit)
        return jsonify({
            'status': 'success',
            'count': len(results),
            'results': results
        })
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

@app.route('/api/random', methods=['GET'])
def random_quote():
    """随机获取一条台词"""
    try:
        with closing(sqlite3.connect(api.db_path)) as conn:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT d.id, m.title, m.year, d.character_name, d.dialogue_text, d.start_time
                FROM dialogues d
                JOIN movies m ON d.movie_id = m.id
                ORDER BY RANDOM()
                LIMIT 1
            ''')
            row = cursor.fetchone()
            if row:
                result = {
                    'id': row[0],
                    'movie_title': row[1],
                    'year': row[2],
                    'character': row[3],
                    'dialogue': row[4],
                    'timestamp': row[5]
                }
                return jsonify({'status': 'success', 'result': result})
            else:
                return jsonify({'status': 'error', 'message': 'No quotes found'}), 404
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

@app.route('/api/movies', methods=['GET'])
def list_movies():
    """获取所有电影列表"""
    try:
        with closing(sqlite3.connect(api.db_path)) as conn:
            cursor = conn.cursor()
            cursor.execute('SELECT id, title, year, director FROM movies ORDER BY year DESC')
            movies = [{'id': row[0], 'title': row[1], 'year': row[2], 'director': row[3]} for row in cursor.fetchall()]
            return jsonify({'status': 'success', 'movies': movies})
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

if __name__ == '__main__':
    # 确保数据库存在
    db = MovieQuoteDB()
    app.run(debug=True, host='0.0.0.0', port=5000)

代码说明:

  1. 使用Flask创建RESTful API,提供搜索、随机获取和电影列表接口。
  2. CORS中间件允许前端跨域调用。
  3. QuoteAPI类封装数据库操作,与Web层解耦。
  4. 参数验证和错误处理确保API稳定性。
  5. 使用RANDOM()函数实现随机台词获取。

API使用示例

启动服务后,可以通过以下方式调用API:

  1. 搜索台词:

    GET /api/search?keyword=offer&movie=Godfather&limit=5
    

    响应:

    {
     "status": "success",
     "count": 1,
     "results": [
       {
         "id": 1,
         "movie_title": "The Godfather",
         "year": 1972,
         "character": "Don Corleone",
         "dialogue": "I'm gonna make him an offer he can't refuse.",
         "timestamp": "00:45:30,000"
       }
     ]
    }
    
  2. 随机台词:

    GET /api/random
    

    响应:

    {
     "status": "success",
     "result": {
       "id": 2,
       "movie_title": "Casablanca",
       "year": 1942,
       "character": "Rick Blaine",
       "dialogue": "Here's looking at you, kid.",
       "timestamp": "01:30:45,000"
     }
    }
    
  3. 电影列表:

    GET /api/movies
    

    响应:

    {
     "status": "success",
     "movies": [
       {"id": 1, "title": "The Godfather", "year": 1972, "director": "Francis Ford Coppola"},
       {"id": 2, "title": "Casablanca", "year": 1942, "director": "Michael Curtiz"}
     ]
    }
    

用户界面设计

简单命令行界面

对于快速测试和原型开发,命令行界面(CLI)是最简单的方式。

def cli_interface():
    """命令行界面"""
    db = MovieQuoteDB()
    
    while True:
        print("\n" + "="*50)
        print("老片经典台词查询系统")
        print("="*50)
        print("1. 搜索台词")
        print("2. 随机台词")
        print("3. 电影列表")
        print("4. 退出")
        
        choice = input("\n请选择操作 (1-4): ").strip()
        
        if choice == '1':
            keyword = input("输入搜索关键词: ").strip()
            if keyword:
                results = db.search_quotes(keyword)
                if results:
                    print(f"\n找到 {len(results)} 条结果:")
                    for i, result in enumerate(results, 1):
                        print(f"\n{i}. 电影: {result['movie_title']} ({result['year']})")
                        print(f"   角色: {result['character']}")
                        print(f"   台词: {result['dialogue']}")
                        if result['timestamp']:
                            print(f"   时间: {result['timestamp']}")
                else:
                    print("未找到匹配的台词。")
        
        elif choice == '2':
            with closing(sqlite3.connect(db.db_path)) as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT d.id, m.title, m.year, d.character_name, d.dialogue_text, d.start_time
                    FROM dialogues d
                    JOIN movies m ON d.movie_id = m.id
                    ORDER BY RANDOM()
                    LIMIT 1
                ''')
                row = cursor.fetchone()
                if row:
                    print("\n随机台词:")
                    print(f"电影: {row[1]} ({row[2]})")
                    print(f"角色: {row[3]}")
                    print(f"台词: {row[4]}")
                    if row[5]:
                        print(f"时间: {row[5]}")
        
        elif choice == '3':
            with closing(sqlite3.connect(db.db_path)) as conn:
                cursor = conn.cursor()
                cursor.execute('SELECT title, year, director FROM movies ORDER BY year DESC')
                movies = cursor.fetchall()
                if movies:
                    print("\n电影列表:")
                    for i, (title, year, director) in enumerate(movies, 1):
                        print(f"{i}. {title} ({year}) - {director}")
                else:
                    print("暂无电影记录。")
        
        elif choice == '4':
            print("感谢使用,再见!")
            break
        
        else:
            print("无效选择,请重新输入。")

if __name__ == "__main__":
    cli_interface()

Web前端界面(HTML + JavaScript)

对于更友好的用户体验,可以构建一个简单的Web界面。以下是一个完整的HTML文件示例,包含搜索、展示和随机台词功能。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>老片经典台词查询</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
            color: #333;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            text-align: center;
            color: #2c3e50;
            margin-bottom: 30px;
        }
        .search-box {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        input, select, button {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
        }
        input[type="text"] {
            flex: 1;
            min-width: 200px;
        }
        button {
            background-color: #3498db;
            color: white;
            border: none;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #2980b9;
        }
        button.secondary {
            background-color: #95a5a6;
        }
        button.secondary:hover {
            background-color: #7f8c8d;
        }
        .results {
            margin-top: 20px;
        }
        .quote-item {
            background: #f8f9fa;
            padding: 15px;
            margin-bottom: 15px;
            border-radius: 5px;
            border-left: 4px solid #3498db;
        }
        .quote-item .movie {
            font-weight: bold;
            color: #2c3e50;
            margin-bottom: 5px;
        }
        .quote-item .character {
            color: #7f8c8d;
            font-style: italic;
            margin-bottom: 8px;
        }
        .quote-item .dialogue {
            font-size: 1.1em;
            line-height: 1.5;
        }
        .quote-item .timestamp {
            color: #95a5a6;
            font-size: 0.9em;
            margin-top: 5px;
        }
        .loading {
            text-align: center;
            color: #7f8c8d;
            padding: 20px;
        }
        .error {
            background: #e74c3c;
            color: white;
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 10px;
        }
        .stats {
            background: #e8f4f8;
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 15px;
            text-align: center;
        }
        .filters {
            display: flex;
            gap: 10px;
            margin-bottom: 10px;
            flex-wrap: wrap;
        }
        .filters input {
            flex: 1;
            min-width: 150px;
        }
        .random-section {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 5px;
            margin-top: 20px;
            text-align: center;
        }
        .random-section h3 {
            margin-top: 0;
            color: #2c3e50;
        }
        .random-quote {
            font-size: 1.2em;
            margin: 15px 0;
            padding: 15px;
            background: white;
            border-radius: 5px;
            border-left: 4px solid #e74c3c;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🎬 老片经典台词查询</h1>
        
        <!-- 搜索区域 -->
        <div class="search-section">
            <div class="search-box">
                <input type="text" id="keyword" placeholder="输入关键词(如:offer, love, Paris)" autofocus>
                <button onclick="searchQuotes()">搜索台词</button>
                <button class="secondary" onclick="getRandomQuote()">随机台词</button>
            </div>
            
            <div class="filters">
                <input type="text" id="movieFilter" placeholder="电影标题过滤(可选)">
                <input type="text" id="characterFilter" placeholder="角色名过滤(可选)">
                <select id="limitSelect">
                    <option value="10">10条</option>
                    <option value="20" selected>20条</option>
                    <option value="50">50条</option>
                    <option value="100">100条</option>
                </select>
            </div>
        </div>

        <!-- 统计信息 -->
        <div id="stats" class="stats" style="display: none;"></div>

        <!-- 错误信息 -->
        <div id="error" class="error" style="display: none;"></div>

        <!-- 结果区域 -->
        <div id="results" class="results"></div>

        <!-- 随机台词区域 -->
        <div id="randomSection" class="random-section" style="display: none;">
            <h3>🎲 随机经典台词</h3>
            <div id="randomQuote" class="random-quote"></div>
            <button onclick="getRandomQuote()">再换一条</button>
        </div>
    </div>

    <script>
        const API_BASE = 'http://localhost:5000/api';
        
        // 显示错误信息
        function showError(message) {
            const errorDiv = document.getElementById('error');
            errorDiv.textContent = message;
            errorDiv.style.display = 'block';
            setTimeout(() => errorDiv.style.display = 'none', 5000);
        }

        // 显示统计信息
        function showStats(count) {
            const statsDiv = document.getElementById('stats');
            statsDiv.textContent = `找到 ${count} 条台词`;
            statsDiv.style.display = 'block';
        }

        // 渲染搜索结果
        function renderResults(results) {
            const resultsDiv = document.getElementById('results');
            resultsDiv.innerHTML = '';
            
            if (results.length === 0) {
                resultsDiv.innerHTML = '<div class="loading">未找到匹配的台词</div>';
                return;
            }

            results.forEach(quote => {
                const item = document.createElement('div');
                item.className = 'quote-item';
                item.innerHTML = `
                    <div class="movie">${quote.movie_title} (${quote.year})</div>
                    <div class="character">${quote.character || '未知角色'}</div>
                    <div class="dialogue">${quote.dialogue}</div>
                    ${quote.timestamp ? `<div class="timestamp">⏱️ ${quote.timestamp}</div>` : ''}
                `;
                resultsDiv.appendChild(item);
            });
        }

        // 渲染随机台词
        function renderRandomQuote(quote) {
            const randomSection = document.getElementById('randomSection');
            const randomQuoteDiv = document.getElementById('randomQuote');
            
            randomQuoteDiv.innerHTML = `
                <div class="movie">${quote.movie_title} (${quote.year})</div>
                <div class="character">${quote.character || '未知角色'}</div>
                <div class="dialogue">${quote.dialogue}</div>
                ${quote.timestamp ? `<div class="timestamp">⏱️ ${quote.timestamp}</div>` : ''}
            `;
            randomSection.style.display = 'block';
        }

        // 搜索台词
        async function searchQuotes() {
            const keyword = document.getElementById('keyword').value.trim();
            const movie = document.getElementById('movieFilter').value.trim();
            const character = document.getElementById('characterFilter').value.trim();
            const limit = document.getElementById('limitSelect').value;

            if (!keyword) {
                showError('请输入搜索关键词');
                return;
            }

            const resultsDiv = document.getElementById('results');
            resultsDiv.innerHTML = '<div class="loading">搜索中...</div>';

            try {
                // 构建URL参数
                const params = new URLSearchParams();
                params.append('keyword', keyword);
                if (movie) params.append('movie', movie);
                if (character) params.append('character', character);
                params.append('limit', limit);

                const response = await fetch(`${API_BASE}/search?${params}`);
                const data = await response.json();

                if (data.status === 'success') {
                    renderResults(data.results);
                    showStats(data.count);
                } else {
                    showError(data.message || '搜索失败');
                }
            } catch (error) {
                showError('网络错误: ' + error.message);
            }
        }

        // 获取随机台词
        async function getRandomQuote() {
            const resultsDiv = document.getElementById('results');
            resultsDiv.innerHTML = '';
            document.getElementById('stats').style.display = 'none';

            try {
                const response = await fetch(`${API_BASE}/random`);
                const data = await response.json();

                if (data.status === 'success') {
                    renderRandomQuote(data.result);
                } else {
                    showError(data.message || '获取失败');
                }
            } catch (error) {
                showError('网络错误: ' + error.message);
            }
        }

        // 回车键支持
        document.getElementById('keyword').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                searchQuotes();
            }
        });

        // 页面加载时获取随机台词
        window.addEventListener('load', function() {
            // 可选:自动加载随机台词
            // getRandomQuote();
        });
    </script>
</body>
</html>

使用说明:

  1. 将上述HTML代码保存为index.html文件。
  2. 确保Flask API服务正在运行(python app.py)。
  3. 在浏览器中打开index.html文件。
  4. 输入关键词进行搜索,或点击”随机台词”按钮。

高级功能扩展

1. 情感分析

使用自然语言处理技术为台词添加情感标签,如积极、消极、中性、幽默等。

from textblob import TextBlob
import nltk

# 下载必要的NLTK数据(首次运行需要)
# nltk.download('vader_lexicon')
# nltk.download('punkt')

def analyze_sentiment(text):
    """
    使用TextBlob进行情感分析
    :param text: 台词文本
    :return: 情感标签
    """
    analysis = TextBlob(text)
    polarity = analysis.sentiment.polarity
    
    if polarity > 0.3:
        return "积极"
    elif polarity < -0.3:
        return "消极"
    else:
        return "中性"

# 示例
print(analyze_sentiment("I love this beautiful day!"))  # 积极
print(analyze_sentiment("I hate this terrible situation."))  # 消极
print(analyze_sentiment("The weather is okay."))  # 中性

2. 台词关联分析

分析台词之间的关联,找出经常一起出现的短语或主题。

from collections import defaultdict
import re

def find_common_phrases(dialogues, min_length=2, min_count=3):
    """
    找出常见的短语组合
    :param dialogues: 台词列表
    :param min_length: 最小单词数
    :param min_count: 最小出现次数
    :return: 常见短语字典
    """
    phrase_count = defaultdict(int)
    
    for dialogue in dialogues:
        # 移除标点,转小写
        text = re.sub(r'[^\w\s]', '', dialogue.lower())
        words = text.split()
        
        # 生成2-3个词的短语
        for i in range(len(words) - min_length + 1):
            for j in range(min_length, min_length + 2):
                if i + j <= len(words):
                    phrase = ' '.join(words[i:i+j])
                    phrase_count[phrase] += 1
    
    # 过滤高频短语
    common_phrases = {phrase: count for phrase, count in phrase_count.items() 
                     if count >= min_count and len(phrase.split()) >= min_length}
    
    return dict(sorted(common_phrases.items(), key=lambda x: x[1], reverse=True))

# 示例
dialogues = [
    "I'm gonna make him an offer he can't refuse.",
    "It's not personal, it's strictly business.",
    "Here's looking at you, kid.",
    "We'll always have Paris.",
    "I'm gonna make him an offer."
]

print(find_common_phrases(dialogues))

3. 语音合成(TTS)

使用文本转语音技术让台词”说出来”。

import pyttsx3

def speak_quote(text, rate=150):
    """
    使用pyttsx3朗读台词
    :param text: 台词文本
    :param rate: 语速
    """
    engine = pyttsx3.init()
    engine.setProperty('rate', rate)
    engine.say(text)
    engine.runAndWait()

# 示例
if __name__ == "__main__":
    speak_quote("I'm gonna make him an offer he can't refuse.")

部署与维护

部署选项

  1. 本地部署:适合个人使用,简单快捷。
  2. 云服务器:使用AWS、阿里云等,适合公开服务。
  3. Serverless:使用AWS Lambda或阿里云函数计算,按需付费。

数据备份与恢复

定期备份数据库至关重要:

import shutil
from datetime import datetime

def backup_database(db_path, backup_dir='backups'):
    """备份数据库"""
    import os
    if not os.path.exists(backup_dir):
        os.makedirs(backup_dir)
    
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_file = os.path.join(backup_dir, f'backup_{timestamp}.db')
    shutil.copy2(db_path, backup_file)
    print(f"备份成功: {backup_file}")

# 定时任务示例(Linux crontab)
# 0 2 * * * /usr/bin/python3 /path/to/backup_script.py

性能监控

使用简单的日志记录监控系统性能:

import logging
import time

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('query_log.txt'),
        logging.StreamHandler()
    ]
)

def log_query_performance(func):
    """装饰器:记录查询性能"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        duration = end_time - start_time
        logging.info(f"查询耗时: {duration:.4f}秒 - 参数: {args}, {kwargs}")
        return result
    return wrapper

# 应用装饰器
@log_query_performance
def search_quotes(self, keyword, movie=None, character=None, limit=20):
    # 原有搜索逻辑
    pass

总结

本文详细介绍了如何构建一个老片经典台词查询系统,从数据收集、处理、存储到检索和展示的完整流程。我们提供了丰富的Python代码示例,包括:

  1. 数据收集:从SRT文件解析台词,文本标准化。
  2. 数据库设计:SQLite表结构和索引优化。
  3. 检索引擎:基础搜索、全文索引(FTS5)和模糊匹配算法。
  4. Web API:使用Flask构建RESTful接口。
  5. 用户界面:命令行和Web前端实现。
  6. 高级功能:情感分析、关联分析和语音合成。
  7. 部署维护:备份、监控和性能优化。

这个系统可以根据需求灵活扩展,例如添加更多电影、集成机器学习模型进行智能推荐,或开发移动应用。通过本文的指导,读者可以快速搭建一个功能完善的台词查询工具,重温经典电影的魅力。

下一步建议:

  • 收集更多电影数据,丰富台词库。
  • 实现用户认证和收藏功能。
  • 添加相似台词推荐系统。
  • 开发浏览器插件,实现网页上的即时台词查询。

希望本文能帮助您成功构建自己的老片经典台词查询系统!