十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python从零构建追番剧集管理命令行工具

用Python从零构建追番剧集管理命令行工具 其实一开始我只是想记录一下《宝可梦地平线》台配版的追番进度比如第 80 集“在第零区有超帅宝可梦”什么时候更新、看到哪一集。但很快发现单靠备忘录根本撑不住一是像“第零区”这样的剧情分区越攒越多二是同一部动画既有台配也有日配文件或标题一多检索就变得非常痛苦。换一个角度看追番信息本身就是结构化数据完全可以交给一个小工具来管理。本文就用 Python 从零写一个“宝可梦地平线剧集信息管理 CLI”覆盖添加剧集、按分区检索、按配音版本过滤、观看状态标记、收藏切换以及使用 ffprobe 检测音轨语言等方法。写完后你不仅能把这部动画整理得清清楚楚也能顺手复用到其他追番场景。1. 追番信息为什么需要程序化管理1.1 一条剧集信息包含多少字段先想象一下你要记录《宝可梦地平线》这部动画单单一集的数据就包含集数、标题、动画分区例如“第零区”、配音版本台配、日配、观看状态、收藏状态、备注、更新日期等。如果只是三到五集Excel 或备忘录都够用。但当集数到几十甚至上百并且同时追多部动画时靠肉眼很难快速回答下面这些问题第 80 集更新了没有台配版我已经看到哪一集了日配版看到哪一集了“第零区”相关的集数一共有多少哪一集还没看哪一集是收藏的某个 BD 或 TV 版的音轨语言是否标记正确这些问题的本质是“筛选”和“统计”。只要数据字段清晰用脚本就能轻松解决这比用笔记一条条翻要可靠得多。1.2 常见管理痛点和解决思路最常见的痛点大概有三个。第一信息分散在多个平台笔记里记录进度聊天记录里收藏了链接视频网站上标记了追番但彼此不同步。第二版本混淆同一集如果有台配、日配、BD 版文件名不完全一致时间久了根本分不清哪个是哪个。第三无法自动统计看完一集以后还要手动改表格改多了容易漏。解决思路也很简单把剧集数据统一放到一个本地 JSON 文件里通过命令行工具进行增删改查。这样数据是集中的操作是结构化的后续还可以迁移到 SQLite 或对接 AniList 这类开放 API。以“台配”这个属性为例只要在数据里设计一个dub_versions字段就能随时筛选出“当前有哪些台配剧集已更新”。1.3 本文技术方案与适用人群本文会实现一个基于 Python 标准库argparse、json、dataclasses、pathlib和collections.Counter的小工具不依赖复杂的外部框架也不需要数据库开箱即用。适用人群包括正在追《宝可梦地平线》或其他长番的观众想练习 Python 命令行工具开发的初学者以及希望把文件音轨信息管理得更规范的个人用户。读完成这一篇你会掌握如何设计一个稳定的剧集数据模型、如何实现 JSON 持久化、如何用命令行参数完成增删改查、如何按“第零区”这种剧情分区做统计以及如何用 ffprobe 检查音轨语言从而确认台配音轨是否匹配。2. 环境准备与项目初始化2.1 开发环境版本约定本文示例在以下环境中验证过但版本本身不是硬性要求你只需要保证 Python 3.9 及以上即可因为代码中使用了list[str]和dataclasses较低版本需要做兼容调整。操作系统Windows 10/11、macOS、Linux 均可Python3.9 及以上命令行工具系统自带终端或 VS Code 终端可选工具FFmpeg / FFprobe用于检查音轨语言如果你的 Python 版本较旧建议先升级或者在项目里使用虚拟环境安装新版本 Python。2.2 初始化项目结构为了让数据、代码和依赖分离建议使用下面这个目录结构anime_tracker/ ├── data/ │ └── pokemon_horizons.json ├── scripts/ │ └── tracker.py └── requirements.txt在终端中执行以下命令创建目录mkdir -p anime_tracker/data anime_tracker/scripts cd anime_tracker touch requirements.txt本文的工具只需要标准库所以requirements.txt可以暂时为空。如果你的环境需要安装ffprobe相关工具可以单独安装 FFmpeg这部分和 Python 依赖无关。2.3 准备虚拟环境与依赖虽然这里不依赖第三方包但为项目创建虚拟环境仍然是个好习惯。它能让项目环境彼此隔离避免未来引入新依赖时污染全局 Python。cd anime_tracker python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate python --version后面所有python tracker.py命令都建议在虚拟环境中运行。如果终端中python不可用可以换成python3根据实际环境调整即可。3. 核心设计剧集信息模型与剧情分区3.1 剧集数据字段怎么设计写代码之前先确定数据模型。这个模型是后续所有功能的基石字段设计得越合理后面检索越方便。我将一条剧集记录设计为包含下面几个字段number集数整数唯一索引用来定位记录。title标题字符串例如“在第零区有超帅宝可梦”。dub_versions列表例如[台配, 日配]表示可用的配音版本。zone剧情分区字符串例如“第零区”“帕底亚地区”。watched布尔值表示是否已观看。favorite布尔值表示是否收藏。notes备注字符串。update_date更新日期字符串推荐使用YYYY-MM-DD格式。设计成列表字段而不是单个字符串是因为同一集可能同时有台配和日配两种版本。如果使用单个字段就只能存“台配/日配”这种拼接字符串后期过滤会很麻烦。3.2 “第零区”与分区统计标题中提到的“第零区”在数据模型里就是一个普通的zone字段值。你可以在录入剧集时把它当作分区标签然后在统计功能中对该字段做聚合统计。这样做的好处是无论动画后续出现多少新区域比如“帕底亚地区”“第零区”你都不需要修改代码只需要在数据里增加记录即可。从技术角度看这就是“维度建模”的简单应用。zone作为维度字段watched和favorite作为状态字段dub_versions作为多值属性。把它们分开存储后续就可以组合出很多查询比如“第零区中已看过且是台配版的剧集有哪些”。虽然这是一个很小的个人工具但设计思路和业务系统中的宽表是相通的。3.3 为什么先用 JSON不用数据库对于个人追番这种规模的数据JSON 是启动成本最低的方案。Python 标准库的json模块直接支持读写文件可读性高能直接放进 Git 做版本管理。只有当数据量很大或者需要复杂关联查询时才值得迁移到 SQLite。JSON 的缺点是并发写不安全但这在个人单机工具里完全不是问题。另一个缺点是查询需要全量加载但几千条记录的扫描对现代 CPU 来说几乎无感知。如果你想之后升级也能比较方便地把 JSON 导入 SQLite。4. 完整实战搭建剧集管理 CLI接下来我们进入核心实战环节先编写数据模型和读写函数再实现检索、统计和命令行入口最后运行验证。4.1 编写数据模型在scripts/tracker.py中先用dataclass定义Episode。使用dataclass的好处是代码简洁、字段明确默认值也好管理。# 文件路径scripts/tracker.py import argparse import json from dataclasses import dataclass, asdict from datetime import date from pathlib import Path from collections import Counter BASE_DIR Path(__file__).resolve().parent.parent DATA_DIR BASE_DIR / data DATA_FILE DATA_DIR / pokemon_horizons.json dataclass class Episode: number: int title: str dub_versions: list[str] zone: str watched: bool False favorite: bool False notes: str update_date: str def to_dict(self): return asdict(self) classmethod def from_dict(cls, data): return cls(**data)to_dict负责把对象转成字典方便保存为 JSON。from_dict负责反向转换方便读取时还原成对象。这里使用cls(**data)时要求 JSON 中的字段名和dataclass字段完全一致所以在写数据文件时要注意字段命名。4.2 实现 JSON 读取与写入接下来是读写层。load_episodes读取整个 JSON 文件并转换成对象列表save_episodes负责把内存数据写回文件。如果数据文件不存在会自动创建空列表。def load_episodes() - list[Episode]: if not DATA_FILE.exists(): save_episodes([]) with open(DATA_FILE, r, encodingutf-8) as f: raw_list json.load(f) return [Episode.from_dict(item) for item in raw_list] def save_episodes(episodes: list[Episode]) - None: DATA_DIR.mkdir(parentsTrue, exist_okTrue) with open(DATA_FILE, w, encodingutf-8) as f: json.dump( [ep.to_dict() for ep in episodes], f, ensure_asciiFalse, indent2, )这里有两个细节值得注意。第一所有文件读写都显式指定encodingutf-8避免在 Windows 环境下出现中文乱码。第二写入时设置ensure_asciiFalse这样 JSON 文件里会直接保存中文标题比如“第零区”而不是\u7b2c\u96f6\u533a这种转义字符文件可读性更好。如果 JSON 文件内容本身有语法错误程序会在json.load阶段抛出JSONDecodeError。这个问题我们在后面“常见问题”中会专门讨论。4.3 实现检索、标记与统计功能下面实现核心业务逻辑。list_episodes支持按分区、配音版本和观看状态过滤。mark_watched和toggle_favorite负责更新状态。stats_by_zone和stats_by_dub负责统计。def list_episodes(episodes, zoneNone, dubNone, unwatchedFalse): result [] for ep in episodes: if zone and ep.zone ! zone: continue if dub and dub not in ep.dub_versions: continue if unwatched and ep.watched: continue result.append(ep) return result def find_episode(episodes, number): for ep in episodes: if ep.number number: return ep return None def mark_watched(episodes, number): ep find_episode(episodes, number) if ep is None: print(f未找到集数 {number}) return False ep.watched True ep.update_date date.today().isoformat() return True def toggle_favorite(episodes, number): ep find_episode(episodes, number) if ep is None: print(f未找到集数 {number}) return False ep.favorite not ep.favorite return True def stats_by_zone(episodes): counter Counter(ep.zone for ep in episodes) for zone, count in counter.most_common(): watched sum(1 for ep in episodes if ep.zone zone and ep.watched) print(f{zone}: 共 {count} 集已观看 {watched} 集) return counter def stats_by_dub(episodes): dub_counter Counter() for ep in episodes: for dub in ep.dub_versions: dub_counter[dub] 1 for dub, count in dub_counter.most_common(): print(f{dub}: {count} 集) return dub_counter这里的list_episodes使用组合条件过滤当多个条件同时传入时只返回同时满足所有条件的记录。mark_watched更新观看状态后自动把update_date设置为当天日期方便后续查看最近标记时间。stats_by_zone用Counter按分区聚合并额外计算每个分区的已观看数这样“第零区”这样的分区有多少集、看了多少集一眼就能看出来。4.4 实现命令行入口为了让工具真正可用我们使用argparse搭建命令行入口。主要支持add、list、watch、fav、stats五个子命令。def add_episode(episodes, number, title, dub_versions, zone, favoriteFalse): if find_episode(episodes, number) is not None: print(f剧集 {number} 已存在如需修改请扩展 update 命令) return ep Episode( numbernumber, titletitle, dub_versionsdub_versions, zonezone, favoritefavorite, update_datedate.today().isoformat(), ) episodes.append(ep) def main(): parser argparse.ArgumentParser( description宝可梦地平线剧集信息管理工具 ) sub parser.add_subparsers(destcommand) add_p sub.add_parser(add, help添加剧集) add_p.add_argument(--number, typeint, requiredTrue) add_p.add_argument(--title, requiredTrue) add_p.add_argument(--dub, nargs, default[日配], help配音版本可传多个) add_p.add_argument(--zone, default待定) add_p.add_argument(--favorite, actionstore_true) list_p sub.add_parser(list, help列出剧集) list_p.add_argument(--zone, help按剧情分区过滤) list_p.add_argument(--dub, help按配音版本过滤) list_p.add_argument(--unwatched, actionstore_true, help只看未观看) watch_p sub.add_parser(watch, help标记已观看) watch_p.add_argument(--number, typeint, requiredTrue) fav_p sub.add_parser(fav, help切换收藏) fav_p.add_argument(--number, typeint, requiredTrue) sub.add_parser(stats, help统计分区和配音版本) args parser.parse_args() episodes load_episodes() if args.command add: add_episode( episodes, args.number, args.title, list(args.dub), args.zone, args.favorite, ) save_episodes(episodes) elif args.command list: result list_episodes( episodes, zoneargs.zone, dubargs.dub, unwatchedargs.unwatched, ) for ep in result: status 已观看 if ep.watched else 未观看 fav ★ if ep.favorite else ☆ print(f[{fav}] 第{ep.number}集 | {ep.title} | {ep.zone} | {status} | {,.join(ep.dub_versions)}) elif args.command watch: if mark_watched(episodes, args.number): save_episodes(episodes) print(f第{args.number}集已标记为观看) elif args.command fav: if toggle_favorite(episodes, args.number): save_episodes(episodes) print(f第{args.number}集收藏状态已切换) elif args.command stats: print(按分区统计) stats_by_zone(episodes) print(按配音版本统计) stats_by_dub(episodes) else: parser.print_help() if __name__ __main__: main()这段代码把命令行参数和业务函数联系起来。当用户输入add时程序读取参数并创建新剧集记录输入list时支持三个可选过滤条件输入watch或fav时更新状态输入stats时输出统计结果。所有修改操作完成后都会调用save_episodes确保数据持久化到 JSON 文件。4.5 运行与验证现在我们来添加一条示例数据。这里以用户给出的标题为例添加第 80 集配音版本为台配分区为第零区并设置收藏状态。python scripts/tracker.py add --number 80 --title 在第零区有超帅宝可梦 --dub 台配 --zone 第零区 --favorite执行完成后可以查看 JSON 文件内容cat data/pokemon_horizons.json预期输出类似下面这样字段顺序可能略有不同但内容是完整的{ number: 80, title: 在第零区有超帅宝可梦, dub_versions: [ 台配 ], zone: 第零区, watched: false, favorite: true, notes: , update_date: 2025-02-10 }接着列出所有剧集python scripts/tracker.py list预期输出[★] 第80集 | 在第零区有超帅宝可梦 | 第零区 | 未观看 | 台配再执行统计命令python scripts/tracker.py stats预期输出按分区统计 第零区: 共 1 集已观看 0 集 按配音版本统计 台配: 1 集最后标记观看并再次查看python scripts/tracker.py watch --number 80 python scripts/tracker.py list --zone 第零区 --dub 台配预期输出[★] 第80集 | 在第零区有超帅宝可梦 | 第零区 | 已观看 | 台配到这里一个最小的剧集信息管理工具已经可以正常工作了。4.6 音轨标记用 ffprobe 查看台配/日配语言数据层面的dub_versions只是我们手动记录的属性。如果本地有视频文件还可以用 FFmpeg 自带的ffprobe来查看音轨语言标识辅助确认某个文件是否是台配。比如假设你有一个文件名为PokemonHorizons_S01E80.mkv的本地视频可以执行ffprobe -v error -show_entries streamindex,codec_type:stream_tagslanguage -of compact PokemonHorizons_S01E80.mkv输出通常会包含类似下面的内容stream|index0|codec_typevideo|tags:languageund stream|index1|codec_typeaudio|tags:languagejpn stream|index2|codec_typeaudio|tags:languagezho这里的jpn表示日语zho表示中文。如果音轨语言标记不标准可能显示为chi或und。不同压制组使用的语言代码可能不一样所以这个命令只能作为辅助参考不能完全替代人工确认。更可靠的做法是配合播放器实际试听或者直接基于我们前面维护的dub_versions字段记录版本信息。5. 常见问题与排查清单5.1 高频报错与处理在运行过程中新手最容易遇到下面几种问题。我把它们整理成表格方便快速定位。问题现象常见原因解决思路ModuleNotFoundError: No module named xxx没有安装第三方包或没有激活虚拟环境检查pip list激活虚拟环境后安装依赖JSONDecodeErrordata/pokemon_horizons.json文件被手动改坏或编码不是 UTF-8备份后重置 JSON 文件使用encodingutf-8读取中文显示乱码终端编码不是 UTF-8或文件读写未指定编码Windows 终端执行chcp 65001代码中统一encodingutf-8PermissionErrordata 目录或 JSON 文件没有写权限检查文件权限确认当前用户有读写权限找不到剧集未找到集数 80数据文件中没有该集数或 number 类型不匹配用list命令查看已有记录确认集数类型为整数ffprobe命令不存在只安装了 Python未安装 FFmpeg安装 FFmpeg 后重试或者改用播放器属性查看5.2 典型排查步骤如果程序运行结果和预期不符建议按下面的顺序排查先确认项目路径。命令需要在anime_tracker根目录下执行否则Path(__file__).resolve().parent.parent计算出的路径会对但如果你直接把脚本复制到其他目录需要同步修改BASE_DIR的计算方式。检查 JSON 文件是否合法。如果手动编辑过可能少了逗号或引号导致json.load无法解析。确认你执行的是save_episodes后的最新文件。某些编辑器会缓存文件内容建议重新打开或执行cat查看实际内容。检查命令行参数是否正确。--dub使用了nargs所以传多个版本时要写成--dub 台配 日配不能写成--dub台配,日配。最后再确认 Python 版本。3.9 以下版本对list[str]的解析会有问题需要改成List[str]并导入typing。6. 最佳实践与工程建议6.1 数据与代码分离本文把 JSON 数据放在独立的data目录而不是直接放在脚本目录目的就是数据与代码分离。这样做的好处是后续更新代码时不用担心数据被覆盖使用 Git 管理时也可以把代码和示例数据分开提交或者通过.gitignore忽略个人数据文件。如果你的数据需要保密只要把data/加入.gitignore即可。6.2 命名规范与备份剧集本地文件的命名也建议统一。比如PokemonHorizons_S01E80_台配.mkv、PokemonHorizons_S01E80_日配.mkv。这样即使不打开播放器也能从文件名快速判断版本。在 JSON 里录入dub_versions时尽量使用固定的枚举值比如“台配”“日配”不要一会儿写“台配”一会儿写“中文配音”否则统计时会出现两个完全不同的分类。此外JSON 文件一定要做好备份。虽然它只是一个文本文件但一旦误操作清空追番进度就会丢失。可以定期把data/pokemon_horizons.json复制到备份目录或者纳入 Git 版本管理。6.3 功能扩展方向目前的 CLI 只是一个非常基础的原型实际使用中可以根据需要继续扩展将存储从 JSON 换成 SQLite使用sqlite3标准库即可适合更大规模的数据和更复杂的查询。增加update命令支持修改标题、分区、配音版本等字段。增加导入导出功能导出 CSV 文件后用 Excel 打开。对接动画数据库 API比如 AniList 或 Bangumi 的公开接口自动获取剧集标题和封面。做成 Web 页面或使用 Textual 做终端 UI。扩展时要注意无论功能怎么增加数据模型尽量保持向后兼容。新增字段时给Episode设置默认值避免老数据无法加载。6.4 合法使用与版权意识最后也是很重要的一点本文只涉及本地元数据管理和 FFprobe 音轨信息查看不提供任何视频下载或盗版资源获取渠道。如果你需要整理动画资源请从正规视频平台或合法渠道获取并在个人合理使用范围内管理自己的观看记录。技术本身是中立的用在自己合法拥有的资料上才能既提升效率又不触碰版权风险。7. 总结与下一步通过这篇文章我们从一条《宝可梦地平线》第 80 集的记录出发完整搭建了一个基于 Python 标准库的剧集信息管理 CLI。现在你可以用add添加剧集用list按分区或配音版本过滤用watch和fav更新观看与收藏状态用stats完成统计还能借助 ffprobe 查看本地视频文件的音轨语言从而辅助确认台配、日配信息。整个过程中更重要的是数据结构化思维先设计字段再写读写层最后封装命令行入口。这个流程可以复用到很多个人工具开发中而不只是追番管理。下一步你可以尝试把存储换成 SQLite或者给工具增加一个--export csv参数把数据导入 Notion、Excel 做更直观的展示。如果后续《宝可梦地平线》更新到更多集数你只需要继续执行add命令维护数据即可完全不需要修改代码。如果你觉得这套方案有用可以收藏备用等动画更新时顺便把工具跑起来。动手改一改你会发现自己写的管理脚本比任何现成追番软件都顺手。
返回列表