第 5 章 · 让输入成为可检查的数据
字符串与报名文本清洗
第 4 章的 M1 能批量判定报名,但键盘输入不一定整齐:编号可能写成小写、字段前后带空格,也可能少一段或混入全角数字。本章学习先检查结构、再定位和清洗字段、最后校验格式,让程序既能规范允许的差异,也能明确拒绝不合格记录。
- 预计阅读
- 65 分钟
- 上机实践
- 90 分钟
- 前置知识
- 第 1—4 章
- 本章产出
- 个人报名文本清洗器
学完本章,你应该能够
- 用长度、索引和切片定位字符串中的字符与片段,区分索引越界和切片越界。
- 说明字符串不会原地修改,正确保存
strip()、upper()等方法返回的新值。 - 使用
count()、find()定位两个分隔符,按规则切出三个字段。 - 用遍历和成员判断检查编号后缀,不丢失前导零,也不放过全角数字。
- 区分可自动清洗的差异与必须拒绝的格式错误,并给出第一项失败原因。
- 独立完成清洗程序、八组测试与个人限时修改。
5.1 报名文本为什么要先清洗
先比较两条记录:陈晨|CL007|EV-0021 和 陈晨 | cl007 | ev-0021 。它们表示相同的三个字段,但直接比较整个字符串会得到不同结果。反过来,陈晨|CL0A7|EV-0021 含错误字符,不能仅靠“看起来差不多”就自动修成正确编号。
本章约定记录格式为 姓名|社团编号|活动编码,中间必须恰好有两个半角竖线。字段首尾空白可以去除,编号大小写可以统一;姓名清洗后不能空,社团编号是 CL 加三位半角数字,活动编码是 EV- 加四位半角数字。
| 阶段 | 做什么 | 例子 |
|---|---|---|
| 结构检查 | 先确认恰有两个半角分隔符 | 少一段或多一段立即报告结构错误 |
| 清洗 | 只做约定允许的规范化 | cl007 → CL007 |
| 校验 | 检查姓名、长度、前缀和数字字符 | CL0A7 拒绝,不猜测原意 |
| 输出 | 只对通过的记录给出标准文本 | 陈晨|CL007|EV-0021 |
本章只确认文本符合教学用格式。是否本校学生、是否付足费用、是否还有名额,仍属于第 3、4 章的业务审批;不能因为文本通过就宣称报名成功。这套 CL、EV- 规则也不是学校真实编号标准。
5.2 字符串、长度与索引
编号 007 是文本,不是人数。若把 "007" 转成整数,就只剩 7,前导零这一格式信息会消失。本章始终把姓名和编号保持为字符串。len() 给出字符数;用方括号索引可取某个位置,首字符索引为 0,最后一个也可写作 -1。
code = "EV-0021"
print(len(code))
print(code[0])
print(code[2])
print(code[-1])
print("007", int("007"))7 E - 1 007 7
这里 code 长度为 7,有效的正索引是 0 到 6。取 code[7] 会报 IndexError;把它作为运行前预测题即可,不要误当作可运行的正确示例。len() 是内置函数,不属于后面六个字符串方法。
检查出第一个字符是 C,并不能证明 CL007 长度、前缀和后缀都正确。索引帮助观察局部;真正校验还要组合多项条件。
5.3 切片:从起点到终点之前
切片 text[start:stop] 取得从 start 开始、到 stop 之前的一段文本;右边界不包含在结果中。省略起点表示从开头取,省略终点表示取到末尾。我们稍后会用它提取字段和编号前后缀。
code = "EV-0021"
print(code[:3])
print(code[3:])
print(code[3:5])
print(code[99:] == "")EV- 0021 00 True
code[:3] 取索引 0、1、2,因此是 EV-;code[3:] 保留了 0021 的两个前导零。单点索引越界会报错,而 code[99:] 返回空字符串,因此最后一行比较得到 True。
切片“不报错”不代表数据有效:CL 的 [2:] 也是空字符串。因此校验编号时还必须单独检查总长度。
5.4 方法返回新字符串:先清洗,再保存
字符串不能像可修改的格子那样在原位置改变某个字符。写 code[0] = "c" 会出错;strip()、upper()、lower() 等方法返回处理后的新字符串,不会自动替换原变量。需要后续使用时,必须把结果赋值给名称。
raw = " cl007 "
raw.strip()
print(f"原值:[{raw}]")
club_code = raw.strip()
club_code = club_code.upper()
print(f"新值:[{club_code}]")
print("Y".lower())原值:[ cl007 ] 新值:[CL007] y
第一行 raw.strip() 的返回值没有保存,raw 仍含空格。后面先保存去除首尾空白的结果,再转大写。lower() 可把输入的 Y 变为 y;但这并不意味着本项目可以随意更改所有字段。
| 方法 | 本章用途 | 不会做的事 |
|---|---|---|
strip() | 去字段首尾空白 | 不会删除姓名内部空格 |
upper() | 统一编号字母大小写 | 不会补齐缺失数字 |
lower() | 理解反方向的大小写统一 | 基础程序不把编号转小写 |
例如 " Li Ming ".strip() 得到 "Li Ming",姓名中间的空格必须保留。把所有空格都删掉会改变原始姓名,不符合本章清洗约定。
5.5 count()、find() 和 replace()
count() 回答某段文本出现几次;find() 返回第一次找到的位置,找不到返回 -1。如果要找第二个竖线,可让第二次查找从第一个位置之后开始。replace() 返回替换后的新字符串,但只能按明确规则使用。
record = "A|B|C"
first_bar = record.find("|")
second_bar = record.find("|", first_bar + 1)
print(record.count("|"))
print(first_bar, second_bar)
print(record.find("/"))
print("A|B".replace("|", "|"))2 1 3 -1 A|B
第二次 find() 的起点是 2,因此找到索引 3。replace() 的示例说明可以把全角竖线转换成半角竖线,但基础任务不允许这样自动修补结构;只有在另存的进阶版中先明确新增规则、再补测试,才可使用这一转换。
本章学习的字符串方法是 strip、upper、lower、count、find、replace。len() 是函数,in 是运算符。split() 会产生列表,留到后续章节。
5.6 先检查结构,再定位并切出三个字段
要读出姓名、社团编号和活动编码,先确认记录中恰好有两个半角竖线。两个分隔位置确定后,三段切片的边界才有依据;不能把姓名长度写死,因为“陈晨”和“Li Ming”长度不同。
record = " 陈晨 | cl007 | ev-0021 "
if record.count("|") == 2:
first_bar = record.find("|")
second_bar = record.find("|", first_bar + 1)
student_name = record[:first_bar].strip()
club_code = record[first_bar + 1:second_bar].strip().upper()
activity_code = record[second_bar + 1:].strip().upper()
print(student_name)
print(club_code)
print(activity_code)陈晨 CL007 EV-0021
第一个字段在第一个竖线左边;第二个字段从第一个竖线后一位到第二个竖线之前;第三个字段从第二个竖线后一位到结尾。每个字段要分别 strip(),因为只清洗整条记录的两端,无法去掉竖线旁边的空白。
结构正确也不保证内容正确:陈晨||EV-0021 仍有两个竖线,但社团编号是空的。程序必须在解析之后继续校验字段。
5.7 遍历后缀,用成员判断校验字符
社团编号规则是“总长 5、前缀 CL、后三位都是半角数字”。字符串可以被 for 逐字符遍历;char not in "0123456789" 判断当前字符不属于允许集合。任何一位不合格,就把布尔标记改为 False,后续合法字符也不能把它改回 True。
club_code = "CL0A7"
club_digits_ok = True
for char in club_code[2:]:
if char not in "0123456789":
club_digits_ok = False
print(char, club_digits_ok)
print(len(club_code) == 5 and club_code[:2] == "CL" and club_digits_ok)0 True A False 7 False False
这就是课堂中的“坏字符标记”:读到 A 后,标记保持 False。用 in 可询问“在不在”,用 not in 可直接表达“不在”。本章限定半角 0123456789,所以全角 007 不能通过。
| 输入 | 长度 | 前缀 | 后缀逐字符 | 结论 |
|---|---|---|---|---|
CL007 | 5 ✓ | CL ✓ | 0、0、7 ✓ | 有效 |
CL0A7 | 5 ✓ | CL ✓ | A 不在半角数字中 | 无效 |
CL | 2 ✗ | CL ✓ | 空后缀,循环零次 | 无效 |
CL 的循环零次执行,标记仍是初始 True;因此不能只凭标记判定有效。活动编码同理,但总长应为 7、前缀为 EV-、后缀从索引 3 开始。
5.8 完成一次处理一条记录的清洗器
现在把“结构 → 定位 → 清洗 → 校验 → 输出”串成一份完整程序。基础版只接受两个半角竖线;姓名清洗后不能为空;两个编号分别检查长度、前缀和逐字符结果。保存为 text_cleaner.py 后,从第一行运行。
# 第5章:一次清洗并校验一条报名记录
# 格式:姓名|社团编号|活动编码
raw_record = input("报名记录(姓名|社团编号|活动编码):")
record = raw_record.strip()
if record.count("|") != 2:
print("校验失败:记录必须恰有两个半角竖线")
else:
first_bar = record.find("|")
second_bar = record.find("|", first_bar + 1)
student_name = record[:first_bar].strip()
club_code = record[first_bar + 1:second_bar].strip().upper()
activity_code = record[second_bar + 1:].strip().upper()
club_digits_ok = True
for char in club_code[2:]:
if char not in "0123456789":
club_digits_ok = False
activity_digits_ok = True
for char in activity_code[3:]:
if char not in "0123456789":
activity_digits_ok = False
if student_name == "":
print("校验失败:姓名不能为空")
elif len(club_code) != 5 or club_code[:2] != "CL" or not club_digits_ok:
print("校验失败:社团编号须为CL加3位半角数字")
elif len(activity_code) != 7 or activity_code[:3] != "EV-" or not activity_digits_ok:
print("校验失败:活动编码须为EV-加4位半角数字")
else:
print(f"姓名:{student_name}")
print(f"社团编号:{club_code}")
print(f"活动编码:{activity_code}")
print(f"标准记录:{student_name}|{club_code}|{activity_code}")
print("校验通过:文本格式有效,尚未执行报名审批")报名记录(姓名|社团编号|活动编码): 陈晨 | cl007 | ev-0021 姓名:陈晨 社团编号:CL007 活动编码:EV-0021 标准记录:陈晨|CL007|EV-0021 校验通过:文本格式有效,尚未执行报名审批
运行结果第一行中,提示后面的文本是示例键盘输入,不是源代码。点击“在线运行”只会复制代码并打开你指定的 W3Cschool 新标签页;仍需粘贴代码,再提供一条输入。下载文件与此完整示例一致。
- 1先守住结构
若半角竖线不是恰好两个,立即只报结构错误,不查找和切片字段。
- 2按位置取字段
两次
find()找到真实分隔位置,再切片、清洗,保留姓名内部空格和编号前导零。 - 3逐字符记住错误
两个后缀各有一个布尔标记,发现不合格字符后保持
False。 - 4只报第一项原因
按姓名、社团编号、活动编码的顺序检查;全部通过才输出标准记录。
CL 07 的内部空格、CL07 的缺位、CL007 的全角数字,都不在本章允许自动清洗的范围。猜测并补正可能把另一条记录误当成有效记录,拒绝并说明原因更可信。
5.9 八组必测数据与错误诊所
先根据规则写预期,再逐条运行。表格里的竖线表示实际要键入的半角字符;H 的数字特意使用全角。失败组不应出现“标准记录”。
| 编号 | 输入 | 应看到的主要结果 |
|---|---|---|
| A | 陈晨 | cl007 | ev-0021 | 标准记录:陈晨|CL007|EV-0021 |
| B | |CL007|EV-0021 | 姓名不能为空 |
| C | 陈晨|CL007 | 必须恰有两个半角竖线 |
| D | 陈晨|CL007|EV-0021|备注 | 必须恰有两个半角竖线 |
| E | 陈晨|CL07|EV-0021 | 社团编号须为 CL 加 3 位半角数字 |
| F | 陈晨|CL007|EV-00A1 | 活动编码须为 EV- 加 4 位半角数字 |
| G | Li Ming | cl007 | ev-0021 | 标准记录:Li Ming|CL007|EV-0021 |
| H | 陈晨|CL007|EV-0021 | 社团编号须为 CL 加 3 位半角数字 |
从现象追查第一处错误
清洗后仍有空白,先看是否保存了 strip() 的返回值,以及是否对三个字段分别清洗;第二个竖线位置和第一个一样,检查第二次 find() 是否从 first_bar + 1 开始;CL 被放行,检查长度条件是否缺失;CL0A7 被放行,检查坏字符标记是否被下一位合法字符错误地重置。
再自己加入空串、全角竖线、空后缀和错误前缀等异常文本。它们应按契约得到失败原因,不能因为程序没有抛异常就算通过。本版只处理一条记录,不保存名单,也不处理文件或非预期运行时错误。
5.10 练习与个人任务
先预测,再看解释
- 索引与切片
code = "CL007"时,code[2:]和code.find("|")分别是什么?查看答案
分别是
"007"和-1。切片保留了前导零;找不到竖线时find()返回 -1。 - 方法返回值
执行
name = " Li Ming "、name.strip()后,name是否已经改变?查看答案
没有。要写
name = name.strip()才让变量关联到新字符串;姓名内部空格仍保留。 - 读边界
为什么
CL的逐字符检查没有发现坏数字,却仍必须判为无效?查看答案
后缀为空时循环零次,标记保持初始
True。总长度只有 2,不符合 5 位要求。 - 区分清洗与校验
cl007与CL007哪个可被基础版规范化?为什么?查看答案
cl007可转为CL007;全角数字不属于约定的半角0123456789,应拒绝而不是猜测转换。
独立完成报名文本清洗器
每位学生自己编写、测试并解释一份程序。页面示例用于理解格式契约;个人作业还需留下独立的八组实际测试、至少一组自选错误文本和现场限时修改证据。同伴只能提供黑盒输入与反馈。
最低验收要求
- 提交
w05_学号_text_cleaner.py,文件开头写个人信息与用途。 - 输入一条记录,严格按“结构 → 姓名 → 社团编号 → 活动编码”的顺序给出唯一结果。
- 保留姓名内部空格与编号前导零;只去字段首尾空白、统一编号大小写。
- 两个编号同时检查长度、前缀与半角数字;失败时不输出标准记录。
- 提交 A—H 八组“预期—实际—结论—修正后复测”记录,另测至少一组自选错误文本。
- 限时个人修改:另存为
w05_学号_text_cleaner_check.py;原格式合法后再限制只接收CL007,对CL008给出“本次只接收CL007社团”,而CL07仍先报格式错误。 - 提交修改运行证据和 80—150 字错误复盘,能解释一项清洗规则与一项校验规则的区别。
进阶可另存版本,用 replace() 接受全角竖线,并增加全角、半角混合输入测试;挑战可用第 4 章的循环批量处理多条记录。新增规则必须写明且与基础版分开,不能悄悄改变 A—H 的验收口径。
章末检查
- 我能说明索引、切片各自的边界和越界表现。
- 我会保存字符串方法的返回值,不把清洗误认为原地修改。
- 我在切字段前检查分隔符数量,第二次
find()从第一处后面开始。 - 我能解释为什么逐字符标记还必须配合长度、前缀检查。
- 我把“格式有效”和“报名审批通过”当作两件不同的事。
延伸阅读:廖雪峰《Python教程》在线 PDF中的“字符串和编码”“切片”“不可变对象”,以及菜鸟教程 Python 3 字符串。本章按 ClubHub 的记录格式重新组织例子,以课程主数据的知识边界为准。