freeCodeCamp 每日编程挑战第 83 题解析:Python 实现基于字母权重的签名校验(Signature Validation)
本篇技术指南以 freeCodeCamp 开源课程仓库中 curriculum/challenges/english/blocks/daily-coding-challenges-python/68e39ed6106dac2f0a98fd65.md(Challenge 83: Signature Validation)为核心,完整还原题目规则、测试用例与官方参考解法,并结合仓库源码讲解这道 Python 挑战在 freeCodeCamp 平台上的组织方式与运行机制。读完本文,你将掌握"字母权重编码 + 求和比对"这类校验型题目的标准解题思路,理解 ord() 与字符区间判断的组合用法,并能够独立写出通过全部单元测试的实现。
挑战背景:这道题在课程体系中的位置
本题是 freeCodeCamp 课程中 daily-coding-challenges-python 板块(block)的第 83 道每日编程挑战。从 板块结构定义 可以看到,该板块包含 200 余道 Python 小题,覆盖字符串处理、数学计算、模拟仿真等常见面试与竞赛题型,帮助学习者以每日一题的方式持续训练编程基本功。
在挑战文件头部 frontmatter 中,本挑战通过 challengeType: 29 标记为每日编程挑战类型,dashedName: challenge-83 作为 URL 化标识;题目本身按 freeCodeCamp 标准挑战格式组织为 --description--(题目描述)、--hints--(测试用例)、--seed--(初始代码)与 --solutions--(官方参考解法)四个部分。该板块在 板块 JSON 中声明了 helpCategory: "Python"、usesMultifileEditor: true,说明它属于 Python 帮助分类,并启用多文件编辑器进行作答。
题目规则:字母权重编码与签名计算
给定三个输入——消息字符串(message)、密钥字符串(key)和一个签名数字(signature),要求按照下述编码方法判断签名是否有效:
- 字母取值规则:消息与密钥中的每个字母对应一个数值——
a到z(小写)分别取值1到26;A到Z(大写)分别取值27到52。
- 非字母字符:所有其他字符(空格、数字、标点符号等)不产生任何数值,即视为
0。 - 签名计算:签名 = 消息中所有字符的数值之和 + 密钥中所有字符的数值之和。
最终,将计算出的签名与传入的 signature 参数比对:相等返回 True,否则返回 False。
字母权重速查表
| 字符区间 | 数值范围 | 计算方式 |
|---|---|---|
a – z |
1 – 26 | 字符在字母表中的位置 |
A – Z |
27 – 52 | 小写对应值 + 26 |
| 其他字符 | 0 | 无贡献 |
例如 a = 1、z = 26、A = 27、Z = 52;空格、逗号、问号、数字等一律按 0 处理。
官方给出的完整示例
题目用消息 "foo"、密钥 "bar" 演示了完整计算过程:
f (6) + o (15) + o (15) = 36
b (2) + a (1) + r (18) = 21
36 + 21 = 57
因此 verify("foo", "bar", 57) 应返回 True;若传入 54,因与真实签名不符,应返回 False。
输入输出契约与初始代码
你需要实现函数 verify,接收三个参数并返回布尔值:
message: str—— 待校验的消息字符串;key: str—— 密钥字符串;signature: int—— 声称的签名值。
返回 True 表示签名有效,False 表示无效。
题目给出的初始(seed)代码如下:
def verify(message, key, signature):
return message
它只是一个占位实现,当前直接返回了 message,需要你替换为真正的校验逻辑才能通过测试。
完整测试用例清单(hints)
挑战通过 6 个单元测试对实现进行验证,覆盖了大小写混合、标点与空格干扰、长字符串等多种场景:
| 调用 | 期望结果 | 说明 |
|---|---|---|
verify("foo", "bar", 57) |
True |
题目示例,纯小写 |
verify("foo", "bar", 54) |
False |
签名偏小,必须拒绝 |
verify("freeCodeCamp", "Rocks", 238) |
True |
大小写混合(C、R 按大写区间计值) |
verify("Is this valid?", "No", 210) |
False |
含空格与问号,均计 0 |
verify("Is this valid?", "Yes", 233) |
True |
相同消息、不同密钥,签名随之变化 |
verify("Check out the freeCodeCamp podcast,", "in the mobile app", 514) |
True |
长消息 + 逗号与空格,考验累加正确性 |
这些用例的意图很明确:密钥不同则签名不同(对比第 4、5 条,同一消息在 "No" 下为 213,在 "Yes" 下为 233),任何非字母字符都必须被安全忽略。
在课程体系中,这些 --hints-- 会被转换成实际的测试执行代码。本挑战的每个断言都以内嵌 JavaScript 的形式声明,运行时通过 runPython 调用 Python 的 unittest 框架执行,例如第一条用例对应:
({test: () => { runPython(`
from unittest import TestCase
TestCase().assertIs(verify("foo", "bar", 57), True)`)
}})
也就是说,平台会把你提交的 verify 函数注入到 Python 解释器中,再依次运行这 6 个断言;任何一条失败都会导致挑战未通过。这也解释了为什么函数必须恰好命名为 verify,且返回值必须是 Python 布尔值。
官方参考解法逐步拆解
挑战自带的参考解法如下:
def verify(message, key, signature):
def charValue(ch) -> int:
if 'a' <= ch <= 'z':
return ord(ch) - ord('a') + 1
elif 'A' <= ch <= 'Z':
return ord(ch) - ord('A') + 27
else:
return 0
def compute_sum(s):
return sum(charValue(ch) for ch in s)
total = compute_sum(message) + compute_sum(key)
return total == signature
第一步:charValue 单字符取值函数
核心是利用 Python 内置的 ord() 函数把字符转换为 ASCII 码,再用"差值偏移"得到题设数值,从而避免手写 52 项映射表:
- 小写分支:
'a' <= ch <= 'z'利用 Python 字符串比较特性判断字符是否位于小写区间;ord(ch) - ord('a') + 1将a映射为1、b映射为2……z映射为26。 - 大写分支:
'A' <= ch <= 'Z'同理,ord(ch) - ord('A') + 27将A映射为27……Z映射为52,与题设"大写 = 小写 + 26"完全吻合。 - 其余情况返回
0,覆盖空格、标点、数字等所有非字母字符。
值得注意,判断大小写也可以用 ch.islower() / ch.isupper(),但官方解法选择区间比较,语义上更严格地限定了 ASCII 英文字母范围。
第二步:compute_sum 字符串求和函数
compute_sum 用生成器表达式 sum(charValue(ch) for ch in s) 一次性遍历字符串,对每个字符调用 charValue 并累加。配合 Python 的 sum() 内建函数,代码非常简洁,且由于生成器惰性求值,不会产生额外列表开销。
第三步:主逻辑与复杂度分析
total = compute_sum(message) + compute_sum(key)
return total == signature
先分别求消息与密钥的权重和,再相加得到完整签名,最后与传入的 signature 做相等比较并直接返回布尔结果。
- 时间复杂度:
O(len(message) + len(key)),只需各遍历一遍。 - 空间复杂度:
O(1)(不含输入本身),除少量标量变量外无额外存储。 - 正确性:比较运算
==在 Python 中返回布尔值,恰好满足函数返回True/False的契约;两个assertIs断言会严格校验返回的就是布尔对象。
从仓库源码看这套挑战的完整运转机制
这道题并非孤立文本,freeCodeCamp 仓库为"每日编程挑战"构建了完整的数据链路,理解它能帮你把握题目的真实运行环境。
1. 挑战数据通过 API 按日期下发
每日编程挑战路由 定义了若干公开 GET 端点,其中最核心的是:
GET /daily-coding-challenge/date/:date—— 按YYYY-MM-DD日期获取当天挑战;GET /daily-coding-challenge/today—— 获取美中时区当天的挑战;GET /daily-coding-challenge/month/:month与/daily-coding-challenge/all—— 按月或全量列出挑战的轻量信息(id、编号、日期、标题);GET /daily-coding-challenge/newest—— 获取最新挑战日期。
值得注意的细节是:路由对"未来挑战"做了保护——在 date/:date 处理中,若请求日期晚于美中时区当天,会返回 404(Challenge not found.),保证挑战按时间逐步解锁;月份与全量接口同样以"日期 ≤ 今天"为查询条件。此外,路由还通过 fastify.Sentry?.metrics?.count 记录 dcc.challenge_viewed、dcc.challenge_not_found 等指标,用于观测挑战的访问情况。
从该路由的单元测试 daily-coding-challenge.test.ts 的 fixture 可以看到,数据库中的 Python 挑战数据形如:python: { tests: [...], challengeFiles: [{ contents, fileKey: 'mainpy' }] },即每个挑战同时携带测试列表与初始代码文件(Python 文件键为 mainpy)。这也印证了本挑战 markdown 中 --seed-- 的 Python 代码最终会作为 challengeFiles 内容呈现给学习者。
2. 客户端对挑战数据做结构校验
API 返回的数据在客户端展示前还会经过 Joi 校验。daily-coding-challenge-validator.ts 中定义了 DailyCodingChallengeFromDb 接口与 validateDailyCodingChallengeSchema 校验函数,要求每个挑战必须包含 id、challengeNumber(正整数,≥1)、title、date、description,且 javascript 与 python 两个语言分支都必须具备 tests 与 challengeFiles 字段。这意味着"Python 版"挑战(如本题)与"JavaScript 版"挑战在数据层完全平行,本挑战正是 Python 分支的一份具体实例。
3. 挑战 Markdown 的结构约定
从本文件可以看到,freeCodeCamp 挑战文档采用统一 frontmatter + 分段格式:
- frontmatter:
id(全局唯一)、title(人类可读标题)、challengeType(29 表示每日编码挑战)、dashedName(URL 友好名); --description--:题目描述与规则;--hints--:每个 hint 是"说明文本 + 可执行测试",本挑战的测试统一走runPython+unittest.TestCase;--seed--/--seed-contents--:学习者看到的初始代码;--solutions--:官方参考实现。
这种结构既便于人工阅读维护,也便于构建工具解析后注入数据库。仓库中的 daily-coding-challenge-validator.ts、客户端页面 show-daily-coding-challenge.tsx 等文件共同完成了从数据库 → API → 前端编辑器 → 测试执行的闭环。
边界条件与常见陷阱
在实现本题时,有四个易错点值得重点检查:
- 非字母字符必须计 0,而不是报错或跳过索引。
"Is this valid?"中的空格与问号、"Check out..."中的逗号都是干扰项,若把它们误计为其他值会导致签名偏大、测试失败。 - 大小写是两套取值区间。大写字母从 27 开始,
"freeCodeCamp"中的两个C与"Rocks"中的R都必须按大写规则取值,混用会导致238对不上。 - 签名不一致要返回
False。函数是"校验器"而非"生成器",输入54、210这类错误签名时不能抛出异常,而要静默返回False。 - 返回真正的布尔值。
total == signature本身即产生布尔值;若写成return 1 if total == signature else 0,虽然assertIs(..., True)仍可能通过,但语义上不如直接返回布尔值清晰。
延伸思考:同板块的"字母求和"家族
在 daily-coding-challenges-python 板块内,与本题思路相近的挑战还包括 Challenge 63 "Battle of Words"、Challenge 204 "Sum the Letters"、Challenge 172 "Letters-Numbers" 等(见 板块结构定义)。它们共享"把字母映射为数值并聚合计算"的核心模式。掌握了 ord() - ord(base) + offset 这一通用偏移技巧后,你可以举一反三地解决这一整类题目;区别仅在于聚合方式是求和、比较双方大小,还是做进一步组合运算。
本文小结:Challenge 83 是一道典型的"编码规则 + 数值校验"入门题。通过字母区间判断与 ASCII 偏移映射,将字符串问题转化为纯数值比较;其官方解法以两个内嵌函数实现了清晰的关注点分离,复杂度为线性。结合仓库源码可见,这类挑战在 freeCodeCamp 中从 Markdown 文档到 API 下发、再到前端编辑器和 unittest 测试执行,已形成一套完整、自动化的每日编程训练闭环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00