
最近在整理本地视频素材时遇到一个挺典型的场景手头有一批从不同渠道下载的剧集或视频文件名五花八门有的带集数有的带平台水印标识有的甚至夹杂着各种符号和宣传语比如【正剧】寒渊渡骨・晚香如梦 第10集【投币200转免】.mp4这种。直接扔进播放列表不仅观感杂乱用刮削器整理元数据时也常常因为文件名不规范而匹配失败。这看起来只是个改名字的小事但背后涉及的是一个更本质的问题如何将零散、非结构化的数字内容通过一套可重复、可解释的规则转化为整洁、可检索的资产。手动一个个改效率低下且容易出错而简单的字符串替换又往往无法应对复杂的命名逻辑。今天我们就以处理这类影视资源文件名为例深入聊聊如何用 Python 从“一次性脚本”走向“健壮的文件名批处理引擎”。1. 文件名清洗远不止是去掉“【】”那么简单面对【正剧】寒渊渡骨・晚香如梦 第10集【投币200转免】.mp4这个文件名我们的目标可能很明确提取出核心剧名“寒渊渡骨・晚香如梦”和集数“第10集”并格式化为寒渊渡骨・晚香如梦 S01E10.mp4这样的标准样式。但直接动手写replace([正剧], ).replace([投币200转免], )就够了吗远远不够。这种硬编码的方式极其脆弱一旦遇到【预告】寒渊渡骨・晚香如梦 番外篇【1080P】.mp4或寒渊渡骨・晚香如梦-第十集.mp4脚本立刻失效。文件名清洗的核心挑战在于模式Pattern的多样性和不规则性。我们需要处理的不是一个固定字符串而是一类符合某种松散规则的文本。一个健壮的清洗流程应该包含以下几个层次模式识别与提取识别出文件名中的“有效信息块”如剧名、季号、集数、分辨率、来源标签等。噪音去除过滤掉无意义的装饰字符、宣传语、平台标识等。结构标准化将提取出的信息块按照目标格式如剧名 SxxExx.扩展名进行重组。冲突处理与日志记录处理重名文件、记录更改操作以便回滚。这听起来复杂但我们可以借助正则表达式re模块这个利器来分解任务。正则表达式的价值在于它允许我们用声明式的规则去匹配文本模式而不是命令式地指定每一个字符位置。2. 构建正则表达式像侦探一样拆解文件名让我们扮演一次“文本侦探”拆解【正剧】寒渊渡骨・晚香如梦 第10集【投币200转免】.mp4。剧名寒渊渡骨・晚香如梦。它通常由中文字符、可能存在的连接符・、-组成并且位于一些标签之后、集数标识之前。集数第10集。常见变体有第10集、EP10、10、第十集等。标签/噪音【正剧】、【投币200转免】。它们通常被【】、[]、()等符号包裹。扩展名.mp4。固定模式用于最后分离。基于此我们可以设计一组正则表达式模式import re # 示例文件名 filename 【正剧】寒渊渡骨・晚香如梦 第10集【投币200转免】.mp4 # 模式1匹配被【】或[]包围的标签视为噪音 tag_pattern r【[^】]*】|\[[^\]]*\] # 模式2匹配“第X集”或“EPX”或纯数字集数 episode_pattern r第(\d)集|EP(\d)|[Ee]p(\d)|(\d) # 模式3匹配剧名假设剧名由非数字、非标签字符组成且长度合理 # 这是一个简化版实际可能需要更精细的规则或排除法 title_pattern r([^【】\[\]\d]?)(?\s*第\d集|\s*EP\d|$)关键思路我们不是一步到位匹配出完美剧名而是通过分步剥离和提取。先去掉已知的噪音标签再从剩余部分中提取剧名和集数。集数提取要捕获数字部分(\d)剧名提取则需要一个“前瞻断言”(?...)来确保匹配到集数标识前停止。3. 从脚本到引擎设计可配置的批处理流程一个只能处理单一模式的脚本价值有限。我们需要一个可以配置、可以扩展的“引擎”。这个引擎的输入是原始文件名列表和一组处理规则输出是标准化后的文件名列表并完成实际的重命名操作。下面是一个引擎的核心框架设计import os import re import shutil from pathlib import Path import logging class FileNameNormalizer: def __init__(self, patterns): patterns: 一个字典定义各种需要匹配或替换的正则模式。 例如 patterns { remove_tags: [r【[^】]*】, r\[[^\]]*\]], extract_episode: r第(\d)集|EP(\d), title_cleanup: [(r[^\w\u4e00-\u9fff\-・\s], )], # 替换非期望字符 } self.patterns patterns self.setup_logging() def setup_logging(self): logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(rename.log), logging.StreamHandler()]) self.logger logging.getLogger(__name__) def normalize(self, old_name): 对单个文件名进行标准化处理 name, ext os.path.splitext(old_name) working_name name # 1. 去除标签 for pattern in self.patterns.get(remove_tags, []): working_name re.sub(pattern, , working_name) # 2. 提取集数 ep_num None ep_match re.search(self.patterns.get(extract_episode, ), working_name) if ep_match: # 从匹配组中找到第一个不是None的数字 ep_num next((g for g in ep_match.groups() if g is not None), None) # 从working_name中移除集数匹配部分避免干扰剧名提取 working_name re.sub(self.patterns[extract_episode], , working_name) # 3. 清理剧名去除两端空格替换内部多余空格 clean_title working_name.strip() for pattern, repl in self.patterns.get(title_cleanup, []): clean_title re.sub(pattern, repl, clean_title) # 合并多余空格 clean_title re.sub(r\s, , clean_title) # 4. 组装新文件名 new_name clean_title if ep_num: # 这里假设是单季季号固定为01。多季情况需要更复杂的逻辑。 new_name f S01E{int(ep_num):02d} new_name ext return new_name, {original: old_name, title: clean_title, episode: ep_num} def process_directory(self, directory_path, dry_runTrue): 处理整个目录下的文件 path Path(directory_path) if not path.exists() or not path.is_dir(): self.logger.error(f目录不存在或不是目录: {directory_path}) return processed_files [] for file_path in path.iterdir(): if file_path.is_file(): old_name file_path.name new_name, meta self.normalize(old_name) old_path file_path new_path file_path.parent / new_name if old_name ! new_name: processed_files.append((old_path, new_path, meta)) if dry_run: self.logger.info(f[模拟] 将重命名: {old_name} - {new_name}) else: try: shutil.move(str(old_path), str(new_path)) self.logger.info(f[执行] 已重命名: {old_name} - {new_name}) except Exception as e: self.logger.error(f重命名失败 {old_name}: {e}) self.logger.info(f处理完成。总计 {len(processed_files)} 个文件待处理/已处理。) return processed_files这个FileNameNormalizer类提供了一个基础框架。patterns字典是其核心允许你外部定义匹配规则。dry_run参数至关重要它允许你先模拟运行查看所有即将发生的更改确认无误后再实际执行。4. 应对边界情况让批处理更健壮上面的基础引擎能处理典型情况但真实世界充满意外。以下是几个必须考虑的边界情况及其处理策略4.1 剧名提取的模糊性([^【】\[\]\d]?)这个剧名模式可能匹配到多余内容比如“正剧高清”如果没被标签规则过滤掉。更稳健的方法是排除法和启发式规则。排除法先定义明确的“非剧名”模式如分辨率1080P、编码x264、语言CHS等并移除。启发式规则剧名通常有一定长度如大于2个字符且不会全是数字或英文。可以设置最小长度阈值。4.2 多季剧集识别S01E10中的季号01从哪里来如果文件名中没有明确季信息如第二季我们需要外部输入或通过上下文推断比如放在不同子目录下。可以在patterns中增加extract_season规则或者通过配置文件为不同目录指定季号。4.3 文件重名冲突当两个不同的原文件名被标准化为同一个新文件名时直接覆盖会导致数据丢失。解决方案冲突检测在process_directory中维护一个set记录已生成的新文件名。冲突解决自动添加后缀如剧名 S01E10 (1).mp4并记录到日志中。# 在 process_directory 循环内添加冲突解决 used_names set() for file_path in path.iterdir(): # ... 生成 new_name ... original_new_name new_name counter 1 while new_name in used_names: name_part, ext_part os.path.splitext(original_new_name) new_name f{name_part} ({counter}){ext_part} counter 1 used_names.add(new_name) # ... 后续处理 ...4.4 处理过程可追溯与回滚dry_run模式生成的日志是第一步。更安全的做法是在执行实际重命名前将原路径到新路径的映射关系保存到一个单独的 JSON 文件。如果需要回滚可以依据这个映射文件反向操作。import json def backup_rename_map(processed_files, map_filerename_map.json): mapping [{old: str(old), new: str(new)} for old, new, _ in processed_files] with open(map_file, w, encodingutf-8) as f: json.dump(mapping, f, indent2, ensure_asciiFalse)5. 超越重命名集成到媒体管理生态文件名标准化不是终点而是媒体资产管理的第一步。整理好的文件名可以无缝对接更强大的工具媒体服务器刮削如 Plex, Emby, Jellyfin。它们通常遵循剧名 (年份)/剧名 SxxExx.扩展名的命名约定。我们的脚本可以很容易地扩展创建并移动文件到对应的剧集目录下。批量信息下载配合tvdb-api或tmdb的 Python 库在重命名后可以用标准化的剧名和季集信息去自动查询并下载海报、剧集简介、演员表等元数据。与文件监控工具联动使用watchdog库监控某个下载文件夹一旦有新文件完成自动触发文件名标准化流程实现全自动化整理。最终我们得到的不仅仅是一个重命名脚本。我们得到的是一套处理“非结构化文本数据”的方法论定义模式 - 分离噪音与信号 - 提取关键信息 - 按规则重构 - 妥善处理异常。这套方法不仅适用于整理视频文件同样可以用于整理图片、文档、代码片段或是清洗任何具有潜在规律的文本数据。当你下次再面对一堆杂乱的文件时不妨先停下来思考一下它们的命名是否隐藏着某种规律能否用几条正则规则描述出来如果能那么自动化清洗的门就已经打开了。从一条正则表达式开始逐步构建起一个健壮的数据处理管道这正是编程思维在日常工作中最实用的体现之一。