Python 大数据毕业设计:电影票房可视化分析系统(Flask+Echarts + 爬虫实战)

在大数据与影视行业深度融合的背景下,电影票房数据的分析与可视化成为热门选题。本文将手把手教你搭建一套完整的电影票房可视化分析系统,通过 Python 爬虫采集权威票房数据,利用 Flask 框架构建 Web 服务,结合 Echarts 实现多维度数据可视化,支持用户注册登录、档期筛选、地域分布分析等核心功能,适合作为计算机专业毕业设计或课程实践项目。

一、项目整体介绍

1.1 项目背景与价值

随着影视行业的蓬勃发展,票房数据成为衡量电影市场表现的核心指标。本系统通过采集主流票房平台数据,实现多维度可视化分析,帮助用户快速洞察不同档期、不同影片的票房表现、地域分布特征及市场占比情况,为影视投资者、发行方及爱好者提供数据支撑。

1.2 技术栈选型

技术模块 核心技术 作用说明
数据采集 Python+requests+BeautifulSoup 爬取权威票房平台数据,包括影片票房、场次、人次、地域分布等信息
数据存储 MySQL + 数据库连接池 高效存储结构化数据,支持批量插入与快速查询
Web 框架 Flask+Jinja2 模板引擎 构建用户交互界面,实现登录注册、数据筛选、可视化展示等功能
可视化展示 Echarts+HTML/CSS/JavaScript 生成折线图、饼图、柱状图等,直观呈现多维度数据
辅助工具 datetime+json+logging 处理时间格式、解析接口数据、记录系统日志

1.3 核心功能模块

系统分为前台用户交互后台管理两大模块,功能结构如下:

前台功能
  • 用户注册 / 登录:实现账号认证,未登录用户无法访问核心功能
  • 数据筛选:支持按年份(2022-2024)、档期(春节档 / 五一档 / 暑期档等)、影片名称筛选数据
  • 可视化分析:
    • 趋势分析:票房 / 场次 / 人次的时间变化趋势(折线图)
    • 占比分析:单档期内各影片的票房 / 场次 / 人次占比(饼图)
    • 地域分析:影片在一线 / 二线 / 三线等城市的分布情况(柱状图)
  • 数据导出:支持将分析结果导出为 Excel 文件(拓展功能)
后台功能
  • 用户管理:查看注册用户列表,支持用户账号启用 / 禁用操作
  • 数据管理:手动同步最新票房数据,支持数据增删改查维护
  • 日志管理:记录用户操作日志与数据采集日志(拓展功能)

二、环境搭建与准备

2.1 开发环境配置

  1. 基础环境:Python 3.8+(推荐 3.9 版本,兼容性更佳)
  2. 依赖库安装:

bash

运行

bash 复制代码
# 核心依赖库
pip install flask requests beautifulsoup4 pymysql flask-sqlalchemy
pip install pandas openpyxl  # 数据处理与Excel导出
pip install logging  # 日志记录
  1. 数据库准备:
    • 安装 MySQL 8.0+,创建数据库movie_box_office_db
    • 执行以下 SQL 创建核心数据表(用户表 + 票房数据表 + 地域分布表):

sql

sql 复制代码
-- 用户表
CREATE TABLE `user` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `username` VARCHAR(50) NOT NULL UNIQUE,
  `password` VARCHAR(100) NOT NULL,
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `status` TINYINT DEFAULT 1 COMMENT '1-正常 0-禁用'
);

-- 票房数据表
CREATE TABLE `movie_box_office` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `thedate` DATE NOT NULL COMMENT '数据日期',
  `rank` INT COMMENT '当日排名',
  `movie_id` VARCHAR(20) NOT NULL COMMENT '影片ID',
  `movie_name` VARCHAR(100) NOT NULL COMMENT '影片名称',
  `box_office` BIGINT COMMENT '当日票房(元)',
  `total_box_office` BIGINT COMMENT '累计票房(元)',
  `total_show_count` INT COMMENT '累计场次',
  `total_audience_count` INT COMMENT '累计人次',
  `box_office_percent` DECIMAL(5,2) COMMENT '票房占比(%)',
  `show_count_percent` DECIMAL(5,2) COMMENT '场次占比(%)',
  `audience_count_percent` DECIMAL(5,2) COMMENT '人次占比(%)',
  `release_date` DATE COMMENT '上映日期'
);

-- 地域分布表
CREATE TABLE `movie_area_distribution` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `thedate` DATE NOT NULL COMMENT '数据日期',
  `movie_id` VARCHAR(20) NOT NULL COMMENT '影片ID',
  `movie_name` VARCHAR(100) NOT NULL COMMENT '影片名称',
  `city_level` VARCHAR(20) NOT NULL COMMENT '城市级别(一线/二线/三线/四线/其它)',
  `box_office` BIGINT COMMENT '当日票房(元)',
  `show_count` INT COMMENT '当日场次',
  `audience_count` INT COMMENT '当日人次'
);

2.2 项目目录结构

plaintext

python 复制代码
movie_analysis_system/
├── app.py                # 项目入口文件(Flask核心配置)
├── config.py             # 配置文件(数据库连接、密钥等)
├── spider/               # 爬虫模块
│   ├── __init__.py
│   ├── box_office_spider.py  # 票房数据采集脚本
│   └── utils.py          # 爬虫工具函数(请求头、数据清洗)
├── models/               # 数据模型
│   ├── __init__.py
│   ├── user_model.py     # 用户相关操作
│   └── data_model.py     # 数据存储与查询操作
├── static/               # 静态资源
│   ├── css/              # 样式文件
│   ├── js/               # JavaScript文件(Echarts核心)
│   └── images/           # 图片资源
├── templates/            # 前端模板
│   ├── login.html        # 登录页面
│   ├── register.html     # 注册页面
│   ├── index.html        # 可视化主页面
│   └── admin/            # 后台管理页面
├── utils/                # 通用工具
│   ├── __init__.py
│   ├── db_pool.py        # 数据库连接池
│   └── excel_utils.py    # Excel导出工具
└── logs/                 # 日志文件

三、核心模块实现

3.1 数据采集模块(爬虫实现)

本模块通过 requests 库采集权威票房平台数据(以艺恩数据为例),包含票房详情与地域分布数据,支持指定日期范围批量采集。

3.1.1 爬虫工具配置(spider/utils.py)

python

运行

python 复制代码
import requests
from fake_useragent import UserAgent

# 生成随机请求头,避免被反爬
def get_headers():
    ua = UserAgent()
    return {
        'User-Agent': ua.random,
        'Referer': 'https://ys.endata.cn/',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Accept': 'application/json, text/javascript, */*; q=0.01'
    }

# 通用请求函数
def send_request(url, data=None, method='POST'):
    headers = get_headers()
    try:
        if method == 'POST':
            response = requests.post(url, headers=headers, data=data, timeout=10)
        else:
            response = requests.get(url, headers=headers, params=data, timeout=10)
        response.raise_for_status()  # 抛出HTTP错误
        return response.json()
    except Exception as e:
        print(f"请求失败:{str(e)}")
        return None
3.1.2 票房数据采集(spider/box_office_spider.py)

python

运行

python 复制代码
import datetime
from spider.utils import send_request
from utils.db_pool import MySQLPool

# 数据库连接
db = MySQLPool()

def get_daily_box_office(date):
    """采集指定日期的票房详情数据"""
    url = 'https://ys.endata.cn/enlib-api/api/movie/getMovie_BoxOffice_Day_List.do'
    data = {
        'r': '0.6690016180153391',
        'datetype': 'Day',
        'date': date,
        'sdate': date,
        'edate': date,
        'bserviceprice': '1',
        'pageindex': '1',
        'pagesize': '200',
        'order': '103',
        'ordertype': 'desc'
    }
    
    response = send_request(url, data=data)
    if not response or 'data' not in response:
        return
    
    movie_list = response['data']['table1']
    box_office_data = []
    for movie in movie_list:
        item = {
            'thedate': date,
            'rank': movie.get('Irank'),
            'movie_id': movie.get('MovieID'),
            'movie_name': movie.get('MovieName'),
            'box_office': movie.get('BoxOffice'),
            'total_box_office': movie.get('TotalBoxOffice'),
            'total_show_count': movie.get('TotalShowCount'),
            'total_audience_count': movie.get('TotalAudienceCount'),
            'box_office_percent': movie.get('BoxOfficePercent'),
            'show_count_percent': movie.get('ShowCountPercent'),
            'audience_count_percent': movie.get('AudienceCountPercent'),
            'release_date': movie.get('ReleaseDate')
        }
        box_office_data.append(item)
    
    # 批量插入数据库
    if box_office_data:
        db.batch_insert('movie_box_office', box_office_data)
        print(f"成功采集{date}票房数据,共{len(box_office_data)}部影片")

def get_area_distribution(date):
    """采集指定日期的地域分布数据"""
    url = 'https://ys.endata.cn/enlib-api/api/movie/getMovie_BoxOffice_Day_Chart.do'
    data = {
        'r': '0.6690016180153391',
        'datetype': 'Day',
        'date': date,
        'sdate': date,
        'edate': date,
        'bserviceprice': '1'
    }
    
    response = send_request(url, data=data)
    if not response or 'data' not in response:
        return
    
    area_list = response['data']['table1']
    area_data = []
    for area in area_list:
        item = {
            'thedate': date,
            'movie_id': area.get('MovieID'),
            'movie_name': area.get('MovieName'),
            'city_level': area.get('CityLevel'),
            'box_office': area.get('BoxOffice'),
            'show_count': area.get('ShowCount'),
            'audience_count': area.get('AudienceCount')
        }
        area_data.append(item)
    
    # 批量插入数据库
    if area_data:
        db.batch_insert('movie_area_distribution', area_data)
        print(f"成功采集{date}地域分布数据,共{len(area_data)}条记录")

def batch_collect_data(start_date, end_date):
    """批量采集指定日期范围的数据"""
    # 转换日期格式
    start = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
    end = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
    delta = datetime.timedelta(days=1)
    
    current_date = start
    while current_date <= end:
        date_str = current_date.strftime('%Y-%m-%d')
        get_daily_box_office(date_str)
        get_area_distribution(date_str)
        current_date += delta

if __name__ == '__main__':
    # 采集2023年春节档数据(1月21日-1月27日)
    batch_collect_data('2023-01-21', '2023-01-27')
3.1.3 数据库连接池(utils/db_pool.py)

python

运行

python 复制代码
import pymysql
from pymysql.err import OperationalError
from config import DB_CONFIG

class MySQLPool:
    def __init__(self):
        self.pool = self.create_pool()
    
    def create_pool(self):
        """创建数据库连接池"""
        return pymysql.connect(
            host=DB_CONFIG['host'],
            port=DB_CONFIG['port'],
            user=DB_CONFIG['user'],
            password=DB_CONFIG['password'],
            database=DB_CONFIG['db_name'],
            charset='utf8mb4',
            autocommit=True
        )
    
    def get_connection(self):
        """获取连接(自动重连)"""
        try:
            if self.pool.ping():
                return self.pool
        except OperationalError:
            self.pool = self.create_pool()
        return self.pool
    
    def batch_insert(self, table, data_list):
        """批量插入数据"""
        if not data_list:
            return
        
        conn = self.get_connection()
        cursor = conn.cursor()
        
        # 获取字段名和占位符
        fields = list(data_list[0].keys())
        placeholders = ','.join(['%s'] * len(fields))
        sql = f"INSERT INTO {table} ({','.join(fields)}) VALUES ({placeholders})"
        
        # 处理数据(None值转换为NULL)
        values = []
        for item in data_list:
            value = [item.get(field) for field in fields]
            values.append(value)
        
        try:
            cursor.executemany(sql, values)
        except Exception as e:
            print(f"批量插入失败:{str(e)}")
            conn.rollback()
        finally:
            cursor.close()

# 配置文件(config.py)
DB_CONFIG = {
    'host': 'localhost',
    'port': 3306,
    'user': 'root',
    'password': '你的数据库密码',
    'db_name': 'movie_box_office_db'
}

3.2 Flask Web 服务实现(app.py

实现用户注册登录、数据筛选、可视化页面渲染等核心功能,采用 Flask-Session 管理用户会话。

python

运行

python 复制代码
from flask import Flask, render_template, request, redirect, url_for, session, jsonify
from models.user_model import UserModel
from models.data_model import DataModel
import hashlib

app = Flask(__name__)
app.secret_key = 'movie_analysis_system_2024'  # 密钥(建议修改为随机字符串)

# 实例化模型
user_model = UserModel()
data_model = DataModel()

# 密码加密函数
def encrypt_password(password):
    md5 = hashlib.md5()
    md5.update(password.encode('utf-8'))
    return md5.hexdigest()

# 登录装饰器(验证用户登录状态)
def login_required(func):
    def wrapper(*args, **kwargs):
        if 'username' not in session:
            return redirect(url_for('login'))
        return func(*args, **kwargs)
    wrapper.__name__ = func.__name__
    return wrapper

# 登录页面
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        encrypted_pwd = encrypt_password(password)
        
        # 验证用户
        user = user_model.get_user_by_username(username)
        if user and user['password'] == encrypted_pwd and user['status'] == 1:
            session['username'] = username
            return redirect(url_for('index'))
        else:
            return render_template('login.html', error='账号或密码错误')
    
    return render_template('login.html')

# 注册页面
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        confirm_pwd = request.form.get('confirm_password')
        
        # 验证密码一致性
        if password != confirm_pwd:
            return render_template('register.html', error='两次密码不一致')
        
        # 检查用户名是否已存在
        if user_model.get_user_by_username(username):
            return render_template('register.html', error='用户名已存在')
        
        # 注册用户
        encrypted_pwd = encrypt_password(password)
        user_model.add_user(username, encrypted_pwd)
        return redirect(url_for('login'))
    
    return render_template('register.html')

# 退出登录
@app.route('/logout')
def logout():
    session.pop('username', None)
    return redirect(url_for('login'))

# 可视化主页面
@app.route('/index')
@login_required
def index():
    # 获取年份和档期选项
    years = data_model.get_distinct_years()
    schedules = [
        {'name': '春节档', 'date_range': '01-21至01-27'},
        {'name': '五一档', 'date_range': '04-29至05-03'},
        {'name': '暑期档', 'date_range': '06-01至08-31'},
        {'name': '中秋国庆档', 'date_range': '09-29至10-07'}
    ]
    return render_template('index.html', years=years, schedules=schedules)

# 获取可视化数据(AJAX接口)
@app.route('/get_visual_data', methods=['POST'])
@login_required
def get_visual_data():
    year = request.form.get('year')
    schedule = request.form.get('schedule')
    movie_name = request.form.get('movie_name')
    
    # 根据档期获取日期范围
    schedule_date_map = {
        '春节档': (f'{year}-01-21', f'{year}-01-27'),
        '五一档': (f'{year}-04-29', f'{year}-05-03'),
        '暑期档': (f'{year}-06-01', f'{year}-08-31'),
        '中秋国庆档': (f'{year}-09-29', f'{year}-10-07')
    }
    start_date, end_date = schedule_date_map.get(schedule)
    
    # 获取各类数据
    trend_data = data_model.get_box_office_trend(start_date, end_date, movie_name)  # 趋势数据
    proportion_data = data_model.get_box_office_proportion(start_date, end_date)  # 占比数据
    area_data = data_model.get_area_distribution(start_date, end_date, movie_name)  # 地域数据
    
    return jsonify({
        'trend_data': trend_data,
        'proportion_data': proportion_data,
        'area_data': area_data
    })

# 后台管理页面
@app.route('/admin/users')
@login_required
def admin_users():
    # 仅管理员可访问(简化版,实际项目需添加角色权限控制)
    if session['username'] != 'admin':
        return redirect(url_for('index'))
    
    users = user_model.get_all_users()
    return render_template('admin/user_management.html', users=users)

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

3.3 数据模型实现(models/data_model.py)

封装数据库查询逻辑,为前端提供可视化所需的数据。

python

运行

python 复制代码
from utils.db_pool import MySQLPool

class DataModel:
    def __init__(self):
        self.db = MySQLPool()
    
    def get_distinct_years(self):
        """获取所有数据年份"""
        conn = self.db.get_connection()
        cursor = conn.cursor(pymysql.cursors.DictCursor)
        sql = "SELECT DISTINCT YEAR(thedate) AS year FROM movie_box_office ORDER BY year DESC"
        cursor.execute(sql)
        result = [str(item['year']) for item in cursor.fetchall()]
        cursor.close()
        return result
    
    def get_box_office_trend(self, start_date, end_date, movie_name=None):
        """获取票房趋势数据(折线图)"""
        conn = self.db.get_connection()
        cursor = conn.cursor(pymysql.cursors.DictCursor)
        
        sql = """
            SELECT thedate, SUM(box_office) AS total_box, SUM(show_count) AS total_show, 
                   SUM(audience_count) AS total_audience
            FROM movie_box_office
            WHERE thedate BETWEEN %s AND %s
        """
        params = [start_date, end_date]
        
        # 筛选影片
        if movie_name and movie_name != '全部':
            sql += " AND movie_name = %s"
            params.append(movie_name)
        
        sql += " GROUP BY thedate ORDER BY thedate"
        cursor.execute(sql, params)
        result = cursor.fetchall()
        cursor.close()
        
        # 格式化数据(适配Echarts)
        dates = [item['thedate'] for item in result]
        box_office = [round(item['total_box']/10000, 2) for item in result]  # 转换为万元
        show_count = [item['total_show'] for item in result]
        audience_count = [item['total_audience'] for item in result]
        
        return {
            'dates': dates,
            'box_office': box_office,
            'show_count': show_count,
            'audience_count': audience_count
        }
    
    def get_box_office_proportion(self, start_date, end_date):
        """获取票房占比数据(饼图)"""
        conn = self.db.get_connection()
        cursor = conn.cursor(pymysql.cursors.DictCursor)
        
        sql = """
            SELECT movie_name, SUM(box_office) AS total_box, 
                   SUM(show_count) AS total_show, SUM(audience_count) AS total_audience
            FROM movie_box_office
            WHERE thedate BETWEEN %s AND %s
            GROUP BY movie_name
            ORDER BY total_box DESC
            LIMIT 10
        """
        cursor.execute(sql, [start_date, end_date])
        result = cursor.fetchall()
        cursor.close()
        
        # 格式化数据
        movie_names = [item['movie_name'] for item in result]
        box_proportion = [round(item['total_box']/sum([x['total_box'] for x in result])*100, 2) for item in result]
        show_proportion = [round(item['total_show']/sum([x['total_show'] for x in result])*100, 2) for item in result]
        audience_proportion = [round(item['total_audience']/sum([x['total_audience'] for x in result])*100, 2) for item in result]
        
        return {
            'movie_names': movie_names,
            'box_proportion': box_proportion,
            'show_proportion': show_proportion,
            'audience_proportion': audience_proportion
        }
    
    def get_area_distribution(self, start_date, end_date, movie_name=None):
        """获取地域分布数据(柱状图)"""
        conn = self.db.get_connection()
        cursor = conn.cursor(pymysql.cursors.DictCursor)
        
        sql = """
            SELECT city_level, SUM(box_office) AS total_box, 
                   SUM(show_count) AS total_show, SUM(audience_count) AS total_audience
            FROM movie_area_distribution
            WHERE thedate BETWEEN %s AND %s
        """
        params = [start_date, end_date]
        
        # 筛选影片
        if movie_name and movie_name != '全部':
            sql += " AND movie_name = %s"
            params.append(movie_name)
        
        sql += " GROUP BY city_level ORDER BY FIELD(city_level, '一线城市', '二线城市', '三线城市', '四线城市', '其它')"
        cursor.execute(sql, params)
        result = cursor.fetchall()
        cursor.close()
        
        # 格式化数据
        city_levels = [item['city_level'] for item in result]
        box_office = [round(item['total_box']/10000, 2) for item in result]
        show_count = [item['total_show'] for item in result]
        audience_count = [item['total_audience'] for item in result]
        
        return {
            'city_levels': city_levels,
            'box_office': box_office,
            'show_count': show_count,
            'audience_count': audience_count
        }

3.4 前端可视化实现(templates/index.html)

结合 Echarts 实现多图表展示,支持数据筛选交互。

html

预览

XML 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>电影票房数据分析可视化系统</title>
    <!-- 引入Echarts和jQuery -->
    <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
    <div class="header">
        <h1>电影票房数据分析可视化系统</h1>
        <div class="user-info">
            欢迎您,{{ session.username }} | <a href="{{ url_for('logout') }}">退出登录</a>
        </div>
    </div>
    
    <div class="filter-bar">
        <div class="filter-item">
            <label>年份选择:</label>
            <select id="year-select">
                {% for year in years %}
                <option value="{{ year }}">{{ year }}年</option>
                {% endfor %}
            </select>
        </div>
        <div class="filter-item">
            <label>电影档期:</label>
            <select id="schedule-select">
                {% for schedule in schedules %}
                <option value="{{ schedule.name }}">{{ schedule.name }}({{ schedule.date_range }})</option>
                {% endfor %}
            </select>
        </div>
        <div class="filter-item">
            <label>代表性电影:</label>
            <select id="movie-select">
                <option value="全部">全部</option>
                <!-- 实际项目可通过AJAX动态加载影片列表 -->
            </select>
        </div>
        <button id="search-btn">查询分析</button>
    </div>
    
    <div class="chart-container">
        <!-- 趋势分析图表 -->
        <div class="chart-item">
            <h3>票房/场次/人次趋势分析</h3>
            <div id="trend-chart" class="chart" style="width: 100%; height: 400px;"></div>
        </div>
        
        <!-- 占比分析图表 -->
        <div class="chart-item">
            <h3>影片票房/场次/人次占比</h3>
            <div id="proportion-chart" class="chart" style="width: 100%; height: 400px;"></div>
        </div>
        
        <!-- 地域分布图表 -->
        <div class="chart-item">
            <h3>地域分布分析(一线/二线/三线/四线/其它)</h3>
            <div id="area-chart" class="chart" style="width: 100%; height: 400px;"></div>
        </div>
    </div>

    <script>
        // 初始化图表
        const trendChart = echarts.init(document.getElementById('trend-chart'));
        const proportionChart = echarts.init(document.getElementById('proportion-chart'));
        const areaChart = echarts.init(document.getElementById('area-chart'));
        
        // 查询按钮点击事件
        $('#search-btn').click(function() {
            const year = $('#year-select').val();
            const schedule = $('#schedule-select').val();
            const movieName = $('#movie-select').val();
            
            // 发送AJAX请求获取数据
            $.ajax({
                url: '/get_visual_data',
                type: 'POST',
                data: {
                    year: year,
                    schedule: schedule,
                    movie_name: movieName
                },
                success: function(data) {
                    // 更新趋势图表
                    updateTrendChart(data.trend_data);
                    // 更新占比图表
                    updateProportionChart(data.proportion_data);
                    // 更新地域图表
                    updateAreaChart(data.area_data);
                },
                error: function() {
                    alert('数据加载失败,请重试!');
                }
            });
        });
        
        // 更新趋势图表
        function updateTrendChart(data) {
            const option = {
                title: { text: '票房/场次/人次时间趋势' },
                tooltip: { trigger: 'axis' },
                legend: { data: ['票房(万元)', '场次', '人次'] },
                grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
                xAxis: {
                    type: 'category',
                    data: data.dates
                },
                yAxis: [
                    {
                        type: 'value',
                        name: '票房(万元)',
                        axisLabel: { formatter: '{value}' }
                    },
                    {
                        type: 'value',
                        name: '场次/人次',
                        axisLabel: { formatter: '{value}' },
                        position: 'right'
                    }
                ],
                series: [
                    {
                        name: '票房(万元)',
                        type: 'line',
                        data: data.box_office,
                        yAxisIndex: 0,
                        smooth: true
                    },
                    {
                        name: '场次',
                        type: 'line',
                        data: data.show_count,
                        yAxisIndex: 1,
                        smooth: true
                    },
                    {
                        name: '人次',
                        type: 'line',
                        data: data.audience_count,
                        yAxisIndex: 1,
                        smooth: true
                    }
                ]
            };
            trendChart.setOption(option);
        }
        
        // 更新占比图表
        function updateProportionChart(data) {
            const option = {
                title: { text: '影片票房占比' },
                tooltip: { trigger: 'item' },
                legend: { orient: 'vertical', left: 'left' },
                series: [
                    {
                        name: '票房占比(%)',
                        type: 'pie',
                        radius: ['40%', '70%'],
                        avoidLabelOverlap: false,
                        itemStyle: {
                            borderRadius: 10,
                            borderColor: '#fff',
                            borderWidth: 2
                        },
                        label: { show: false, position: 'center' },
                        emphasis: {
                            label: { show: true, fontSize: 16, fontWeight: 'bold' }
                        },
                        labelLine: { show: false },
                        data: data.movie_names.map((name, index) => ({
                            name: name,
                            value: data.box_proportion[index]
                        }))
                    }
                ]
            };
            proportionChart.setOption(option);
        }
        
        // 更新地域图表
        function updateAreaChart(data) {
            const option = {
                title: { text: '地域分布(票房/场次/人次)' },
                tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
                legend: { data: ['票房(万元)', '场次', '人次'] },
                grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
                xAxis: { type: 'category', data: data.city_levels },
                yAxis: { type: 'value' },
                series: [
                    {
                        name: '票房(万元)',
                        type: 'bar',
                        data: data.box_office,
                        itemStyle: { color: '#5470c6' }
                    },
                    {
                        name: '场次',
                        type: 'bar',
                        data: data.show_count,
                        itemStyle: { color: '#91cc75' }
                    },
                    {
                        name: '人次',
                        type: 'bar',
                        data: data.audience_count,
                        itemStyle: { color: '#fac858' }
                    }
                ]
            };
            areaChart.setOption(option);
        }
        
        // 页面加载时默认查询
        $(function() {
            $('#search-btn').trigger('click');
        });
        
        // 窗口大小变化时重置图表
        window.addEventListener('resize', function() {
            trendChart.resize();
            proportionChart.resize();
            areaChart.resize();
        });
    </script>
</body>
</html>

四、系统运行与效果展示

4.1 运行步骤

  1. 启动 MySQL 服务,确保数据库连接配置正确;
  2. 运行爬虫脚本采集数据:python spider/box_office_spider.py
  3. 启动 Flask 服务:python app.py
  4. 浏览器访问http://localhost:5000/register注册账号,或使用管理员账号(admin/admin123)登录;
  5. 在主页面选择年份、档期、影片,点击 "查询分析" 即可查看可视化结果。

4.2 核心效果展示

1. 登录与注册页面
  • 支持账号注册、密码加密存储,登录失败给出友好提示;
  • 未登录用户无法访问核心功能,自动跳转至登录页面。
2. 数据筛选功能
  • 下拉选择框支持年份(2022-2024)、档期(春节档 / 五一档等)、影片名称筛选;
  • 筛选后实时更新所有图表数据,响应速度快。
3. 可视化图表效果
  • 趋势图:直观展示档期内每日票房、场次、人次变化,支持多指标对比;
  • 饼图:展示单档期内 TOP10 影片的票房占比,清晰呈现头部影片市场份额;
  • 柱状图:对比不同城市级别的票房、场次、人次分布,洞察地域消费特征。

4.3 后台管理功能

  • 管理员可查看所有注册用户列表,支持用户状态管理(启用 / 禁用);
  • 支持手动同步最新票房数据,确保数据时效性。

五、项目拓展与优化方向

  1. 数据采集优化:

    • 添加定时采集任务(使用 APScheduler),自动同步每日最新数据;
    • 增加异常重试机制与反爬策略(如 IP 代理池、请求间隔控制)。
  2. 功能拓展:

    • 实现数据导出功能(Excel/PDF),支持用户下载分析结果;
    • 添加影片类型分析(喜剧 / 动作 / 科幻等),丰富可视化维度;
    • 集成机器学习模型(如线性回归),实现票房预测功能。
  3. 系统优化:

    • 引入 Redis 缓存热门查询结果,提升系统响应速度;
    • 完善用户权限管理(普通用户 / 管理员 / 超级管理员);
    • 优化前端界面,支持响应式设计(适配手机 / 平板设备)。

六、总结

本项目基于 Python 实现了一套完整的电影票房可视化分析系统,涵盖数据采集、存储、Web 服务、可视化展示全流程,技术栈贴合企业实际应用场景,适合作为计算机专业毕业设计或大数据实践项目。通过本项目的开发,可熟练掌握 Python 爬虫、Flask Web 开发、Echarts 可视化、MySQL 数据库操作等核心技能,同时理解大数据分析项目的完整开发流程。

项目代码结构清晰、注释详细,可根据实际需求灵活拓展功能。建议在开发过程中注重代码规范性与可维护性,同时积累爬虫反爬、数据清洗、性能优化等实战经验,为后续就业或深造打下坚实基础。

相关推荐
JAVA+C语言6 小时前
Python+Django 核心介绍
开发语言·python·django
中年程序员一枚6 小时前
不想花钱买会员,自己动手用python制作视频
开发语言·python·音视频
qq_214782616 小时前
Hadley Wickham长文回顾:R语言tidyverse过去20年的演进之路、现状与未来展望!
python·算法·线性回归
励志成为糕手6 小时前
MapReduce工作流程:从MapTask到Yarn机制深度解析
大数据·hadoop·分布式·mapreduce·yarn
源码获取_wx:Fegn08956 小时前
基于springboot + vue考勤管理系统
java·开发语言·vue.js·spring boot·后端·spring·课程设计
认真敲代码的小火龙6 小时前
【JAVA项目】基于JAVA的仓库管理系统
java·开发语言·课程设计
Code_Geo6 小时前
JAVA大数据场景使用StreamingOutput
java·大数据·开发语言·streamingoutput
BoBoZz196 小时前
CellsInsideObject 网格面内外与交界的判定
python·vtk·图形渲染·图形处理