← 项目列表
课程已完成2026年5月

NarrativeAnalysis — 大数据电影叙事分析

基于 Hadoop 生态的大数据电影叙事分析——从剧本清洗、情感计算到 HBase 存储、API 查询的全链路实现。

HadoopHBasePythonFlaskDocker大数据PythonFlaskHadoopHBaseDocker
📋 项目概览
技术栈
PythonFlaskHadoopHBaseDocker
功能特性
  • 情感词典驱动的剧本情感分析(17 部电影,80720 行清洗数据)
  • 角色共现网络构建与场景分割
  • 数据管道 Pipeline(预处理 → 情感分析 → HBase 写入 → API)
  • RESTful API 后端服务(Flask)
  • HBase 三表存储(movie_emotion / character_network / narrative_pattern)
📖 技术分析报告在 GitHub 查看 ↗

大数据叙事分析系统

数据管道来源:本系统的数据清洗与预处理管道来源于课程 HDFS 数据处理 Pipeline(作业一),本项目的 data_pipeline.py 在其基础上扩展了完整的系统集成。

项目概述

基于 Hadoop + HBase 的大规模电影剧本叙事分析系统。对多部电影剧本进行情感计算、角色网络分析和叙事模式提取,结果存入 HBase 并通过 Flask API 暴露给前端可视化展示。这是一个完整的大数据应用 demo:数据管道 -> 分布式存储 -> API 服务 -> Web 前端。

架构设计

系统包含五个组件:

数据管道(data_pipeline.py):从清洗后的剧本文件出发,加载情感词典,逐剧本分析场景情感、角色共现和叙事模式。结果以 JSON 格式输出到 api_data/ 目录,同时支持写入 HBase。

HBase 存储(hbase_schema.sh):创建三张表——movie_emotion(场景情感序列)、character_network(角色共现矩阵)、narrative_pattern(叙事模式指纹)。表结构围绕列族设计,每张表的 RowKey 为电影 ID。

Flask API(api/app.py):提供 RESTful 接口,从 HBase 读取分析数据返回 JSON 或直接读取本地 JSON 文件作为 fallback。支持按电影 ID 查询、列表查询等端点。

前端展示(frontend/index.html):可视化展示情感曲线、角色关系网络和叙事模式对比,作为数据分析结果的直观呈现。

集群启动(cluster/start-cluster.sh):一键启动 Hadoop + HBase 集群的脚本,支持 Docker 环境。

技术亮点

多级数据管道设计:Pipeline 支持三种模式(full/api/hbase),数据经过预处理 -> 情感分析 -> 结构化 JSON -> HBase 导入四个阶段。每一阶段产物可独立使用,JSON 文件可直接供前端展示,HBase 存储用于大规模查询。

情感时序分析:逐场景计算净情感值(pos-neg)/总词数,形成贯穿全剧的情感变化曲线。用于分析剧本的叙事节奏——情感曲线在剧情高潮、反转和结尾阶段呈现明显波动。

叙事模式聚类准备:每部电影提取情感均值、标准差、场景数、角色数等特征构成叙事指纹,后续可基于这些向量进行剧本聚类分析,发现叙事风格的相似性。

设计决策

HBase 列族设计遵循查询模式驱动原则:emotion 列族支持场景级情感范围扫描,cooccur 列族支持角色对查询,features 列族支持叙事特征向量提取。三张表分别服务于不同的分析视角,避免单表列族过多带来的性能问题。

Flask API 同时支持 HBase 和本地 JSON 两种数据源,使得前端开发可以不依赖 Hadoop 集群,本地 JSON 作为开发数据源,HBase 作为生产数据源。

关键代码解读

def analyze_script(filepath, movie_id, movie_name, word_dict):
    # 场景分割 -> 情感计算 -> 角色共现
    for line in lines:
        if stripped.startswith('[SCENE]'):
            # 场景切换
        elif re.match(r'^[A-Z][A-Z\s\.]{1,30}$', stripped):
            # 角色识别
    # 情感归一化
    net_emotion = round((pos_count - neg_count) / max(total_words, 1), 6)

剧本分析引擎同时完成场景分割、角色识别和情感计算三项任务。一次遍历完成所有分析,时间复杂度 O(n) 且空间占用仅与角色数量和场景数相关,与剧本长度无关。

🚀 在线演示新窗口打开 ↗
💻 核心代码Python · Flask · Hadoop · HBase · Docker
python情感分析数据管道data_pipeline.py · 127 行

预处理管道:加载情感词典 → 分割场景 → 计算情感得分 → 构建角色共现 → HBase 写入。

# ── 情感分析核心 ──
def analyze_script(filepath, movie_id, movie_name, word_dict):
    """分析单部剧本,返回结构化的情感 + 角色数据"""
    with open(filepath, 'r', encoding='utf-8') as f:
        text = f.read()

    lines = text.split('\n')
    scenes = []
    current_scene = {'scene_id': 0, 'name': 'UNKNOWN',
                     'lines': [], 'characters': set()}
    scene_counter = 0
    all_characters = set()
    char_scene_map = defaultdict(set)

    for line in lines:
        stripped = line.strip()
        # 识别场景标记 [SCENE]
        if stripped.startswith('[SCENE]'):
            if current_scene['lines']:
                scenes.append(current_scene)
            scene_counter += 1
            current_scene = {
                'scene_id': scene_counter,
                'name': stripped.replace('[SCENE] ', '').strip(),
                'lines': [], 'characters': set()
            }
        # 识别角色名(全大写缩写)
        elif stripped and re.match(r'^[A-Z][A-Z\s\.]{1,30}$', stripped):
            char_name = stripped.strip()
            all_characters.add(char_name)
            current_scene['characters'].add(char_name)
            char_scene_map[char_name].add(current_scene['scene_id'])
        else:
            current_scene['lines'].append(stripped)

    if current_scene['lines']:
        scenes.append(current_scene)

    # 无场景标记时按窗口分割
    if len(scenes) <= 1:
        scenes = []
        window_size = 50
        for i in range(0, len(lines), window_size):
            ...  # windowed fallback

    # ── 情感分析 ──
    emotion_results = []
    for scene in scenes:
        pos_count = 0
        neg_count = 0
        total_words = 0
        for line in scene['lines']:
            words = line.lower().split()
            total_words += len(words)
            for w in words:
                w_clean = re.sub(r'[^a-z]', '', w)
                if w_clean in word_dict:
                    p, n = word_dict[w_clean]
                    pos_count += p
                    neg_count += n

        net_emotion = round((pos_count - neg_count) / max(total_words, 1), 6)
        emotion_results.append({
            'scene': scene['scene_id'],
            'name': scene['name'],
            'pos': pos_count, 'neg': neg_count,
            'net': net_emotion
        })

    # ── 角色共现 ──
    cooccur = defaultdict(int)
    for scene in scenes:
        chars = list(scene['characters'])
        for i in range(len(chars)):
            for j in range(i + 1, len(chars)):
                pair = tuple(sorted([chars[i], chars[j]]))
                cooccur[pair] += 1

    return {
        'id': movie_id, 'name': movie_name,
        'total_scenes': len(scenes),
        'total_characters': len(all_characters),
        'scenes': emotion_results,
        'cooccurrences': [
            {'charA': a, 'charB': b, 'count': c}
            for (a, b), c in sorted(cooccur.items(), key=lambda x: -x[1])[:30]
        ]
    }


# ── HBase 写入 ──
def write_to_hbase(movies_data):
    """将分析结果写入 HBase(3 张表)"""
    connection = happybase.Connection('localhost', 9090)
    table_emotion = connection.table('movie_emotion')
    table_network = connection.table('character_network')
    table_pattern = connection.table('narrative_pattern')

    for movie in movies_data:
        mid = movie['id']
        # movie_emotion: 场景情感序列
        table_emotion.put(mid, {
            'emotion:scenes': json.dumps(movie['scenes']),
            'meta:name': movie['name'],
            'meta:scenes': str(movie['total_scenes']),
            'meta:characters': str(movie['total_characters']),
        })
        # character_network: 角色共现关系
        for c in movie['cooccurrences']:
            pair_key = f"{c['charA']}#{c['charB']}"
            table_network.put(mid, {f"cooccur:{pair_key}": str(c['count'])})
        # narrative_pattern: 叙事模式指纹
        emotion_series = [s['net'] for s in movie['scenes']]
        mean_emo = sum(emotion_series) / max(len(emotion_series), 1)
        std_emo = (sum((e - mean_emo)**2 for e in emotion_series) / max(len(emotion_series), 1))**0.5
        pattern = {
            'acts': len(movie['scenes']) // 10 + 1,
            'emotion_mean': round(mean_emo, 6),
            'emotion_std': round(std_emo, 6),
            'scene_count': movie['total_scenes'],
            'char_count': movie['total_characters'],
        }
        table_pattern.put(mid, {
            'features:fingerprint': json.dumps(pattern),
            'acts:count': str(pattern['acts']),
        })
    connection.close()
pythonFlask API 后端app.py · 47 行

RESTful API 服务,提供电影列表、情感曲线、角色共现等查询接口。

@app.route('/movies', methods=['GET'])
def list_movies():
    """获取所有电影基本信息列表"""
    data = load_all_movies()
    movies = [{
        'id': m['id'], 'name': m['name'],
        'total_scenes': m['total_scenes'],
        'total_characters': m['total_characters'],
    } for m in data.get('movies', [])]
    return jsonify({'count': len(movies), 'movies': movies})


@app.route('/movies/<movie_id>/emotion', methods=['GET'])
def get_emotion_curve(movie_id):
    """获取电影情感曲线(含转折点检测)"""
    movie = get_movie_by_id(movie_id)
    if movie is None:
        return jsonify({'error': f'Movie not found: {movie_id}'}), 404

    scenes = movie.get('scenes', [])
    emotion_curve = [{
        'scene': s['scene'], 'name': s['name'],
        'positive': s['pos'], 'negative': s['neg'],
        'net_emotion': s['net'],
    } for s in scenes]

    # 检测叙事转折点(ECR 简化版)
    turning_points = []
    for i in range(1, len(scenes) - 1):
        prev_net = scenes[i - 1]['net']
        curr_net = scenes[i]['net']
        next_net = scenes[i + 1]['net']
        # 局部极值 = 情感趋势转折
        if (curr_net > prev_net and curr_net > next_net) or \
           (curr_net < prev_net and curr_net < next_net):
            turning_points.append({
                'scene': scenes[i]['scene'],
                'net_emotion': curr_net,
                'type': 'peak' if curr_net > prev_net else 'valley'
            })

    return jsonify({
        'movie_id': movie_id,
        'emotion_curve': emotion_curve,
        'turning_points': turning_points,
        'total_scenes': len(scenes),
    })
📁 源文件清单GitHub 仓库 ↗
路径说明行数
scripts/data_pipeline.py数据管道(预处理→情感分析→HBase)315
api/app.pyFlask 后端 API 服务180
scripts/preprocess.py剧本清洗预处理85
api/requirements.txtPython 依赖5
cluster/docker-compose.ymlHadoop 集群 Docker 配置40
cluster/start-cluster.sh集群启动脚本55
frontend/index.html前端可视化页面320
网站智能助手
💬 和我聊聊
🤖
你好呀 👋 有什么想聊的?