
平时写代码累了我习惯戴上耳机听点老歌。和纯听歌的人不太一样我整理歌单的方式有点“程序员味”先把想听的歌一条一条放进 JSON 文件再写个脚本按不同维度打分、排序最后自动生成一页榜单。最近在循环的是披头士。披头士这个乐队很多开发者应该都不陌生。你即使不主动听也会在各种电影、游戏、广告里碰到他们的歌。但真正想听懂一个乐队不能只靠播放器随机播放。我决定做一个“披头士十首盘点”的系列内容把心目中值得反复听的十首歌按自己的标准排个序。第一期就是标题里写的这首《Please Mister Postman》。这篇文章不是单纯聊音乐而是把这次盘点做成一个可以复用的工程化流程。我会先用一首歌讲清楚榜单的评选逻辑再给出完整的歌单数据结构、Python 命令行工具和 HTML 报告生成方案。你以后想盘点任何主题比如自己最喜欢的电影、最常用的开源库、最想去的城市都可以套用这套流程。1. 这篇文章真正要解决的问题先交代一个现实内容盘点这件事看起来简单做起来很碎。如果你只是自己在备忘录里列一个“我最喜欢的十首歌”那当然不需要什么工具。但如果你想长期维护一份榜单并且希望每一次更新都能留下记录、能给别人看、能按不同维度排序纯手写文档很快就失控了。举个具体例子。今天你觉得《Please Mister Postman》应该是第一过了半年你听腻了想把它降到第三。手动改文档意味着你要同步修改排名、说明、标签、生成日期。如果榜单里有十首歌每首还有三四个属性这个维护成本会随着文章数量线性增长。所以这篇文章要解决的问题有三个怎么把音乐盘点这类主观内容拆成一套结构化的数据模型。怎么用最轻量的方式不引入数据库、不引入 Web 框架实现榜单数据的校验、排序和展示。怎么让这套流程在“系列文章”场景下持续复用而不是每写一篇就重写一次脚本。先说明我的技术选型判断。很多人看到“工具”两个字会下意识想到要上 Flask、FastAPI、MySQL 或者 MongoDB。但在这个场景里数据量只有十首歌结构固定更新频率低用 JSON 文件加 Python 标准库就足够了。引入数据库反而会让项目变重你要处理依赖安装、连接配置、迁移脚本而这些和“盘点歌曲”这个核心目标没有关系。技术选型最重要的原则是匹配问题规模。JSON 加 Python 标准库已经能覆盖榜单项目的全部需求。这篇文章最适合的读者是两类人。第一类喜欢披头士同时想看看音乐内容怎么做成结构化数据的开发者第二类正在学 Python想找一个能快速练手、跑通命令行工具的小项目的人。2. 《Please Mister Postman》的背景与榜单评选标准在进入代码之前先聊聊这首歌本身。你会更好地理解为什么我会把它排在第一。《Please Mister Postman》并不是披头士的原创作品而是一首翻唱。原唱是摩城唱片旗下的女子组合 The Marvelettes1961 年发行。披头士在 1963 年把它收录进第二张英国录音室专辑《With the Beatles》。这首歌的歌词内容很直白标题本身就是一个请求“邮差先生请把信送给我吧”。这种“把情感交给一封信”的写法在今天看来简单但在上世纪六十年代初的流行音乐里是非常典型的直给式表达。披头士之所以翻唱这首歌背后是他们对节奏布鲁斯和摩城音乐的系统性吸收。早期披头士的现场和专辑里有相当一部分曲目来自翻唱这不是偷懒而是一种乐队训练方式——在复刻中理解原曲的节奏、和声和演唱方法。在主唱配置上披头士版本的《Please Mister Postman》由约翰·列侬担任主唱保罗·麦卡特尼和乔治·哈里森负责背景和声。和 The Marvelettes 的原版相比披头士版本少了些“规范感”多了一些直接的冲击力。当然写技术博客不能只给一个“感觉好听”的主观结论。为了让盘点可比较我给这次十首歌曲评选设定了五个维度维度说明权重旋律记忆点听一遍后能否哼出主旋律高和声与编曲人声和声、乐器编排有没有特点中文化影响力这首歌对乐队或整个流行音乐意味着什么中制作工艺录音、混音、结构上的开创性中个人循环价值单曲循环会不会腻高这五个维度不追求绝对客观但它让每次排名都具备可解释性。比如《Please Mister Postman》的“个人循环价值”很高因为它足够短、足够直接节奏又非常带感适合写代码的时候反复放。最终十首歌曲的预览榜单如下后续系列文章会逐首展开排名歌曲专辑年份类型1Please Mister PostmanWith the Beatles1963翻唱2Love Me DoSingle1962原创3I Want to Hold Your HandSingle1963原创4Here Comes the SunAbbey Road1969原创5YesterdayHelp!1965原创6A Day in the LifeSgt. Peppers Lonely Hearts Club Band1967原创7Let It BeLet It Be1970原创8Hey JudeSingle1968原创9SomethingAbbey Road1969原创10Come TogetherAbbey Road1969原创这个表格会直接成为后面 JSON 数据文件的一部分也是整个项目的核心数据来源。3. 环境准备与前置条件本项目的运行环境非常简单不需要装在虚拟机里也不需要装 Docker。你需要准备的是Python 3.9 或更高版本。本文所有代码只用标准库不依赖第三方包。一个文本编辑器或 IDE推荐 VS Code 或 PyCharm。一个命令行终端。验证方法python --version输出类似Python 3.12.4如果当前的 Python 版本低于 3.9建议先升级 Python。后面的代码里用到了list[dict]这样的类型注解写法这是 Python 3.9 才支持的特性。如果你还在用 Python 3.8可以不用这种写法但更稳妥的方式是直接升级。这个项目不安装任何第三方库核心原因有两个。第一json、argparse、html、pathlib都是 Python 标准库可以完成全部需求第二减少依赖意味着项目在任何一台装了 Python 的机器上都能直接运行这对工具类脚本非常重要。4. 歌单数据模型设计设计数据结构是整个项目最值得花时间思考的环节。我先想清楚一件事每一首歌需要用哪些字段来描述。基础信息字段rank当前排名。title歌曲标题。album所属专辑。如果是非专辑单曲写作Single。year发行年份。type歌曲类型。填入cover代表翻唱original代表原创。origin如果type是cover这里填原唱者如果是原创可以为空字符串。评分相关字段subjective_score主观评分取值范围 0 到 10。内容字段learning_point对这首歌的技术或内容点评比如“早期翻唱中的节奏与和声训练”。把这些字段整理成 JSON 文件后整个项目的数据部分就完成了。后面每一期系列文章更新时只需要改这个文件不需要动代码。完整文件路径为songs.json{ series: Beatles Top 10 Playlist, updated: 2025-01-01, songs: [ { rank: 1, title: Please Mister Postman, album: With the Beatles, year: 1963, type: cover, origin: The Marvelettes, subjective_score: 9.0, learning_point: 早期翻唱中的节奏与和声训练列侬主唱让这首摩城曲目变得更直接。 }, { rank: 2, title: Love Me Do, album: Single, year: 1962, type: original, origin: , subjective_score: 8.5, learning_point: 口琴与人声齐唱开场是披头士录音室作品里辨识度最高的标志之一。 }, { rank: 3, title: I Want to Hold Your Hand, album: Single, year: 1963, type: original, origin: , subjective_score: 9.0, learning_point: 早期原创中旋律密度很高的代表作也是乐队走向全球的起点之一。 }, { rank: 4, title: Here Comes the Sun, album: Abbey Road, year: 1969, type: original, origin: , subjective_score: 8.9, learning_point: 乔治·哈里森创作吉他编曲明亮是 Abbey Road 专辑里循环价值最高的单曲之一。 }, { rank: 5, title: Yesterday, album: Help!, year: 1965, type: original, origin: , subjective_score: 8.8, learning_point: 弦乐四重奏的配置在流行乐队作品里少见的克制情绪表达非常集中。 }, { rank: 6, title: A Day in the Life, album: Sgt. Peppers Lonely Hearts Club Band, year: 1967, type: original, origin: , subjective_score: 8.8, learning_point: 从叙事段落到管弦乐高潮结构实验感很强是晚期披头士的代表作品。 }, { rank: 7, title: Let It Be, album: Let It Be, year: 1970, type: original, origin: , subjective_score: 8.6, learning_point: 旋律简洁质感厚重属于乐队晚期最容易被大众接受的作品之一。 }, { rank: 8, title: Hey Jude, album: Single, year: 1968, type: original, origin: , subjective_score: 8.7, learning_point: 后半段长时间的合唱段落让现场互动氛围非常强结构上很大胆。 }, { rank: 9, title: Something, album: Abbey Road, year: 1969, type: original, origin: , subjective_score: 8.5, learning_point: 乔治·哈里森又一首经典旋律舒缓和声层次丰富适合夜里听。 }, { rank: 10, title: Come Together, album: Abbey Road, year: 1969, type: original, origin: , subjective_score: 8.4, learning_point: 贝斯律动非常突出整体氛围偏蓝调和同一专辑里其他歌曲差异明显。 } ] }数据模型的设计有几个关键判断。第一我没有用关系型数据库因为歌单数据没有复杂关联关系用数据库属于杀鸡用牛刀。第二我在 JSON 顶层保留series和updated字段是为了让每份数据文件具备自描述能力。以后别人打开这个 JSON第一眼就能知道这是什么项目、数据更新到什么时间。第三我故意没有把五维评分拆成五个独立字段而是用了一个subjective_score。原因是这个项目不需要支撑复杂的加权计算字段越多维护成本越高。这样的设计也留下了一个清晰的扩展点。如果你想做成一个真正的评分系统可以去掉subjective_score改成五个评分字段再写一个加权函数。数据结构允许这种演进这就是 JSON 文件的好处它没有数据库的强约束但比脚本里硬编码变量要规范得多。5. 完整示例与代码实现下面实现核心命令行工具。文件路径为main.py。#!/usr/bin/env python3 # 文件路径main.py import argparse import html import json import sys from pathlib import Path REQUIRED_FIELDS { rank, title, album, year, type, origin, subjective_score, learning_point, } def load_songs(path: str) - list[dict]: with open(path, r, encodingutf-8) as f: data json.load(f) if songs not in data: raise ValueError(JSON 文件缺少 songs 字段) return data[songs] def check_songs(songs: list[dict]) - list[str]: errors [] for i, song in enumerate(songs, start1): missing REQUIRED_FIELDS - set(song.keys()) if missing: errors.append( f第 {i} 首歌曲{song.get(title, unknown)} f缺少字段: {, .join(sorted(missing))} ) score song.get(subjective_score) if score is not None and ( not isinstance(score, (int, float)) or not 0 score 10 ): errors.append( f第 {i} 首歌曲{song.get(title, unknown)} f评分必须在 0-10 之间 ) return errors def sort_songs(songs: list[dict]) - list[dict]: return sorted( songs, keylambda s: (-s.get(subjective_score, 0), s.get(rank, 999)), ) def render_markdown(songs: list[dict]) - str: lines [# 披头士十首盘点榜单, ] for song in songs: lines.append(f## {song[rank]}. {song[title]}) lines.append() lines.append(f- 专辑{song[album]}) lines.append(f- 年份{song[year]}) lines.append(f- 类型{翻唱 if song[type] cover else 原创}) if song.get(origin): lines.append(f- 原唱{song[origin]}) lines.append(f- 主观评分{song[subjective_score]}) lines.append(f- 技术启示{song[learning_point]}) lines.append() return \n.join(lines) def render_html(songs: list[dict]) - str: items [] for song in songs: origin ( f原唱{html.escape(str(song[origin]))}br if song.get(origin) else ) items.append( f div classsong h2{html.escape(str(song[rank]))}. {html.escape(str(song[title]))}/h2 p专辑{html.escape(str(song[album]))} | 年份{html.escape(str(song[year]))}/p p类型{翻唱 if song[type] cover else 原创} | {origin} 评分{song[subjective_score]}/p p技术启示{html.escape(str(song[learning_point]))}/p /div ) return f!DOCTYPE html html langzh-CN head meta charsetutf-8 title披头士十首盘点榜单/title style body {{ max-width: 800px; margin: 40px auto; padding: 0 16px; font-family: sans-serif; line-height: 1.8; }} .song {{ border-bottom: 1px solid #eee; padding: 16px 0; }} /style /head body h1披头士十首盘点榜单/h1 {.join(items)} /body /html def main() - None: parser argparse.ArgumentParser(description披头士十首盘点 CLI 工具) parser.add_argument( --input, defaultsongs.json, help歌单 JSON 文件路径, ) parser.add_argument( --format, choices[markdown, html], defaultmarkdown, help输出格式, ) parser.add_argument( --check, actionstore_true, help仅校验数据不输出榜单, ) args parser.parse_args() if not Path(args.input).exists(): print(f错误找不到文件 {args.input}, filesys.stderr) sys.exit(1) try: songs load_songs(args.input) except Exception as e: print(fJSON 解析失败{e}, filesys.stderr) sys.exit(1) errors check_songs(songs) if errors: print(数据校验失败, filesys.stderr) for err in errors: print(f - {err}, filesys.stderr) sys.exit(1) if args.check: print(f校验通过共 {len(songs)} 首歌曲) return songs sort_songs(songs) if args.format html: output Path(beatles_report.html) output.write_text(render_html(songs), encodingutf-8) print(f已生成 {output}) else: print(render_markdown(songs)) if __name__ __main__: main()代码的模块划分是这样的load_songs负责读取 JSON 文件并验证基本结构。check_songs负责字段校验和评分范围校验。sort_songs负责排序。排序逻辑是按评分降序评分相同时按rank升序。render_markdown把歌曲列表渲染成 Markdown 文本。render_html把歌曲列表渲染成 HTML 页面。main负责命令行参数解析和调用各函数。有一个细节需要特别说明HTML 渲染里的html.escape。歌曲标题和点评内容里如果出现、、等字符直接拼进 HTML 会导致页面结构错乱。html.escape会把特殊字符转成安全的 HTML 实体这是 Web 内容渲染的基本防线。即使现在数据是自己写的 JSON也应当养成“输出到 HTML 先转义”的习惯。Markdown 渲染函数没有做转义原因不同。Markdown 更接近纯文本生成的内容是给人直接看的文本标题里出现特殊符号的概率很低。如果你要把这份 Markdown 发布到支持 HTML 注入的平台建议也做一层转义。技术选型没有银弹关键是知道自己做的每个决定的边界。6. 运行结果与效果验证把两个文件放在同一个目录beatles-tools/ ├── main.py └── songs.json首先执行数据校验python main.py --check预期输出校验通过共 10 首歌曲然后生成 Markdown 榜单python main.py --format markdown会直接打印到终端开头内容类似# 披头士十首盘点榜单 ## 1. Please Mister Postman - 专辑With the Beatles - 年份1963 - 类型翻唱 - 原唱The Marvelettes - 主观评分9.0 - 技术启示早期翻唱中的节奏与和声训练列侬主唱让这首摩城曲目变得更直接。最后生成 HTML 版报告python main.py --format html预期输出已生成 beatles_report.html然后用浏览器打开beatles_report.html。页面顶部是标题下面是十首歌曲的卡片每张卡片都包含专辑、年份、类型、原唱、评分和技术启示。如果运行失败第一步要看终端里有没有异常堆栈。常见的情况是 JSON 文件格式错误比如少了一个逗号或者使用了全角引号。Python 的json.load对格式要求非常严格任何格式错误都会直接退出。这时可以把songs.json里的内容复制到任意 JSON 在线格式化工具里检查也可以运行python -m json.tool songs.json来验证python -m json.tool songs.json如果这个命令能正常输出格式化后的 JSON说明文件本身没有问题。生成 HTML 报告时有一个容易踩的坑Windows 系统上控制台默认编码可能是 GBK而 Python 读取 UTF-8 文件在部分场景下会报UnicodeDecodeError。解决方案是运行命令时显式指定编码set PYTHONIOENCODINGutf-8 python main.py --format htmlmacOS 和 Linux 系统一般不需要处理这个问题。7. 常见问题与排查思路实际使用这套工具时整理了几个高频问题。问题现象可能原因排查方式解决方案JSON 解析失败JSON 格式错误用python -m json.tool songs.json验证检查逗号、引号、括号是否完整缺少字段: XXXsongs.json 里某首歌字段不完整查看报错里提示的歌曲序号对照REQUIRED_FIELDS补充字段排名没有按 rank 输出默认按 subjective_score 降序检查排序逻辑sort_songs想按 rank 输出时修改排序 keyWindows 控制台中文乱码控制台编码不是 UTF-8运行echo %PYTHONIOENCODING%执行set PYTHONIOENCODINGutf-8HTML 页面显示特殊字符异常输出内容未转义检查标题是否含等符号渲染 HTML 时使用html.escape想增加第五个评分维度数据结构没有该字段检查当前 JSON 字段在 JSON 增加字段并在校验函数中同步再补充一个很容易被忽略的问题如果你把songs.json和main.py放在不同的目录运行脚本时必须注意当前工作目录。直接运行python main.py时默认读取的是当前目录下的songs.json。如果报“找不到文件”不要急着改代码先看清当前目录是不是脚本所在目录或者用--input指定完整路径python main.py --input data/songs.json这套工具的设计目标就是保持简单所以绝大多数问题都集中在 JSON 格式和编码这两类不会有复杂的依赖冲突问题。8. 最佳实践与工程建议通过这个项目有几条工程经验值得沉淀下来。8.1 数据和展示分离这个项目里songs.json是数据main.py是展示逻辑两者完全独立。你改评分、换曲目、调整点评内容都不需要碰代码。这个设计原则几乎适用于所有小型工具项目。只要有“内容经常变结构相对稳定”这个特征都建议把内容抽成数据文件。实际维护时我只改 JSON 就能完成一次榜单更新。更新之后重新生成 Markdown 和 HTML整个流程不超过一分钟。如果没有做数据分离每一期文章都要复制粘贴一段完整代码去改里面写死的列表那才是真正的灾难。8.2 数据校验要前置check_songs这个函数看起来不起眼但它是这个项目里最有价值的部分之一。人的手工操作一定会出错可能是少写一个字段可能是评分填成了 11早期发现问题永远比到最后生成报告时发现问题成本更低。如果你把同样思路用在生产环境里对应的就是配置校验、参数校验、输入校验。校验逻辑不一定复杂但一定要执行得足够早。脚本启动时校验比运行到一半报错要好得多。8.3 主观数据要有可解释性榜单类内容的主观性很强。同样一首歌可能有人觉得第一有人觉得第八。为了让讨论有意义我给每首歌加了learning_point字段说明这首歌为什么值得进入榜单以及它带来的核心启发。这样一来排名不再是不可解释的“我觉得好听”而是一个带理由的技术判断。这个思路也可以用在代码评审里。当你在团队中提出“这段代码应该重构”时最好同时给出判断依据。理由可以是复杂度太高、测试覆盖不足、业务扩展点发生变化但一定要具体化。8.4 版本管理建议updated字段记录了数据文件的更新时间但它只是一个静态字符串。更严谨的做法是用 Git 管理整个目录。每次更新歌单提交一次变更记录。这样以后能回看“一年前我的排名是什么”甚至能写出一个统计函数计算评分排名的变化趋势。如果你想让这个项目更有“持久性”可以加上一个history数组字段记录每次排名变化的时间戳和旧评分。但我不建议在现在就加。收益很小却会让 JSON 文件的复杂度上升。保持轻量直到你确实需要。8.5 后续扩展方向如果你想继续练习这个项目有四个自然的扩展方向接入真实音乐平台 API获取歌曲封面和试听链接。注意必须使用正规授权的数据源妥善管理 API 密钥不要泄露到公开仓库。把 Markdown 输出发布到博客或笔记系统做成自动化发布脚本。增加多个榜单类型比如“吉他开场最惊艳的十首歌”“键盘编曲最出彩的十首歌”每个榜单一个 JSON 文件。用subjective_score以外的字段做维度分析把五维评分做成雷达图。不过雷达图需要引入绘图库建议在确有需要时再加。9. 总结与后续学习方向这里把整个系列开篇的内容做一个快速收束。第一《Please Mister Postman》是一首很值得反复听的翻唱歌曲它能代表披头士早期的学习和输出方式。第二一篇好的盘点内容不能只靠感觉需要把评选标准拆成可解释的维度。第三代码侧用 JSON 加 Python 标准库就足够支撑一份十首榜单的长期维护不需要引入数据库和 Web 框架。现在你手上有了一份完整的、可以直接运行的项目源码。你可以做三件事先把songs.json替换成你自己的歌单或者换成任何你想盘点的话题然后跑通--check、--format markdown、--format html三个命令最后把维护好的 JSON 文件提交到 Git把它当作一个长期维护的项目来养。下一期会继续使用这套工具具体展开第二首歌曲的盘点。如果你也正在循环某首歌不妨现在就去把它写进 JSON然后写个脚本把它输出成榜单。