009、字符串是门手艺活:f-string、模板、编码、10 个生产技巧 009、字符串是门手艺活f-string、模板、编码、10 个生产技巧上周五晚上十一点我被运维同事从被窝里拽起来——线上日志系统突然报了一堆UnicodeDecodeError所有包含中文的请求日志全部写不进去。我远程连上去一看代码里写的是log.write(f用户 {name} 登录成功)看起来人畜无害但问题出在name是从一个旧系统的API返回的那个接口返回的编码是GBK而我们的日志文件默认用UTF-8打开。Python在拼接f-string时字符串本身是Unicode对象但写入文件时编码转换炸了。这个坑让我意识到字符串处理从来不是写个引号完事那么简单。f-string你以为你懂了其实你只用了10%f-string是Python 3.6引入的语法糖但大多数人只拿它做变量替换。我见过最离谱的写法是# 别这样写这是把f-string当format用name张三age28resultf姓名{name}年龄{age}这没问题但f-string真正的威力在表达式求值。你可以直接在花括号里写任何Python表达式# 这里踩过坑花括号里可以调用函数但别写太复杂price99.9discount0.8print(f折后价{price*discount:.2f}元)# 直接计算并格式化# 甚至可以调用方法items[苹果,香蕉,橘子]print(f购物车{, .join(items)})# 这里踩过坑join返回新字符串原列表不变生产环境里最实用的技巧是f-string的调试模式——Python 3.8新增的语法# 调试时直接打印变量名和值省得写 print(x , x)x42yhelloprint(f{x},{y})# 输出x42, yhello# 甚至可以加格式化print(f{x:08b})# 输出x00101010二进制补零这个特性在排查线上问题时简直是救命稻草。有一次接口返回的数据结构特别复杂我直接在日志里写f{response.json()}瞬间定位到某个字段类型不对。模板字符串被低估的生产力工具很多人不知道Python标准库里有string.Template或者觉得它不如f-string好用。但在某些场景下模板字符串比f-string更安全、更可控。比如你从数据库里读出一堆配置模板需要动态替换占位符fromstringimportTemplate# 这里踩过坑模板字符串的占位符是 $name 或 ${name}template_str尊敬的${name}您的订单${order_id}已发货预计${delivery_date}送达。templateTemplate(template_str)# 安全替换不会执行任意代码resulttemplate.safe_substitute(name李四,order_idORD2024001,delivery_date2024-01-15)为什么说它安全因为f-string和format方法在替换时如果占位符里包含恶意代码可能会被注入执行。而Template的safe_substitute只会做简单的字符串替换不会解析任何Python表达式。这在处理用户输入的模板时特别重要——别问我怎么知道的我见过有人把用户昵称直接塞进f-string结果昵称是{__import__(os).system(rm -rf /)}。编码每个Python程序员都该懂的字符战争编码问题是我见过最多的生产事故来源没有之一。Python 3默认用Unicode处理字符串这本来是好事但很多人不理解字符串和字节串的区别。# 字符串是给人看的字节串是给机器看的text你好# 这是str类型Unicode字符串bytes_datatext.encode(utf-8)# 这是bytes类型字节序列print(type(text))# class strprint(type(bytes_data))# class bytes# 这里踩过坑bytes和str不能直接拼接# result text bytes_data # 会报TypeError最常见的坑是从文件读数据时默认用系统编码打开。Windows上是GBKLinux上是UTF-8。如果你的代码在Windows上开发部署到Linux上文件读写就会炸。# 别这样写依赖系统默认编码withopen(data.txt)asf:contentf.read()# 应该显式指定编码withopen(data.txt,encodingutf-8)asf:contentf.read()另一个坑是网络传输的数据通常是字节串但很多人直接当字符串处理。比如从requests库获取响应内容importrequests resprequests.get(https://api.example.com/data)# resp.text 是字符串resp.content 是字节串# 如果API返回的是GBK编码用resp.text会乱码# 正确做法resp.encodinggbk# 手动指定编码dataresp.text10个生产技巧从血泪史中提炼1. 字符串拼接用join别用# 别这样写每次都会创建新字符串O(n²)复杂度resultforiteminitems:resultitem,# 应该这样写join只创建一次新字符串result,.join(items)2. 字符串格式化优先用f-string# 别这样写可读性差print(用户 %s 年龄 %d%(name,age))# 也别这样写虽然比%好一点print(用户 {} 年龄 {}.format(name,age))# 这样写最清晰print(f用户{name}年龄{age})3. 处理大字符串用io.StringIOfromioimportStringIO# 别这样写大量拼接会爆内存large_strforiinrange(100000):large_strf第{i}行\n# 应该这样写bufferStringIO()foriinrange(100000):buffer.write(f第{i}行\n)resultbuffer.getvalue()4. 字符串切片用start:stop:steptextPython编程# 反转字符串reversed_texttext[::-1]# 程编nohtyP# 每隔一个字符取一个every_othertext[::2]# Pto编5. 检查字符串开头结尾用startswith/endswith# 别这样写ifurl[:7]http://orurl[:8]https://:pass# 应该这样写ifurl.startswith((http://,https://)):pass6. 字符串查找用in别用find# 别这样写iftext.find(error)!-1:pass# 应该这样写iferrorintext:pass7. 去除空白用strip别自己写循环# 别这样写whiletextandtext[-1] :texttext[:-1]# 应该这样写texttext.strip()# 去除两端空白texttext.lstrip()# 去除左侧空白texttext.rstrip()# 去除右侧空白8. 字符串替换用replace别用正则# 简单替换用replacetexthello worldnew_texttext.replace(world,python)# 只有复杂模式才用正则importre text电话138-1234-5678new_textre.sub(r\d{3}-\d{4}-\d{4},***-****-****,text)9. 多行字符串用三引号别用\n拼接# 别这样写sqlSELECT * FROM users WHERE name name# 应该这样写而且用参数化查询防止SQL注入sqlf SELECT * FROM users WHERE name %s cursor.execute(sql,(name,))10. 字符串比较用别用is# 这里踩过坑is比较的是内存地址不是值ahellobhelloprint(ab)# True值相等print(aisb)# TruePython会缓存短字符串但不保证# 别依赖字符串驻留机制ahello worldbhello worldprint(aisb)# 可能是False取决于Python实现个人经验性建议字符串处理看起来简单但生产环境里80%的编码问题都出在我以为上。我的建议是所有文件操作都显式指定编码别偷懒用默认值。UTF-8是通用选择但也要考虑历史遗留系统的GBK。f-string的调试模式{var}是排查问题的利器线上日志里多用它能省去大量加print的时间。处理用户输入时永远用Template或format别用f-string直接拼接。安全第一功能第二。理解str和bytes的区别这是Python 3最重要的概念之一。遇到编码错误先检查类型。字符串是不可变对象任何修改操作都会创建新字符串。大量拼接时用join或StringIO。最后分享一个我常用的调试技巧当遇到字符串乱码时先打印出字节序列的hex值看看实际是什么编码text你好print(text.encode(utf-8).hex())# e4bda0e5a5bdprint(text.encode(gbk).hex())# c4e3bac3这样能快速判断数据来源的编码比瞎猜靠谱多了。字符串处理是门手艺活多踩几个坑就熟练了。