最近在整理舞蹈表演视频时,发现很多文件名都采用了类似"4:29-369.76 160109 Brand New Girl Dance Performance"这样的格式。这种命名方式看似随意,但实际上包含了丰富的信息。本文将详细解析这种文件命名规范,并分享一套完整的舞蹈视频管理系统搭建方案。
1. 舞蹈视频文件命名规范解析
1.1 命名结构拆解
以"4:29-369.76 160109 Brand New Girl Dance Performance"为例,我们可以将其分解为以下几个关键部分:
- 4:29:视频时长,表示4分29秒
- 369.76:文件大小或编号标识
- 160109:日期信息,通常为2016年1月9日
- Brand New Girl:舞蹈曲目名称
- Dance Performance:内容类型标识
1.2 各字段含义详解
时长字段(4:29)时长信息对于舞蹈视频管理至关重要。在实际使用中,建议采用标准的时分秒格式:
- 短于1小时的视频:MM:SS(如4:29)
- 长于1小时的视频:HH:MM:SS(如1:23:45)
日期字段(160109)日期编码采用YYMMDD格式,这种紧凑的表示方式便于排序和检索。在实际系统中,建议同时保存标准日期格式(2016-01-09)用于数据库存储。
曲目名称(Brand New Girl)曲目名称应当规范化处理:
- 去除特殊字符
- 统一大小写格式
- 使用下划线替代空格(可选)
1.3 命名规范最佳实践
基于分析,我们推荐以下命名规范:
[时长]_[日期]_[曲目]_[类型]_[版本]示例:4_29_20160109_Brand_New_Girl_Performance_v1.mp4
2. 舞蹈视频管理系统环境搭建
2.1 系统环境要求
- 操作系统:Windows 10/11 或 macOS 10.14+
- Python版本:3.8+
- 数据库:SQLite(开发环境)/ MySQL(生产环境)
- 前端框架:Flask或Django
- 视频处理库:OpenCV, MoviePy
2.2 项目结构设计
dance_video_manager/ ├── app.py # 主应用文件 ├── requirements.txt # 依赖包列表 ├── config/ │ └── config.py # 配置文件 ├── models/ │ └── video_model.py # 数据模型 ├── utils/ │ └── file_parser.py # 文件解析工具 ├── static/ │ ├── css/ │ ├── js/ │ └── images/ └── templates/ └── index.html # 前端模板3. 核心功能实现
3.1 文件命名解析器
首先实现一个强大的文件名解析器,能够处理各种命名格式:
# utils/file_parser.py import re from datetime import datetime from typing import Dict, Optional class VideoFileParser: def __init__(self): self.patterns = [ # 匹配 "4:29-369.76 160109 Brand New Girl Dance Performance" r'(?P<duration>\d+:\d+)[-\-\s]+(?P<size>\d+\.\d+)\s+(?P<date>\d{6})\s+(?P<title>[\w\s]+)\s+(?P<type>[\w\s]+)', # 其他常见格式模式... ] def parse_filename(self, filename: str) -> Optional[Dict]: """解析舞蹈视频文件名""" for pattern in self.patterns: match = re.match(pattern, filename) if match: return self._process_match(match.groupdict()) return None def _process_match(self, data: Dict) -> Dict: """处理匹配结果""" # 处理时长 duration = self._parse_duration(data.get('duration', '')) # 处理日期 date = self._parse_date(data.get('date', '')) # 清理标题和类型 title = data.get('title', '').strip() file_type = data.get('type', '').strip() return { 'duration': duration, 'file_size': data.get('size'), 'record_date': date, 'title': title, 'type': file_type, 'original_name': data.get('original_name', '') } def _parse_duration(self, duration_str: str) -> int: """解析时长字符串为秒数""" if ':' in duration_str: parts = duration_str.split(':') if len(parts) == 2: return int(parts[0]) * 60 + int(parts[1]) elif len(parts) == 3: return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]) return 0 def _parse_date(self, date_str: str) -> str: """解析日期字符串""" try: if len(date_str) == 6: # 假设为YYMMDD格式 date_obj = datetime.strptime(date_str, '%y%m%d') return date_obj.strftime('%Y-%m-%d') except ValueError: pass return date_str3.2 数据库模型设计
# models/video_model.py from datetime import datetime from sqlalchemy import Column, Integer, String, DateTime, Text, Float from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class DanceVideo(Base): __tablename__ = 'dance_videos' id = Column(Integer, primary_key=True) original_filename = Column(String(255), nullable=False) duration_seconds = Column(Integer, default=0) file_size_mb = Column(Float, default=0) record_date = Column(DateTime) title = Column(String(100), nullable=False) video_type = Column(String(50)) # Performance, Practice, etc. file_path = Column(String(500)) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def to_dict(self): return { 'id': self.id, 'original_filename': self.original_filename, 'duration': self._format_duration(), 'file_size': f"{self.file_size_mb:.2f} MB" if self.file_size_mb else "N/A", 'record_date': self.record_date.strftime('%Y-%m-%d') if self.record_date else "Unknown", 'title': self.title, 'type': self.video_type, 'file_path': self.file_path } def _format_duration(self) -> str: """将秒数格式化为 MM:SS 或 HH:MM:SS""" seconds = self.duration_seconds if seconds < 3600: return f"{seconds // 60:02d}:{seconds % 60:02d}" else: hours = seconds // 3600 minutes = (seconds % 3600) // 60 secs = seconds % 60 return f"{hours:02d}:{minutes:02d}:{secs:02d}"4. 完整系统实现
4.1 主应用搭建
# app.py from flask import Flask, render_template, request, jsonify from models.video_model import DanceVideo, Base from utils.file_parser import VideoFileParser from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import os app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'static/uploads' app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB # 数据库初始化 engine = create_engine('sqlite:///dance_videos.db') Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) @app.route('/') def index(): return render_template('index.html') @app.route('/api/videos', methods=['GET']) def get_videos(): """获取视频列表""" session = Session() try: videos = session.query(DanceVideo).all() return jsonify([video.to_dict() for video in videos]) finally: session.close() @app.route('/api/upload', methods=['POST']) def upload_video(): """上传并解析视频文件""" if 'file' not in request.files: return jsonify({'error': 'No file uploaded'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 # 保存文件 filename = file.filename filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) # 解析文件名 parser = VideoFileParser() video_info = parser.parse_filename(filename) if not video_info: return jsonify({'error': 'Cannot parse filename'}), 400 # 保存到数据库 session = Session() try: video = DanceVideo( original_filename=filename, duration_seconds=video_info['duration'], file_size_mb=float(video_info['file_size']) if video_info['file_size'] else 0, record_date=datetime.strptime(video_info['record_date'], '%Y-%m-%d') if video_info['record_date'] != 'Unknown' else None, title=video_info['title'], video_type=video_info['type'], file_path=filepath ) session.add(video) session.commit() return jsonify(video.to_dict()) except Exception as e: session.rollback() return jsonify({'error': str(e)}), 500 finally: session.close() if __name__ == '__main__': if not os.path.exists(app.config['UPLOAD_FOLDER']): os.makedirs(app.config['UPLOAD_FOLDER']) app.run(debug=True)4.2 前端界面实现
<!-- templates/index.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> .container { max-width: 1200px; margin: 0 auto; padding: 20px; } .upload-section { margin-bottom: 30px; padding: 20px; border: 2px dashed #ccc; } .video-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } .video-card { border: 1px solid #ddd; padding: 15px; border-radius: 5px; } .video-info { margin: 10px 0; } </style> </head> <body> <div class="container"> <h1>舞蹈视频管理系统</h1> <div class="upload-section"> <h3>上传视频文件</h3> <input type="file" id="fileInput" accept="video/*"> <button onclick="uploadFile()">上传</button> </div> <div class="video-list" id="videoList"> <!-- 视频列表将通过JavaScript动态加载 --> </div> </div> <script> // 页面加载时获取视频列表 document.addEventListener('DOMContentLoaded', loadVideos); async function loadVideos() { try { const response = await fetch('/api/videos'); const videos = await response.json(); displayVideos(videos); } catch (error) { console.error('加载视频列表失败:', error); } } function displayVideos(videos) { const container = document.getElementById('videoList'); container.innerHTML = videos.map(video => ` <div class="video-card"> <h4>${video.title}</h4> <div class="video-info"> <div>时长: ${video.duration}</div> <div>日期: ${video.record_date}</div> <div>类型: ${video.type}</div> <div>文件大小: ${video.file_size}</div> </div> </div> `).join(''); } async function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert('请选择文件'); return; } const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); if (response.ok) { alert('上传成功'); loadVideos(); // 刷新列表 fileInput.value = ''; // 清空文件选择 } else { const error = await response.json(); alert('上传失败: ' + error.error); } } catch (error) { alert('上传过程中发生错误: ' + error.message); } } </script> </body> </html>5. 系统部署与配置
5.1 依赖环境配置
创建requirements.txt文件:
Flask==2.3.3 SQLAlchemy==2.0.23 openpyxl==3.1.2 Pillow==10.0.1 moviepy==1.0.3 opencv-python==4.8.1.785.2 数据库配置优化
对于生产环境,建议使用MySQL数据库:
# config/config.py import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here' # SQLite配置(开发环境) SQLALCHEMY_DATABASE_URI = 'sqlite:///dance_videos.db' # MySQL配置(生产环境) # SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://username:password@localhost/dance_videos' SQLALCHEMY_TRACK_MODIFICATIONS = False UPLOAD_FOLDER = 'static/uploads' MAX_CONTENT_LENGTH = 500 * 1024 * 10246. 高级功能扩展
6.1 视频元数据提取
除了文件名解析,还可以直接从视频文件中提取元数据:
# utils/video_metadata.py import cv2 from moviepy.editor import VideoFileClip import os class VideoMetadataExtractor: def extract_metadata(self, filepath: str) -> Dict: """提取视频文件的元数据""" metadata = {} try: # 使用OpenCV获取基础信息 cap = cv2.VideoCapture(filepath) if cap.isOpened(): metadata['width'] = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) metadata['height'] = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) metadata['fps'] = cap.get(cv2.CAP_PROP_FPS) metadata['frame_count'] = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) cap.release() # 使用MoviePy获取时长 with VideoFileClip(filepath) as video: metadata['duration'] = video.duration metadata['audio'] = video.audio is not None # 文件大小 metadata['file_size'] = os.path.getsize(filepath) / (1024 * 1024) # MB except Exception as e: print(f"提取元数据失败: {e}") return metadata6.2 智能分类与标签系统
# utils/classifier.py from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans import numpy as np class DanceVideoClassifier: def __init__(self): self.vectorizer = TfidfVectorizer(max_features=1000) self.kmeans = KMeans(n_clusters=5, random_state=42) self.is_fitted = False def fit(self, video_titles: List[str]): """基于视频标题进行聚类训练""" if len(video_titles) < 5: return # 数据量太少不进行聚类 X = self.vectorizer.fit_transform(video_titles) self.kmeans.fit(X) self.is_fitted = True def predict_category(self, title: str) -> int: """预测视频类别""" if not self.is_fitted: return -1 X = self.vectorizer.transform([title]) return self.kmeans.predict(X)[0]7. 常见问题与解决方案
7.1 文件名解析失败
问题现象:系统无法正确解析上传的文件名
常见原因:
- 文件名格式不符合预期模式
- 包含特殊字符或编码问题
- 日期格式不标准
解决方案:
# 增强解析器的容错能力 def enhanced_parse(self, filename: str) -> Dict: # 移除文件扩展名 name_only = os.path.splitext(filename)[0] # 尝试多种解析模式 for parser_method in [self._parse_pattern1, self._parse_pattern2, self._parse_fallback]: result = parser_method(name_only) if result: return result # 最终回退方案:使用文件名作为标题 return { 'title': name_only, 'duration': 0, 'record_date': 'Unknown', 'type': 'Unknown' }7.2 数据库性能优化
当视频数量增多时,需要优化查询性能:
-- 为常用查询字段创建索引 CREATE INDEX idx_video_date ON dance_videos(record_date); CREATE INDEX idx_video_title ON dance_videos(title); CREATE INDEX idx_video_type ON dance_videos(video_type); -- 复合索引用于复杂查询 CREATE INDEX idx_date_type ON dance_videos(record_date, video_type);8. 最佳实践建议
8.1 文件命名规范
推荐使用统一的命名规范:
YYYYMMDD_曲目名称_舞蹈类型_时长_版本.扩展名示例:20160109_BrandNewGirl_Performance_4m29s_v1.mp4
8.2 数据备份策略
定期备份视频文件和数据库:
# utils/backup_manager.py import shutil from datetime import datetime import os class BackupManager: def create_backup(self, source_dir: str, backup_dir: str): """创建系统备份""" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_path = os.path.join(backup_dir, f'backup_{timestamp}') # 备份数据库 db_source = 'dance_videos.db' if os.path.exists(db_source): shutil.copy2(db_source, f'{backup_path}_db.db') # 备份上传目录 if os.path.exists(source_dir): shutil.copytree(source_dir, f'{backup_path}_videos')8.3 安全注意事项
- 文件上传安全:验证文件类型和大小,防止恶意文件上传
- 数据库安全:使用参数化查询,防止SQL注入
- 访问控制:在生产环境中添加用户认证和权限管理
- 数据加密:敏感信息应进行加密存储
这套舞蹈视频管理系统不仅解决了文件命名规范化的问题,还提供了完整的视频管理解决方案。通过合理的系统设计和代码实现,可以显著提高舞蹈视频的管理效率和使用体验。