
简介这是一份基于Flask框架的在线电影视频播放网站毕业设计源码适合计算机相关专业学生用于毕设、课程设计或项目演示。网站前端采用HTML5与Bootstrap后端使用Python3与Flask数据库为MySQL涵盖视频浏览、搜索筛选、登录注册、收藏评论等前台功能后台支持对视频、用户、评论、轮播图、操作日志和管理员进行管理。压缩包共2000个文件以JS、HTML、CSS等前端资源为主另有Python源码、SQL数据库脚本及说明文档整体大小21.02MB结构清晰便于二次开发。资源附带了管理员与用户测试账号代码已通过运行测试答辩评审平均分达96分已有550人学习下载。对于需要完成相似课题或学习Flask全栈开发的同学这份源码能提供完整项目框架、配置思路与可直接运行的示例具备较高参考价值。1. Flask视频播放网站别急着看代码先想清楚视频怎么发出去网上能搜到大量「Python Flask 电影网站源代码」的zip包解压后结构基本一致一个app.py、一个models.py、一个templates目录、一个sqlite数据库文件。文档说明写得比代码还长但真正能拖动进度条、播放不卡的项目十个里挑不出两三个。这套东西的本质是毕业设计和数据库课程设计里的常客核心难点从来不在增删改查而是视频文件如何通过HTTP正确到达浏览器。Flask本身不擅长文件服务但配合Range协议和Nginx就能把视频播放做得像模像样。这篇博文不负责帮你交作业只帮你把这个zip里的每个模块落成自己能跑、能改、能上线的小型视频网站。2. 数据库先行电影信息、播放记录与用户体系如何落表2.1 影视站的表结构先分清主数据和行为数据一个小型在线电影播放站的数据库远比想象中简单主数据就三张分类、电影、用户。行为数据是播放记录和评论。很多课程设计把字段堆进一张表里导致后面做搜索、做分页、做播放记录时全糊在一起。主数据的核心是电影表字段里最容易忽略的是status。它控制着电影是否在前台展示做上下架功能时不需要删行改个数字就行。播放记录表要存「看到哪里」用户下次进入时能从上次进度继续。评论表则绑定用户和电影两个外键。表与表之间的关系决定了后面ORM建模时relationship的写法也决定了查询时的性能。2.2 Flask-SQLAlchemy模型定义五张表的关系映射# models.py from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db SQLAlchemy() class Category(db.Model): __tablename__ category id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(32), uniqueTrue, nullableFalse) movies db.relationship(Movie, backrefcategory, lazydynamic) class Movie(db.Model): __tablename__ movie id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(128), nullableFalse, indexTrue) # 标题建索引搜索走索引 cover db.Column(db.String(256)) video_path db.Column(db.String(256), nullableFalse) # 相对路径不含域名 category_id db.Column(db.Integer, db.ForeignKey(category.id)) summary db.Column(db.Text) play_count db.Column(db.Integer, default0) status db.Column(db.SmallInteger, default1) # 1上架 0下架 created_at db.Column(db.DateTime, defaultdatetime.now) class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) is_admin db.Column(db.Boolean, defaultFalse) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class PlayHistory(db.Model): __tablename__ play_history id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) movie_id db.Column(db.Integer, db.ForeignKey(movie.id)) progress db.Column(db.Integer, default0) # 播放进度单位秒 updated_at db.Column(db.DateTime, defaultdatetime.now, onupdatedatetime.now)这里用video_path而不是video_url刻意区分一个概念库里存相对路径域名和前缀交给模板拼接。这样将来换存储目录、换CDN不需要回改数据库。play_count字段在热门排行榜里要用到它属于冗余计数可以在播放视图里累加也可以后台上架时初始化。lazydynamic表示获取分类下的电影列表时返回一个查询对象支持继续叠加过滤条件而不是一次性把全部结果加载进内存。2.3 从建库到初始化数据库课程设计的三个命令# 以 MySQL 为例创建数据库 mysql -uroot -p -e CREATE DATABASE movie_site DEFAULT CHARSET utf8mb4; # 建表 python -c from app import app, db; app.app_context().push(); db.create_all() # 写入初始分类和测试电影 python seed.py第一行建库时指定utf8mb4是为了中文标题和简介不出乱码这是数据库课程设计里最常扣分的点。第二行通过应用上下文建表Flask 2.x必须在app_context()里执行create_all()直接跑会报Working outside of application context。第三行的seed.py是初始化数据脚本插入默认分类和几个测试电影方便打开站点就有内容可看。数据库表结构建好后接下来要解决的核心问题就是视频文件怎么发出去。对比项SQLiteMySQL初始化成本零配置复制即用需要单独安装并建库并发能力写并发差单写多读读写并发稳定备份部署备份一个文件即可需要mysqldump和权限管理适合场景课程设计、单机演示多人访问的真实站点3. 播放的核心Flask如何把视频流送到浏览器3.1 视频拖不动进度条多半是Range请求没处理浏览器里的video标签播放MP4时不会一次性把整个文件拉完。它先发一个不带Range的请求探测文件大小然后根据用户拖动进度条用Range: bytes1234-5678这种请求头向服务器要文件的某一段。服务器返回206 Partial Content和对应的数据段播放器才能拖动跳转。很多课程设计代码直接用send_file(path)文件小的时候一切正常文件上到几百兆播放器会卡在缓冲、进度条拖不动。所以视频播放视图要自己解析Range头。下面是完整实现# video_stream.py import os from flask import request, Response, abort, current_app def iter_file(path, start, end, chunk_size65536): 按块读取文件区间 [start, end] 的数据 with open(path, rb) as f: f.seek(start) remaining end - start 1 while remaining 0: size min(chunk_size, remaining) data f.read(size) if not data: break yield data remaining - len(data) def parse_range(range_header, file_size): 解析 Range 头返回 (start, end)越界时限制到文件大小内 if not range_header or not range_header.startswith(bytes): return 0, file_size - 1 try: start_s, end_s range_header[6:].split(-, 1) start int(start_s) end int(end_s) if end_s else file_size - 1 except (ValueError, KeyError): abort(416) if start file_size or end start: abort(416) return start, min(end, file_size - 1) app.route(/play/int:movie_id) def play_stream(movie_id): movie Movie.query.get_or_404(movie_id) path os.path.join(current_app.config[MEDIA_ROOT], movie.video_path) if not os.path.exists(path): abort(404) file_size os.path.getsize(path) range_header request.headers.get(Range) start, end parse_range(range_header, file_size) resp Response(iter_file(path, start, end), status206 if range_header else 200, content_typevideo/mp4, direct_passthroughTrue) resp.headers[Content-Range] fbytes {start}-{end}/{file_size} resp.headers[Accept-Ranges] bytes resp.headers[Content-Length] str(end - start 1) return resp几个参数要说明iter_file的chunk_size65536是64KB这个值太小会让TCP小包过多太大会占用内存和磁盘突发IO64KB是视频流服务里的常见折中direct_passthroughTrue告诉Flask跳过默认的Etag和缓存逻辑让响应变成裸文件流避免每次请求都对整个文件做哈希计算。parse_range里abort(416)对应Range Not Satisfiable这是播放器拖动到文件尾端时需要的正确响应码写错成404会导致播放器直接停止播放。文件路径用MEDIA_ROOT配置拼接这要求config.py里必须有这个常量否则os.path.join(None, path)会抛异常。3.2 全片直出、HLS还是DASH按场景选传输方案Range方案适合MP4、WebM这类「可随机访问」的容器格式。Flask项目里最常见的坑是拿了RMVB或AVI格式硬往video里塞浏览器直接不认。最常见的做法是统一转成H.264编码的MP4一条ffmpeg命令搞定ffmpeg -i input.avi -c:v libx264 -c:a aac -movflags faststart output.mp4faststart参数会把moov元数据块挪到文件头部这样播放器不用等文件全部下载完就能开始播放对MP4直出方案几乎是必加的参数。不转封装直接改扩展名是无效的浏览器按容器格式解析后缀名只影响服务器返回的Content-Type。方案服务端工作量进度条支持兼容性MP4 Range中需要实现Range逻辑好所有现代浏览器HLS/m3u8低返回静态文件即可靠播放器实现iOS原生支持Web端需要hls.jsDASH低好需要dash.js标准较复杂HLS切片适合视频量大的场景ffmpeg切片成m3u8加ts分片天然支持码率自适应。代价是课程设计级别的前端要额外引hls.js且切片文件数量多管理略麻烦。大多数课程设计和中小型影视站MP4加Range是性价比最高的方案。3.3 防盗链Referrer校验入门签名Token才是正解视频网站上线后第一个被薅的就是视频地址。用户拿video的src直接下载或者别的站点把视频地址嵌到自己页面上播放。防外部站点盗用最简单的是校验Referer头app.route(/play/int:movie_id) def play_stream(movie_id): referer request.headers.get(Referer, ) if not referer.startswith(request.host_url): abort(403) # 后续Range处理逻辑同上这个方案对「拿地址浏览器直接下载」无效因为浏览器直接访问地址时Referer为空。更可靠的是生成带有效期的签名URL在电影详情页点击播放时拿到?tokenxxxexpirexxx的临时地址服务端校验通过才允许进入流文件逻辑。签名生成代码放在模板路由里import hmac, hashlib, time def sign_video_url(movie_id): expire int(time.time()) 3600 * 6 # 6小时有效 msg f{movie_id}:{expire}.encode() token hmac.new(current_app.config[SECRET_KEY], msg, hashlib.sha1).hexdigest() return f/play/{movie_id}?expire{expire}token{token}校按时重新按相同参数计算一遍HMAC相等才放行。注意SECRET_KEY不能用默认的dev或空值否则等于签名形同虚设。签名URL过期后播放器重新拉一次详情页就能刷新对用户体验影响很小。4. 页面与播放器模板渲染、搜索分页和后台上线的联动4.1 播放器选型原生video标签配合续播接口前台播放页用原生video标签搭底需要弹幕、倍速、画中画的时候再引入DPlayer或video.js。原生标签结构如下video idplayer controls preloadauto width100% height420 source src{{ play_url }} typevideo/mp4 /video script const player document.getElementById(player); player.currentTime {{ history_progress }}; // 续播定位到上次进度 player.addEventListener(pause, () { fetch(/api/progress, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ movie_id: {{ movie.id }}, progress: Math.floor(player.currentTime) }) }); }); /scriptpreloadauto让页面加载后就拉少量数据便于用户快速看到第一帧。续播通过player.currentTime直接定位把播放进度回写到PlayHistory表。注意浏览器自动播放政策下带声音的视频不能自动启动播放所以这里只定位进度不做play()调用。播放过程中定期保存进度比只在pause时保存更可靠用户直接关标签页时pause事件不一定会触发。4.2 搜索和分页条件查询、排序、翻页一次写全前台首页的搜索要按标题模糊查、按分类筛选、按播放数排序。Flask-SQLAlchemy的查询写法需要把过滤条件累积起来app.route(/) def index(): page request.args.get(page, 1, typeint) keyword request.args.get(kw, ).strip() cid request.args.get(cid, 0, typeint) query Movie.query.filter(Movie.status 1) if keyword: query query.filter(Movie.title.like(f%{keyword}%)) if cid: query query.filter(Movie.category_id cid) movies query.order_by(Movie.play_count.desc()) \ .paginate(pagepage, per_page12, error_outFalse) return render_template(index.html, moviesmovies, pagesmovies.pages, pagepage, kwkeyword, cidcid)paginate是Flask-SQLAlchemy内置的分页对象返回的movies.items是当前页数据movies.pages是总页数error_outFalse防止页码越界时抛404。排序用play_count.desc()让热播电影靠前。搜索词kw和分类cid要回传到模板里否则翻页后筛选条件会丢掉。分页链接生成时用url_for(index, pagep, kwkw, cidcid)带上原参数。4.3 后台发布电影大文件上传要避开Flask的内存陷阱后台发布电影的字段和Movie表一一对应但video_path不放文件上传的结果而是放「服务器上已有的视频文件相对路径」。这样避免Flask接收大文件时把内存打爆。视频上传采用保存到临时目录加秒传映射的方式文件名用时间戳加随机串防止中文文件名导致路径解析错误# admin.py 上传功能的简化版 from werkzeug.utils import secure_filename import time, os, uuid app.route(/admin/movie/add, methods[POST]) def admin_add_movie(): if not current_user.is_authenticated or not current_user.is_admin: abort(403) file request.files.get(video) if not file or not file.filename.endswith(.mp4): flash(仅支持MP4文件) return redirect(request.referrer) # 按月划分子目录避免单目录文件过多 sub_dir time.strftime(%Y%m) os.makedirs(os.path.join(current_app.config[MEDIA_ROOT], sub_dir), exist_okTrue) base f{int(time.time())}_{uuid.uuid4().hex[:6]}.mp4 avatar_path os.path.join(sub_dir, base) file.save(os.path.join(current_app.config[MEDIA_ROOT], avatar_path)) movie Movie(titlerequest.form[title], video_pathavatar_path, category_idint(request.form[category_id]), summaryrequest.form[summary], status1) db.session.add(movie) db.session.commit() return redirect(/admin/movies)文件名里时间戳加uuid4前6位保证一个时间点内并发上传不撞名也避免直接用原始文件名让secure_filename处理中文时被截断成空串。按月建子目录是防止文件系统单目录出现几万文件的寻址瓶颈。前端上传超时是另一个坑Nginx的client_max_body_size默认1m必须在server块里调大同时proxy_read_timeout也要放宽到300秒以上否则大视频传到一半就被断掉。5. 权限与安全登录、防刷与数据库增删改查的隐藏风险5.1 登录态用session配合装饰器控制前后台角色前台用户和后台管理员共用一张User表通过is_admin字段区分角色。Flask自带的session底层是签名Cookie不占用服务端内存课程设计规模下足够。登录逻辑里必须注意session.permanent决定会话生命周期前端用户要持久登录后台要短会话。登录接口和权限装饰器拆开写from functools import wraps from flask import session, redirect, url_for, abort def login_required(view_func): wraps(view_func) def wrapper(*args, **kwargs): if not session.get(user_id): return redirect(url_for(login, nextrequest.url)) return view_func(*args, **kwargs) return wrapper def admin_required(view_func): wraps(view_func) def wrapper(*args, **kwargs): if not session.get(is_admin): abort(403) return view_func(*args, **kwargs) return wrapper装饰器的实现要点wraps保留原视图的元信息nextrequest.url让登录成功后跳回原地址abort(403)返回403而不是重定向避免后台地址被爆破尝试时混淆判断。session里的is_admin在登录成功时写入退出登录时用session.clear()清空全部会话数据。5.2 不只是防SQL注入表单、模板与请求头的三个入口Flask-SQLAlchemy的ORM参数绑定天然过滤SQL注入但仍有三个容易忽略的入口。第一个是手写text()或execute(sql)时直接拼接参数正确写法是db.session.execute(text(SELECT * FROM movie WHERE title LIKE :kw), {kw: f%{kw}%})。第二个是模板里的XSSJinja2对{{ movie.summary }}默认转义HTML但用|safe过滤器的地方要严格控制数据来源。第三个是重定向攻击request.referrer用于后台跳转时不要直接透传。数据库增删改查在后台的体现是标准的四个视图列表、新增、编辑、删除。删除用db.session.delete(movie)提交后还要把对应的视频文件从磁盘删除否则数据库删了文件还在慢慢占满磁盘。这个「两级删除」是课程设计里代码和文档都很少提到、但上线一定遇到的事。删除前先取movie.video_path拼出绝对路径os.path.exists检查后再os.remove。5.3 播放量防刷用一张计数表做幂等累加play_count每次播放视图加一刷新一次页面就多一次。常见的防刷方案是同一IP在60秒内对同一部电影只计一次数。直接依赖一张计数表加联合主键CREATE TABLE play_count_log ( movie_id INT NOT NULL, ip_hash CHAR(40) NOT NULL, first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (movie_id, ip_hash) );在播放视图里先执行INSERT IGNORE影响行数为1说明是第一次访问再累加play_count。ip_hash不存原始IP用SHA1做一次散列既满足聚类统计又避免敏感信息明文落库。这个方案对多实例部署同样有效瓶颈只在数据库单行插入的并发量。要防止同一用户换IP刷量可以叠加session里的user_id生成校验值双因子判断更稳。6. 部署与排错zip里的项目能跑起来更要能上线6.1 Gunicorn加Nginx视频站点的最小上线配置开发环境里flask run自带单进程服务页面最多几十个并发视频请求很容易卡死。上线的常见结构是Nginx当反向代理加静态文件服务Gunicorn跑Flask应用。启动命令如下pip install gunicorn gunicorn -w 2 -b 127.0.0.1:8000 app:app --timeout 30Nginx侧的配置有两段关键locationlocation /media/ { alias /data/movie/media/; sendfile on; sendfile_max_chunk 1m; aio on; } location /play/ { proxy_pass http://127.0.0.1:8000; proxy_buffering off; }sendfile_max_chunk 1m防止单次sendfile占住worker太久aio on对视频文件走异步IO。proxy_buffering off对/play/必须关闭否则Nginx缓冲视频流导致Range响应被吞掉进度条又拖不动。Gunicorn的-w 2只开两个worker视频流IO密集worker太多反而在GIL上互相争抢。--timeout 30防止某个慢请求把worker整个拖死超过30秒直接被杀了重启。6.2 一次Range请求出错的排查路线视频播放中最常见的故障是「前几秒能放拖动就转圈」。按这个顺序排查先确认浏览器发出的请求头里有Range且Nginx没有剥离它再确认Flask返回的Content-Range格式是bytes start-end/total最后确认Content-Length和实际响应体字节数一致。一个隐蔽的坑是Response里设置了Content-Encoding: gzip视频流一旦被压缩编码播放器无法解析Range必须在Nginx对视频location关闭gzip。用curl直接验证是最快的方式curl -I -H Range: bytes0-1023 http://127.0.0.1:8000/play/1看到HTTP/1.1 206 Partial Content和正确的Content-Range就是Range链路正常。如果返回200说明Flask响应没走流媒体分支检查request.headers.get(Range)拿到的值是否为None以及路径拼接是否正确。顺便确认MEDIA_ROOT配置在config.py里有值别拿None拼进os.path.join。6.3 磁盘IO扛不住时用X-Accel-Redirect把文件传输交给Nginx单机视频站最大的瓶颈是Flask逐块读文件再转发。Nginx提供了X-Accel-Redirect机制Flask校验权限通过后只返回一个特殊响应头Nginx直接接管文件传输Gunicorn worker瞬间释放。改造/play/视图的返回逻辑app.route(/play/int:movie_id) def play_stream(movie_id): movie Movie.query.get_or_404(movie_id) path os.path.join(current_app.config[MEDIA_ROOT], movie.video_path) if not os.path.exists(path): abort(404) resp Response(, status200) resp.headers[X-Accel-Redirect] f/media/{movie.video_path} resp.headers[Content-Type] video/mp4 return respNginx收到这个响应头后会从/media/内部location去取静态文件整个过程对Flask来说是瞬时的。这个方案让Flask只做鉴权和计数文件传输全部交给NginxCPU和内存占用立刻降下来。要注意X-Accel-Redirect指向的必须是Nginx可解析的内部路径不能直接暴露文件系统绝对路径。配合前面的PlayHistory续播接口单机撑住几百个并发视频请求没有问题。本文还有配套的精品资源点击获取