1. 项目概述1.1 核心需求解析说个实话很多朋友练Python最头疼的不是语法不熟而是不知道练什么练到什么深度算合格。今天这篇是Python练习系列的第五篇我把它定位成一个绕不开的核心主题字符串处理与文本解析。为什么选这个主题因为日常开发里日志清洗、配置文件读取、数据表格预处理、爬虫信息提取几乎每一件实际工作都跑不掉字符串操作。很多人学了函数、列表、字典之后就不知道怎么练了其实把字符串处理练扎实你就能解决一大半的业务问题。这套练习适合三类人刚学完Python基础、想找实战题目做巩固的同学工作中经常手动处理文本、想用脚本解放双手的办公族准备面试、想补强代码基本功的求职者。练习题目的难度从入门延伸到进阶每个题目都附了完整的实现思路和参考代码。这篇练习的核心价值不光是给出答案而是把每一行代码背后的选择逻辑讲清楚——为什么用正则而不是切片、为什么用字典而不是列表、什么样的写法在高频调用下会变成性能瓶颈。如果你能答出这些问题说明你真的吃透了。1.2 系列定位与本次练习目标这是系列第五篇前四篇分别覆盖了基础语法结构、循环与列表操作、字典与集合应用、文件读写基本功。到了第五篇练习重心开始从“知道有什么工具”转向“知道怎么组合工具”。本篇会给出一套三个递进的练习题目日志时间戳归一化、敏感信息掩码工具、结构化文本解析器。三个题目在难度上呈阶梯状同时对应的也是三种最常见的应用场景日志清洗处理带格式的时间戳与混合分隔符数据脱敏在保留格式的前提下替换敏感字段文本解析从零实现一个通用的键值对解析器每个题目我都会从需求拆解开始带着你走完思考路径然后给出可以直接跑通的参考代码。最后还会把实操中踩过的坑集中梳理一遍。毕竟写代码这件事看十遍不如错一次记得牢。2. 内容整体设计与思路拆解2.1 为什么围绕“字符串处理”来设计练习序列在规划第五篇练习题目时我反复考虑过几个方向爬虫、网络请求、数据分析、Web开发。但最后都否决了。原因很简单——这些方向都依赖第三方库而单纯练字符串处理用的全是Python标准库能力。基础不牢直接上复杂场景只会两头都学不精。字符串处理是数据结构的分水岭。你去看任何一家的算法面试题字符串反转、括号匹配、最长公共前缀哪个不是高频考题这些题目表面考逻辑底层拼的其实是字符串索引、切片、拼接的效率意识。我特意用“业务驱动”的方式来设计题目。比如日志时间戳归一化实际场景中不同模块打出来的日志格式可能完全不一样。有的是2025-01-12 08:33:41有的是2025/1/12 8:33:41甚至还有带毫秒、带时区的。你如果只会一个split()就想通吃必然翻车。通过这类贴近真实场景的题目你学到的不只是某个方法的用法更重要的是一种思路当输入不可控时你的代码该怎么写才够稳健。这种能力在任何项目里都属于硬通货。2.2 方案选型标准库优先、分步实现、边界先行在设计练习的时候我给自己定了三条原则也是我在真实项目中一贯坚持的标准库优先。能用re、datetime、collections解决的绝不让读者装第三方包。这样练习门槛低只要有Python环境就能跑。同时也是在培养一种习惯——不要一遇到需求就想上大而全的框架先用标准库做一遍你会对底层逻辑有更深的理解。分步实现。每个练习都拆成两到三个版本。第一个版本先跑通主流程第二个版本再优化健壮性。我故意不把完整答案一次给出来因为实际项目里你也是这样迭代的先实现再测试然后发现漏了边界情况回头修。边界先行。这是我在踩了无数次坑后总结出来的。空字符串、纯空格、大小写混搭、分隔符不一致、中英文标点混杂——这些问题在练习中我会反复在“注意事项”里提醒。真实数据永远是脏的不做好边界处理你的脚本上线后大概率会被用户手里的数据一巴掌扇醒。这样设计还有一个好处每一道题的产出物都是可以直接改改就用的小工具不是那种练完就扔的“习题答案”。读者做完以后把代码存起来下次遇到类似需求改两行就能接着用。3. 核心细节解析与实操要点3.1 练习一日志时间戳归一化需求描述有一份混合多种格式时间戳的日志文件每一行类似于这样[2025-01-12 08:33:41] 用户登录成功 [2025/1/12 8:33:41] 用户修改密码 [2025.01.12 08:33:41.325] 订单创建完成 [2025-01-12 08:33:4108:00] 支付回调触发要求把所有时间戳统一转换成YYYY-MM-DD HH:MM:SS格式并保持日志的其余部分不变。为什么不能只靠字符串切片先说一个新手很容易踩进去的坑看到这种题目第一反应就是找固定位置切片。比如前19个字符是时间戳切一下再替换。这个方案在数据格式完全一致时能跑但只要出现2025/1/12这种不补零的短格式切片位置就全错位了。所以正确的解法是先匹配时间戳片段再解析成结构化对象最后还是统一格式输出。Python里处理时间戳核心模块就是datetime加re。re负责从日志行里“抠出”时间戳片段datetime.strptime负责把这串文本按指定格式解析成日期对象最后用strftime输出标准格式。参考实现import re from datetime import datetime def normalize_timestamp(log_line: str) - str: # 匹配多种时间戳模式时间戳单独作为一组捕获 pattern re.compile( r\[(\d{4}[-\/.]\d{1,2}[-\/.]\d{1,2}[ T]\d{1,2}:\d{2}:\d{2} r(?:\.\d)?(?:\\d{2}:\d{2})?)\] ) match pattern.search(log_line) if not match: return log_line raw match.group(1) # 分隔符统一替换方便 strptime 解析 normalized raw.replace(/, -).replace(., -) if T in normalized: normalized normalized.replace(T, ) # 去掉毫秒和时区避免格式不兼容 normalized re.sub(r\.\d, , normalized) normalized re.sub(r\08:00, , normalized) dt_obj datetime.strptime(normalized, %Y-%m-%d %H:%M:%S) new_ts dt_obj.strftime(%Y-%m-%d %H:%M:%S) return log_line[: match.start()] [ new_ts ] log_line[match.end():]操作要点拆解这个例子看起来代码不长但里面埋了三个关键决策我说给你听第一正则一定要用re.compile预编译。如果日志文件有几千行、几万行每次调用re.search都现场编译一次模式性能损耗是很扎眼的。把compile放在函数外模式只编译一次后面全是复用。第二先用正则匹配再用字符串替换来统一分隔符。很多初学者会把分隔符处理直接写进一个大正则里比如同时支持-、/、.。不是不行但正则会很长很难懂维护成本高。先匹配到片段再单独把/和.替换成-逻辑就清晰多了。第三毫秒和时区单独剔除。strptime对格式非常严格不同Python版本对%f毫秒和时区偏移的处理有细微差异。为了避免兼容性问题干脆在解析之前先去掉。这样的处理顺序本质上是正则负责“框定”预处理负责“统一”解析负责“建模”格式化负责“输出”每一步只做一件事。这个思想在后续两个练习里也会反复出现。边界情况自查清单空行应该原样返回而不是报错找不到时间戳的行原样返回日期为2025-1-2 3:4:5这种不补零形式正常解析输出补零格式日志中同时存在INFO和ERROR不同前缀不影响时间戳提取3.2 练习二敏感信息掩码工具需求描述给定一个字符串把其中的手机号、邮箱、身份证号打码后输出。要求手机号保留前3位和后4位中间4位替换为****邮箱保留首字符和后的域名其余用***替代身份证号保留前6位和后4位中间部分替换为********示例输入输出input: 联系电话 13812345678邮箱 test_userexample.com output: 联系电话 138****5678邮箱 t***example.com为什么选择“先识别、再分组替换”的方案敏感信息掩码业务里最大的痛点是你不知道用户会以什么格式把敏感信息粘贴进来。手机号可能带空格、带横线邮箱可能大小写混用。所以这个练习的核心是两个字识别。识别靠正则的分组能力。手机号的正则要写完整匹配11位数字还得过滤掉13位以上的纯数字串。身份证号要匹配18位数字且最后一位可能是X。域名部分要兼容example.com、example.com.cn这种多级域名。参考实现import re def mask_sensitive(text: str) - str: phone_pattern re.compile(r(?!\d)(1\d{2})\d{4}(\d{4})(?!\d)) email_pattern re.compile(r(?Pprefix.)[\w.-](?Pdomain[\w.-])) idcard_pattern re.compile(r(?!\d)(\d{6})\d{8}(\d{3}[\dXx])(?!\d)) text phone_pattern.sub(r\1****\2, text) text email_pattern.sub(lambda m: f{m.group(prefix)}***{m.group(domain)}, text) text idcard_pattern.sub(r\1********\2, text) return text为什么身份证正则里最后要用[\dXx]身份证号最后一位可能是数字也可能是大写的X或小写的x。只写\d会把x结尾的真实身份证号漏掉导致没有打码这就是生产事故。把字符类扩到[\dXx]一个字符的差异覆盖的场景差很多。为什么手机号要做前后断言不加(?!\d)和(?!\d)的话一个13位数字的订单号也会被当成手机号打码。加了负向断言后只有前后都不是数字的11位数字组合才会被匹配误伤率大幅降低。这是我强烈建议你在真实项目里也保留的习惯花两秒钟写上去换来的是不会把业务订单号批量毁容。实操心得掩码替换有一个容易忽略的坑电话、邮箱、身份证号的匹配顺序。我建议把手机号放第一个身份证号放第二个邮箱最后。原因是手机号正则比较简单匹配失败很快先跑成本低身份证号匹配要遍历更长的数字串放前面也不会出问题。顺序的实质是让你在做完第一遍替换后后面的正则不会被已替换出的****干扰。3.3 练习三结构化文本解析器需求描述有一段类似配置文件格式的文本server: host: 192.168.1.10 port: 8080 enabled: true timeout: 30 database: host: db.internal port: 5432 user: admin password: secret要求写一个解析器把缩进表示层级、键: 值形式的文本解析成嵌套字典同时把true、false、纯数字字符串自动转换成布尔值和整数。实现思路与代码from pprint import pprint def parse_indented_text(text: str): lines text.splitlines() root {} # 用一个栈记录每一级的字典 stack [(0, root)] for line in lines: if not line.strip() or line.startswith(#): continue indent len(line) - len(line.lstrip()) content line.strip() key, _, value content.partition(:) key key.strip() while stack and indent stack[-1][0]: stack.pop() current_dict stack[-1][1] if value.strip(): raw_value value.strip() if raw_value true: parsed True elif raw_value false: parsed False else: try: parsed int(raw_value) except ValueError: parsed raw_value current_dict[key] parsed else: new_dict {} current_dict[key] new_dict stack.append((indent, new_dict)) return root sample server: host: 192.168.1.10 port: 8080 enabled: true timeout: 30 database: host: db.internal port: 5432 user: admin password: secret pprint(parse_indented_text(sample))运行后输出的结果字典是这样的{ server: {enabled: True, host: 192.168.1.10, port: 8080, timeout: 30}, database: {host: db.internal, port: 5432, user: admin, password: secret} }栈这个数据结构在这里解决什么问题这个题目的技术含量不在正则而在用栈维护层级关系。文本里每一行的缩进决定了它在字典树中的位置。遇到一个不带值的键比如server:就创建一个新的子字典同时把这个子字典连同它的缩进级别压进栈。遇到下一个键时如果缩进比栈顶小就一路弹出回到正确的父级。你可以把栈想象成餐厅里的一摞盘子每次进一层就往上叠一个遇到缩进变少就往下收掉多余盘子。栈顶永远是当前正在写入的那个字典。类型转换不要直接用eval很多新手到这里会脱口而出用eval啊一个函数搞定。打住eval是一条恶劣的捷径。你永远不知道输入里会不会藏着一个危险的表达式而配置文件的来源你未必能完全控制。老老实实做白名单解析true和false先用字面量判断数字用int()尝试转不了就保留字符串。单测视角检查如何判断解析器处理了空值和注释行空行和#开头的行直接continue如何防止缩进使用Tab导致计算错误可以先expandtabs(4)把Tab都转成空格如何保证同名键在不同层级互不干扰因为每一级都是独立的子字典4. 实操过程与核心环节实现4.1 完整实操流程实录我用演练的方式来走一遍这三个练习的完整过程。第一步准备测试数据集不要一上来就拿完整数据跑。先准备一个“最小验证集”和“压力验证集”。最小验证集覆盖正常输入压力验证集专门放各种脏数据。我把练习一的样本做成了一个小文件# test_sample.log [2025-01-12 08:33:41] 用户登录成功 [2025/1/12 8:33:41] 用户修改密码 [2025.01.12 08:33:41.325] 订单创建完成 [2025-01-12 08:33:4108:00] 支付回调触发第二步写主函数跑通主流程先把三个练习各自封装成独立函数。这一步的目标很简单输入输出能跑通先不管性能优化和边界处理。第三步用压力数据验证把样本中插入几行乱七八糟的数据比如没有时间戳的行、空行、时间戳被截断的行。你会立刻发现练习一的normalize_timestamp存在一个问题——如果正则没匹配到时间戳当前版本的实现会原样返回整行。这其实正是产品需要的默认行为很合理。练习二也一样用纯数字订单号测试后发现如果不加断言订单号会被打码。加上断言后问题消失。第四步看解析结果手动校验类型练习三写完后用pprint打印输出你会直观看到哪些字段是int、哪些是bool、哪些还是str。逐个确认后就说明解析正确。4.2 参数选择与格式规划说明关于正则在三个练习中的不同用法我补充一个规划认知练习一的正则负责“提取”用的是捕获组练习二的正则负责“定位并替换”用的是sub练习三本质上不需要正则它靠缩进和partition分割正则反而容易把简单问题复杂化所以并不是所有题目都要硬上正则合理评估问题的复杂度选择最省力且易读的方案才是成熟工程师的做事方式。在时间戳格式规划上我建议统一采用%Y-%m-%d %H:%M:%S因为这个格式在日志排序、入库、前端展示方面兼容性最好。毫秒和时区信息不是不重要而是业务需求里没有要求可以单独保留在原始字段里不要混在归一化字段里。4.3 实操过程中的性能考量日志文件一上规模比如几万行你就会发现正则的处理速度成为关键瓶颈。这里有一个很实用的经验减少正则的回溯次数。以练习一的日期匹配为例\d{1,2}在匹配的时候引擎会尝试1位、再尝试2位虽然代价很小但如果整行匹配失败引擎还要回到起点重新尝试下一个可能的起始位置。为了减少这种损耗可以把“空格或T”分隔符这种常量部分尽量落到实处不要全部交给.*?这种松散的匹配。字符串替换也是同理。先整体执行一次text.splitlines()再逐行处理比每次在超大字符串上做全局正则替换要安全得多内存和CPU都更可控。5. 常见问题与排查技巧实录5.1 时间格式解析失败的三种典型情况情况一时区偏移导致strptime报错08:00不是所有Python版本都能用%z正确解析特别是Windows环境下的老版本。我的建议是像练习一那样先把时区信息去掉只保留纯日期时间。情况二毫秒字段导致格式不匹配2025-01-12 08:33:41.325带毫秒如果直接按%Y-%m-%d %H:%M:%S解析必然报错。要么在正则里把\.\d单独捕获要么在预处理阶段抹掉。练习里选的是预处理阶段直接抹除逻辑简单。情况三日期分隔符不统一/、-、.混用相当常见。先匹配、再统一替换的思路是最省心也最不容易漏边的做法。5.2 掩码替换里的“误伤”与“漏网”打码工具很怕两件事把不该打码的打了把该打的漏掉了。不该打码的打了是断言没写好。比如11位手机号前后如果挨着其他数字就会误伤订单号。加上(?!\d)和(?!\d)负向断言就好了。该打的漏掉了往往是格式变体太多。邮箱部分我故意漏掉了带的地址形式比如usertagexample.com因为真实项目中这种格式很少见为它破坏正则的可读性不划算。但遇到具体项目这类定制扩展要自己补上。5.3 文本解析器常见异常解析缩进文本最常见的异常是缩进层级突变。如果上一行缩进是2格突然来一个4格的行严格意义上的YAML解析器会直接报错。我这版的实现比较温和缩进突增只是作为更深一层处理不会崩。如果你的需求是要严格校验配置格式那就要在解析时增加断言判断一旦缩进跳跃不符合预期就抛出详细异常。还有一个容易踩到的是键名包含冒号。比如url: http://example.com的值部分就带冒号。处理方式是用partition(:)它只分割第一次出现的冒号后面的全部留在value里不会丢信息。典型问题根因解决方案时间戳解析报错时区、毫秒格式兼容性预处理统一格式误伤业务数字缺少边界断言加负向断言值带冒号导致分割错误使用split()改用partition()Tab缩进引发解析混乱len()计算空格数不准先expandtabs(4)布尔值变成字符串直接赋值原字符串白名单转换6. 练习心得与进阶建议三个练习做完你手里实际上已经有三个可以复用的工具模块了。把它们单独保存成一个text_utils.py日常清洗数据、处理配置文件、脱敏打印日志时直接from text_utils import mask_sensitive非常顺手。最后再说一个很多人忽视的细节写字符串处理代码时多给函数写文档字符串和类型注解。def normalize_timestamp(log_line: str) - str这行看似不起眼但在两个月后你回来看代码时它能在三秒钟内告诉你入参和出参长什么样。这个习惯价值被低估了。下一篇练习我计划往面向对象方向走用一个完整的案例把今天写的三个工具类串起来做成命令行工具。如果做题过程中有问题按弹栈思路想一想——代码世界里大多数困惑都是层级关系没理清。