资讯动态

老版《神秘博士》编号混乱?用Python与ffprobe规范媒体库文件

发布时间:2026/9/5 21:36:34 来源:尧图企业网站定制
这几年陆续收了一批老剧资源尤其是老版《神秘博士》Classic Doctor Who真正让我头疼的其实不是画质而是“编号”。比如打开《死亡特使》The Ambassadors of Death第三集文件名里赫然写着 S07E03但拿去给 Emby、Kodi 一刮削经常匹配出完全不同的结果到底应该按“第 7 季第 3 集”理解还是按“第 7 季第 3 个故事的第 3 集”理解这个细节如果没搞明白媒体库就会乱成一锅粥。本文就围绕这个问题讲清楚老版《神秘博士》这类“多集连续单元剧”的资源命名逻辑并给出一个用 Python 批量整理文件名的完整方案。大家可以把它当成一次“影视文件元数据规范化”的实战记录来看既能解决老剧归档的实际痛点也能学到一些文件整理、ffprobe 校验和媒体库刮削的通用技巧。1. 老版《神秘博士》的“季/集”为什么容易搞混1.1 老版与新版的结构差异很多朋友是从 2005 年重启版《神秘博士》开始入坑的。重启版的结构很清晰每一季由若干相对独立的剧集组成一集一个故事偶尔遇到上下集也会通过 Episode 编号连续排下去。所以看到 S07E03可以直接理解成“第 7 季第 3 集”。但老版《神秘博士》不一样。老版从 1963 年一直播到 1989 年在 2005 年重启前每一季内部通常是分成若干个“连续单元剧”英文里常叫 Serial / Story。每个 Serial 负责讲一个完整故事而这个完整故事又被拆成 4 到 10 集不等。以 1970 年的第 7 季为例这一季包含 4 个主要故事其中就包括《Spearhead from Space》《Doctor Who and the Silurians》《The Ambassadors of Death》《Inferno》。《死亡特使》正是第 7 季的第 3 个故事。因此标题里写到的“S07E03”和“The Ambassadors of Death Episode 3”放在一起正确含义更接近“第 7 季、第 3 个故事、第 3 集”而不是很多新入坑观众理解的那种“第 7 季的第 3 个独立剧集”。1.2 影视资料库眼中的老版编号如果你去查 TheTVDB 或者 TMDB 这类影视元数据库会发现老版《神秘博士》的数据建模方式比我们日常文件夹复杂得多。它需要同时管理“故事”和“分集”两层信息Story 维度描述某个故事属于哪一季、是第几个故事。Episode 维度描述某个分集在这个故事内部是第几集以及在整个季播出序列中是第几集。正是因为这个“两套数据维度”老剧收藏者最常踩的坑就是拿着文件名里的 S07E03 去匹配刮削器匹配出来的却是《Spearhead from Space》第三集或者干脆匹配到 2005 年新版《神秘博士》的第七季第三集因为新版的数据源更新、热度更高一些刮削器会优先命中新版条目。1.3 从技术上理解这个问题如果把这个场景抽象成数据问题大概可以这么理解文件名是“物理主键”影视数据库的编号是“逻辑主键”两者之间存在一个映射关系。我们要做的就是维护好这张映射表把物理文件名翻译成媒体库能够识别的逻辑编号。在整理大规模经典影集资源时大家经常会遇到类似情况不是所有视频站、种子资源站都遵循同一种命名规范。有人喜欢把老版《神秘博士》写成Doctor.Who.1963.S07E03有人写成Doctor Who (1963) - 7x03还有人会写成Doctor.Who.S07.E03.The.Ambassadors.of.Death。这些写法都会造成刮削混乱。所以先别急着抱怨服务器识别错本质是缺一个“从资源标题到数据库编号”的清洗规则。下面我们从工程化角度一步步解决。2. 环境准备与工具说明2.1 建议的工作目录结构在进行资源整理前建议把“下载/临时目录”和“媒体库目录”严格分开。这样即使某一步脚本出问题也不会污染已经整理好的媒体库。示例目录结构如下D:\classic-who-work ├── _raw # 原始文件建议只读不直接修改 │ ├── Doctor.Who.1963.S07E03.Part3.The.Ambassadors.of.Death.mkv │ └── Doctor.Who.1963.S07E03.Part3.The.Ambassadors.of.Death.ass ├── _renamed # 脚本输出目录放置整理后的文件 └── _logs # 重命名日志与报告在实际项目中大家可以根据自己的磁盘结构调整。比如《神秘博士》老版资源可能按季存放E:\TV\Doctor Who (1963) ├── Season 07 │ ├── Doctor Who (1963) - S07E01 - Spearhead from Space - Part 1.mkv │ ├── Doctor Who (1963) - S07E02 - Spearhead from Space - Part 2.mkv │ ├── ...但注意这个结构不一定能全网通用因为不同媒体服务器的解析规则不同。后面实战章节会给出一种更灵活的方案。2.2 Python 环境本文的脚本使用 Python 编写建议 Python 3.10 及以上版本。需要确保本机可以正常运行python命令。在 Windows 下可以通过 CMD 或 PowerShell 执行python --version如果输出版本号低于 3.10建议先升级或者至少使用 3.8否则有些新版语法特性可能不支持。为了减少环境依赖脚本不会使用第三方 Python 库只用标准库os、re、json、csv、shutil、argparse、pathlib。也就是说只要 Python 环境没问题代码拿到就能跑不需要执行pip install。2.3 FFmpeg 与 FFprobeFFprobe 是 FFmpeg 套件中的一个工具用来读取媒体文件信息比如编码格式、时长、分辨率、音轨数量、字幕轨道等。在检查视频文件是否完整时非常有用。FFmpeg 的安装方式因系统而异常见的是Windows从 FFmpeg 官网下载编译好的 Windows 版本解压后把bin目录加入系统 PATH。macOS可以通过 Homebrew 安装命令是brew install ffmpeg。Linux多数发行版通过包管理器安装比如sudo apt install ffmpeg。安装完成后建议验证一下ffprobe -version只要能看到版本信息就说明工具可用。由于 FFmpeg 版本迭代比较快本文示例不依赖某个特定版本不同版本的输出格式大体一致但个别字段会略有差异请以你本机实际输出为准。2.4 可选工具媒体管理服务器如果只是单纯整理文件不一定要安装 Emby、Jellyfin 或 Kodi。但如果想验证最终命名是否被正确刮削可以用 Jellyfin 或者 Kodi 搭建一个本地媒体库。Jellyfin 是开源软件适合作为本地测试环境不会额外收费。不过这不属于本文的核心内容后面只在排错章节里作为示范场景提到。3. 老版剧集元数据建模与命名思路3.1 拆解标题中的关键字段我们先把标题信息拆开看Classic Doctor Who S07E03 The Ambassadors Of Death Episode 3。这个标题可以解释成如下字段seriesDoctor WhoformatClassic表示老版season7story_index3说明这是第 7 季的第 3 个故事story_titleThe Ambassadors of Deathpart_index3说明这是该故事的第 3 集之所以出现“Episode 3”和“S07E03”同时出现正是因为老版资源在发布时习惯用“SxxEyy”表示“第几季第几个故事”再用“Episode z”表示“这个故事的第几集”。在媒体库里为了保持最终文件名的可读性通常希望得到这样一层信息剧集数据库里的 Season 7 剧集数据库里的 Episode 服务端匹配后计算出的分集顺序 故事标题 The Ambassadors of Death 分集序号 3问题在于不同影视数据库对“分集顺序”的计算方式不同。有的数据库会把这个故事的第 1 集映射成 Season 7 Episode 12有的可能映射成 Episode 15。如果只根据我们的记忆写死很容易在 Emby 里出现“文件内容与简介不符”的情况。3.2 用 JSON 构造映射表为了规避个人记忆偏差工程化的做法是把“从文件名中抽取到的 story_level_key”作为主键把“在某个数据库中确认过的 episode_number”作为映射结果保存成一份独立的 JSON 或 CSV 表。例如{ 07x03: { season: 7, story_index: 3, story_title: The Ambassadors of Death, total_parts: 7, database_episode_offset: 9 } }这里的database_episode_offset字段需要你通过 TheTVDB 或 TMDB 的官方页面确认不同站点数值可能不同。如果你在整理老版《神秘博士》时希望刮削器准确识别建议以 TheTVDB 当前数据为准不要凭记忆硬填。这个偏移量代表该故事的第 1 集相对于当前季第 1 个 EP 编号的偏移。有了它脚本就可以通过part_index计算出最终的 Episode 号最终 Episode 故事首集在季中的起始集号 part_index - 1当然如果你完全不想维护映射表也可以走另一条路径保留文件名的原始结构然后为该视频生成一个 NFO 文件。Kodi、Emby、Jellyfin 都支持通过 NFO 文件读取剧集信息这样即使文件名不那么“标准”也能被正确识别。后面第 5 节会写一个兼顾两种模式的脚本。3.3 多命名风格兼容整理老剧时我们最好对资源发布者的“恶趣味”有心理准备。同一个文件你可能会看到下面这些命名Doctor.Who.1963.S07E03.The.Ambassadors.of.Death.Part.3.mkv Doctor.Who.S07.E03.The.Ambassadors.of.Death.3of7.mkv Doctor Who (1963) - 7x03 - The Ambassadors of Death Episode 3.mkv 死亡特使.第三集.mkv正则表达式要做的事情是先把这些写法统一提取成下面的结构化信息season7 story_index3 part_index3 story_titleThe Ambassadors of Death如果没有 story_title只靠数字那么“07x03”是“第 3 个故事”还是“整个季播出顺序中的第 3 集”其实是无解的。这就是为什么老剧整理必须额外维护映射表不能只用正则全自动解决。影视文件的自动整理从来不是单纯的“文件重命名”而是一个小型的“元数据清洗”项目。4. 批量整理脚本实战4.1 脚本目标下面我们写一个完整的 Python 脚本用来批量整理目录下的《神秘博士》老版资源文件。它具备以下能力扫描指定目录。从文件名中抽取季、故事序号、分集序号。根据内置映射表生成标准化文件名。支持--dry-run默认只输出将要执行的变更不真正改文件。生成 CSV 报告方便回滚。可选把文件移动到整理目录。为了保护原始资源默认建议开启--dry-run跑一遍确认无误后再去掉该参数执行。4.2 完整脚本#!/usr/bin/env python3 # -*- coding: utf-8 -*- Classic Doctor Who 老版资源批量重命名脚本 使用方式 python rename_classic_who.py --root D:/classic-who-work/_raw --dry-run python rename_classic_who.py --root D:/classic-who-work/_raw --output D:/classic-who-work/_renamed import argparse import csv import re import shutil from pathlib import Path from typing import Dict, List, Optional, Tuple # ------------------------------------------------------------ # 1. 手动维护的映射表 # 每个 key 表示 S07E03 这类文件前缀映射到编剧层面的故事信息。 # 注意database_episode_offset 不是固定值请以 TheTVDB 实际数据为准。 # 如果某个故事在数据库中的首集集数是 10则 offset 10 - 1 9。 # ------------------------------------------------------------ STORY_MAP: Dict[str, Dict] { 07x01: { story_title: Spearhead from Space, total_parts: 4, database_episode_offset: 0, }, 07x02: { story_title: Doctor Who and the Silurians, total_parts: 7, database_episode_offset: 4, }, 07x03: { story_title: The Ambassadors of Death, total_parts: 7, database_episode_offset: 11, }, 07x04: { story_title: Inferno, total_parts: 7, database_episode_offset: 18, }, } # 输出目录里的剧集目录名这里直接用 1963 老版标识 SERIES_FOLDER_NAME Doctor Who (1963) # ------------------------------------------------------------ # 2. 从原始文件名抽取结构化信息 # ------------------------------------------------------------ def extract_story_key(filename: str) - Optional[Tuple[int, int]]: 尝试从文件名中匹配 S07E03 / 7x03 / S07.E03 等模式。 返回 (season, story_index)如果没有匹配则返回 None。 # 常见写法S07E03、S07.E03、7x03、7x03 patterns [ r[Ss](\d{1,2})[.\-_ ]?[Ee](\d{1,3}), r[Ss](\d{1,2})[.\-_ ]?[Ee](\d{1,3}), r(\d{1,2})[xX](\d{1,3}), r[Ss](\d{1,2})[.\-_ ]?[Ee](\d{1,3}) ] for pattern in patterns: match re.search(pattern, filename) if match: season int(match.group(1)) story_index int(match.group(2)) return season, story_index return None def extract_part_index(filename: str) - int: 尝试从文件名中抽取分集号比如 Part.3、EP3、Episode.3、3of7、第3集 如果抽取不到默认返回 1。 patterns [ r[Pp]art[\s._\-]?(\d{1,2}), r[Ee][Pp][\s._\-]?(\d{1,2}), r[Ee]pisode[\s._\-]?(\d{1,2}), r(\d{1,2})\s?of\s?\d{1,2}, r第\s?(\d{1,2})\s?集, ] for pattern in patterns: match re.search(pattern, filename) if match: return int(match.group(1)) return 1 def extract_file_suffix(path: Path) - str: 获取文件后缀保留 .mkv .mp4 等扩展名。 return path.suffix.lower() def parse_raw_file(path: Path) - Optional[Dict]: 解析单个文件得到可用于后续处理的字典。 返回 None 表示未识别到有效故事编号。 filename path.name story_key extract_story_key(filename) if not story_key: return None season, story_index story_key story_key_str f{season:02d}x{story_index:02d} story_info STORY_MAP.get(story_key_str) if not story_info: return None part_index extract_part_index(filename) return { source_path: path, story_key: story_key_str, season: season, story_index: story_index, part_index: part_index, story_title: story_info[story_title], total_parts: story_info[total_parts], offset: story_info[database_episode_offset], } # ------------------------------------------------------------ # 3. 生成标准化文件名 # ------------------------------------------------------------ def build_new_name(item: Dict, preferred_style: str episode) - str: 根据解析结果生成新的文件主名不含后缀。 style 参数 - episode生成类似 Doctor Who (1963) - S07E14 的命名 - story生成类似 Doctor Who (1963) - S07E03 - The Ambassadors of Death Part 3 的命名 season item[season] part_index item[part_index] story_title item[story_title] offset item[offset] if preferred_style episode: # 根据数据库偏移量计算在季中的连续集号 episode_number offset part_index # 保留 2 位季号与集号是为了方便通过文件资源管理器排序 return f{SERIES_FOLDER_NAME} - S{season:02d}E{episode_number:02d} - {story_title} - Part {part_index} # story 风格保留 SxxExx 作为故事编号 story_index item[story_index] return f{SERIES_FOLDER_NAME} - S{season:02d}E{story_index:02d} - {story_title} Part {part_index} def build_destination_path(root: Path, item: Dict, style: str) - Path: 构造目标路径。 如果 root 为 None则文件原地重命名否则将文件移动到 root 下。 new_name build_new_name(item, style) suffix extract_file_suffix(item[source_path]) new_filename new_name suffix if root is None: return item[source_path].with_name(new_filename) season_folder root / SERIES_FOLDER_NAME / fSeason {item[season]:02d} return season_folder / new_filename # ------------------------------------------------------------ # 4. 扫描目录并执行重命名/移动 # ------------------------------------------------------------ def scan_directory(root: Path) - List[Dict]: 扫描目录中所有视频与字幕文件。 video_extensions {.mkv, .mp4, .avi, .ts, .m2ts, .mov} subtitle_extensions {.srt, .ass, .ssa, .vtt, .sub} items [] for path in root.rglob(*): if not path.is_file(): continue if path.suffix.lower() in video_extensions or path.suffix.lower() in subtitle_extensions: parsed parse_raw_file(path) if parsed: parsed[is_video] path.suffix.lower() in video_extensions items.append(parsed) return items def rename_files(items: List[Dict], output_root: Optional[Path], dry_run: bool, style: str) - str: 执行重命名并返回 CSV 报告内容。 report_rows [] for item in items: src item[source_path] dst build_destination_path(output_root, item, style) action move if output_root and dst.parent ! src.parent else rename # 如果目标文件已存在则添加序号避免覆盖 counter 1 original_dst dst while dst.exists(): dst original_dst.with_name( original_dst.stem f_{counter} original_dst.suffix ) counter 1 report_rows.append({ source: str(src), destination: str(dst), action: action, story_key: item[story_key], part_index: item[part_index], dry_run: str(dry_run), }) if dry_run: print(f[DRY RUN] {action}: {src.name} - {dst}) else: if dst.parent ! src.parent: dst.parent.mkdir(parentsTrue, exist_okTrue) shutil.move(str(src), str(dst)) print(f[OK] {action}: {src.name} - {dst}) return report_rows def write_report(report_rows: List[Dict], report_path: Path) - None: 把报告写入 CSV方便后续核对或恢复。 report_path.parent.mkdir(parentsTrue, exist_okTrue) with open(report_path, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnames[source, destination, action, story_key, part_index, dry_run]) writer.writeheader() writer.writerows(report_rows) print(f报告已写入: {report_path}) # ------------------------------------------------------------ # 5. 主函数入口 # ------------------------------------------------------------ def main(): parser argparse.ArgumentParser(description整理 Classic Doctor Who 老版资源) parser.add_argument(--root, requiredTrue, help原始资源所在目录) parser.add_argument(--output, requiredFalse, help整理后的目录不填则原地重命名) parser.add_argument(--dry-run, actionstore_true, help只预览不修改) parser.add_argument(--style, choices[episode, story], defaultepisode, help输出命名风格) parser.add_argument(--report, requiredFalse, defaultrename_report.csv, helpCSV 报告路径) args parser.parse_args() root Path(args.root) if not root.exists(): print(f目录不存在: {root}) return output_root Path(args.output) if args.output else None items scan_directory(root) if not items: print(未识别到任何符合规则的文件请检查目录中是否有 S07E03、7x03 之类的命名。) return # 先进行 dry-run展示完整计划 print(f共识别到 {len(items)} 个文件下面列出的操作均未真正执行。) report_rows rename_files(items, output_root, dry_runTrue, styleargs.style) if not args.dry_run: write_report(report_rows, Path(args.report)) else: # 只是演示时保留 dry_run 报告也可以写 write_report(report_rows, Path(args.report)) print(当前为 dry-run 模式未修改任何文件。确认无误后请去掉 --dry-run 再执行一次。) if __name__ __main__: main()4.3 代码说明这个脚本虽然不短但逻辑非常清晰核心是三个部分。第一部分是STORY_MAP。它相当于我们的“人工知识库”管理 season、story_index、story_title、total_parts 以及数据库偏移量。老版《神秘博士》由于年代久远不同资料站对分集编号的统计口径不同所以我把最容易被搞错的database_episode_offset也暴露成配置项。你可以去 TheTVDB 搜索对应剧集查看实际 Season 7 中《死亡特使》第 1 集显示的 Episode number然后填入这里。第二部分是解析函数。extract_story_key()通过正则尝试多种写法包括S07E03、7x03、S07.E03等。注意正则匹配在真实文件名中会有很多边界情况比如文件名里同时出现好几次年份或集数因此这个写法针对一个文件只取第一次匹配。如果发现有的文件没有正确解析你可以先单独打印filename再调整正则。第三部分是重命名逻辑。build_new_name()支持两种风格episode风格生成类似Doctor Who (1963) - S07E14 - The Ambassadors of Death - Part 3.mkv这种命名更容易被 Emby/Jellyfin 按标准连续剧处理。story风格生成类似Doctor Who (1963) - S07E03 - The Ambassadors of Death Part 3.mkv这种命名保真度高适合对老剧编号有执念的收藏者。个人建议优先使用episode风格因为媒体服务器的刮削器更喜欢这种连续编号形式。但如果你搞不清数据库里的正确编号宁可使用story风格也不要张冠李戴。脚本在运行前默认会走一次 dry-run并把报告写到rename_report.csv。这份 CSV 很有用一旦重命名后发现问题可以根据报告里的source和destination恢复原名。恢复时可以用下面的 Python 代码import pandas as pd # 仅演示思路也可以使用 csv 标准库 df pd.read_csv(rename_report.csv) for _, row in df.iterrows(): # 实际执行时请先确认目标文件存在且原路径没有被占用 print(row[destination], -, row[source])不过在没有任何 Python 环境的情况下也可以直接打开 CSV 表格把第一列当作原路径手动恢复。4.4 运行效果演示假设在_raw目录下存在下面的文件Doctor.Who.1963.S07E03.Part3.The.Ambassadors.of.Death.mkv Doctor.Who.1963.S07E03.Part3.The.Ambassadors.of.Death.srt执行python rename_classic_who.py --root D:/classic-who-work/_raw --dry-run --report D:/classic-who-work/_logs/rename_report.csv预期输出类似于共识别到 2 个文件下面列出的操作均未真正执行。 [DRY RUN] rename: Doctor.Who.1963.S07E03.Part3.The.Ambassadors.of.Death.mkv - Doctor Who (1963) - S07E14 - The Ambassadors of Death - Part 3.mkv [DRY RUN] rename: Doctor.Who.1963.S07E03.Part3.The.Ambassadors.of.Death.srt - Doctor Who (1963) - S07E14 - The Ambassadors of Death - Part 3.srt 报告已写入: D:/classic-who-work/_logs/rename_report.csv注意这里示例中的 S07E14 是根据database_episode_offset11计算出来的。如果你的数据库显示第 14 集那才正确。如果实际上第 14 集是另一个故事的内容说明偏移量设置错误你需要修改后再跑。当确认 dry-run 结果无误后再执行真正重命名python rename_classic_who.py --root D:/classic-who-work/_raw --output D:/classic-who-work/_renamed --report D:/classic-who-work/_logs/rename_report.csv此时脚本会把整理后的文件放到_renamed目录下并自动创建Doctor Who (1963)/Season 07文件夹。如果不想移动只希望原地改名则省略--output。4.5 校验文件完整性重命名之后最好对文件做一次基础校验。用 FFprobe 可以快速看视频文件是否完整、时长是否合理ffprobe -v error -show_entries formatfilename,duration,size -of defaultnoprint_wrappers1 Doctor Who (1963) - S07E14 - The Ambassadors of Death - Part 3.mkv输出看起来大致是filenameDoctor Who (1963) - S07E14 - The Ambassadors of Death - Part 3.mkv duration1500.000000 size1234567890如果ffprobe报错比如No such file or directory大概率是文件名中的空格或特殊字符导致路径没有正确转义。在 Windows CMD 中需要给文件路径加英文双引号。如果duration明显过短比如一个剧集时长只有几十秒说明这个文件可能下载不完整建议回原始来源补档。这一步和重命名没有直接关系但在整理大影集时非常必要。5. 常见问题与排查思路5.1 Emby/Jellyfin 匹配到 2005 版《神秘博士》这是老版剧集刮削最常见的问题。原因是文件命名中没有年份标识而刮削器默认匹配到热度更高、封面资料更全的新版。解决方案是在文件名中加入(1963)例如Doctor Who (1963) - S07E14 - Part 3.mkv。如果还是匹配错误可以在 Emby/Jellyfin 的元数据管理里手动指定 TheTVDB ID 或 TMDB ID。不同服务器操作入口不一样但思路一致手动“锁定”到老版条目避免服务器再次自动识别。5.2 S07E03 被识别成其他剧集这个问题的根源是故事编号与连续集号不一致。如果你坚持用Doctor.Who.1963.S07E03.The.Ambassadors.of.Death.Part.3.mkv这种命名媒体服务器很可能把S07E03当作整季播出顺序的第 3 集于是匹配到《Spearhead from Space》的某一集。排查时先打开该剧在 TheTVDB 的页面查看 Season 7 的完整分集列表找到《死亡特使》第 3 集在服务器中的 Episode number。然后据此修改映射表。这一步骤没有捷径属于典型的“本地命名体系”与“平台编号体系”之间的数据对齐。5.3 外挂字幕匹配不上很多老剧资源会附带字幕文件。如果视频文件是Doctor Who (1963) - S07E14 - Part 3.mkv字幕文件也需要改成同样的名字比如Doctor Who (1963) - S07E14 - Part 3.srt否则播放器无法自动加载。上面的脚本已经自动处理了字幕文件因为在扫描时会把.srt、.ass等扩展名一并纳入解析。如果你遇到了“视频有声音但没有中文字幕”的情况可以先检查是不是文件名不一致再用播放器手动挂载字幕测试。如果字幕时间轴整体偏移可以先用 FFplay 手动播放并记录偏移时间再使用 Subtitle Edit 或 FFmpeg 的字幕延迟参数调整。字幕处理是一个相对独立的话题这里不展开。5.4 重命名后做种失效如果你从 PTPrivate Tracker下载了资源并正在做种那么直接重命名文件、移动目录都会导致种子校验失败影响下载贡献情况严重时会影响账号考核。最佳实践是重命名操作只针对“已经完成下载、不参与做种、用于本地归档”的文件。如果还在做种请先复制一份到临时目录再对副本执行整理。千万不要为了美观而直接改正在做种的文件名。从工程角度讲资源和归档最好分离这也是第 2 节建议“一个原始目录一个媒体库目录”的原因。5.5 脚本解析不到部分文件主要原因是文件的命名风格差异过大比如名称里没有Part、没有Episode只写了The Ambassadors of Death 3。这种情况下脚本无法自动判断3是分集序号所以得不到正确结果。有两种处理办法一种是先手动把极少数不规则的视频文件重命名成统一格式再让脚本处理其余文件另一种是在脚本里增加自定义正则规则。考虑到人工处理几十个文件比较麻烦个人更建议先统一文件命名风格避免为了少数异类文件编写过于复杂的正则。6. 最佳实践与工程建议6.1 把“人工知识”从代码里分离很多朋友整理影视资源时会把这些规则写在“心里”实际上最好写进代码或者配置文件里。本文的STORY_MAP本质上就是知识配置。你可以把它单独放到story_map.json中这样以后遇到另一部老剧比如《神秘博士》第 8 季、第 9 季只需要维护 JSON 配置而不需要改动主脚本。推荐的结构是configs/ doctor_who_1963_season07.json scripts/ rename_classic_who.py raw/ ... output/ ... logs/ ...在代码里读取 JSONimport json with open(configs/doctor_who_1963_season07.json, r, encodingutf-8) as f: STORY_MAP json.load(f)这样做的好处很明显过几个月回头维护时不需要翻开 Python 源码只需在配置里修改编号和标题映射脚本就能复用。6.2 始终先跑 dry-run任何批量重命名脚本都必须支持 dry-run。不要觉得这一步多余。真实场景中一次随手批量重命名可能影响几百个文件。误操作后恢复成本很高尤其是文件名里如果带着中文、空格、方括号等特殊字符不同操作系统之间的文件名兼容性问题也会暴露出来。哪怕不写脚本在 Windows / macOS / Linux 上手动重命名文件时也要养成“先列清单再执行”的习惯。这条经验不仅适用于老版《神秘博士》也适用于所有大规模数字资源的整理。6.3 保留原始文件名记录建议每次批量操作都输出一份 CSV 报告。这份报告有两个用途出错时快速回滚。反向追溯“这个文件原来叫什么”。如果在媒体库刮削后依然有问题我们通常还需要回到source路径查看原始发布的命名格式确认是不是发布者使用了另一种编号体系。这些原始信息在重命名后会被覆盖所以报告要保留在单独目录比如_logs。6.4 视频文件校验与备份对于收藏型老剧资源文件完整性远比美观文件名重要。重命名操作一般不会破坏文件内容但在移动大文件时仍要注意磁盘空间是否足够。如果磁盘空间不足shutil.move可能移动一半后失败。简单做法是在移动前用脚本检查目标磁盘剩余空间。标准库shutil.disk_usage()可以帮你获取磁盘信息。更稳妥的方案是先对视频文件做一次哈希校验比如计算 SHA-256移动之后再算一次。哈希校验计算量相对较大可以在夜间批量任务里执行日常整理则不必每次计算。6.5 安全与版权提醒老版《神秘博士》是 BBC 的版权作品。这篇文章讨论的是如何整理“你合法获取的数字化资源”包括正版 DVD 的私人备份、已进入合理使用范围或你拥有相应授权的内容。请务必遵守所在地法律法规和平台规则不要传播盗版资源也不要从非正规渠道下载内容。在整理网络资源时来自 PT 站或 BT 站的文件要注意做种规则保护自己的账号权益。整理过程中如果发现某个文件来源可疑文件名带有明显盗录小组标志建议直接删除或放弃使用。收藏影集是为了长期观看不是给系统制造安全风险。6.6 考虑后续自动化扩展当你掌握了基础的“文件名清洗”思路后可以进一步扩展接入 TheTVDB API自动获取故事标题、分集编号、首播日期减少人工维护成本。接入 TMDB API用于下载海报、背景图和演职员信息。在重命名完成后调用 Jellyfin API 触发媒体库刷新。把脚本放到 NAS 上配合定时任务定期检查下载目录。本文的脚本刻意没有引入这些外部接口是为了保证作为教程的“最小可运行”特性。当你真正理解了映射关系再封装成自动化任务就不难了。7. 总结与进一步整理方向老版《神秘博士》的资源整理表面上是“改文件名”实际上是在做两件事一是把不同发布者的命名风格统一成自己的归档规范二是把资源文件和影视数据库的编号建立起可信的映射关系。理解这一点后再去处理其他经典剧集比如老《星际迷航》、经典科幻剧、动画长剧思路都是通用的。本文给出了一个可直接运行的 Python 重命名脚本支持STORY_MAP配置、dry-run 预览、CSV 日志和字幕同步处理并演示了如何使用 FFprobe 校验文件完整性。哪怕你完全没写过 Python只要按照前两节把环境准备好也可以把脚本复制下来把root改为自己的目录跑一次看看效果。如果你正准备搭建自己的经典剧集媒体库建议下一步先做三件事把散乱的原始资源先按“原始目录 / 整理目录 / 日志目录”分层。去 TheTVDB 或 TMDB 核对一遍目标季的完整分集列表把结果填进STORY_MAP。使用 dry-run 模式在副本上测试整批重命名确认无误后再对正式文件执行。老剧编号的混乱是历史遗留问题很难靠某一个播放器自动解决。能真正解决问题的始终是自己维护的那张映射表。如果这篇文章对你整理《神秘博士》或其他老剧有启发可以收藏备用后续遇到命名混乱时再对照操作。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价