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="张三"age=28result=f"姓名:{name},年龄:{age}"这没问题,但f-string真正的威力在表达式求值。你可以直接在花括号里写任何Python表达式:
# 这里踩过坑:花括号里可以调用函数,但别写太复杂price=99.9discount=0.8print(f"折后价:{price*discount:.2f}元")# 直接计算并格式化# 甚至可以调用方法items=["苹果","香蕉","橘子"]print(f"购物车:{', '.join(items)}")# 这里踩过坑,join返回新字符串,原列表不变生产环境里最实用的技巧是f-string的调试模式——Python 3.8新增的=语法:
# 调试时直接打印变量名和值,省得写 print("x =", x)x=42y="hello"print(f"{x=},{y=}")# 输出:x=42, y='hello'# 甚至可以加格式化print(f"{x=:08b}")# 输出:x=00101010,二进制补零这个特性在排查线上问题时简直是救命稻草。有一次接口返回的数据结构特别复杂,我直接在日志里写f"{response.json()=}",瞬间定位到某个字段类型不对。
模板字符串:被低估的生产力工具
很多人不知道Python标准库里有string.Template,或者觉得它不如f-string好用。但在某些场景下,模板字符串比f-string更安全、更可控。
比如你从数据库里读出一堆配置模板,需要动态替换占位符:
fromstringimportTemplate# 这里踩过坑:模板字符串的占位符是 $name 或 ${name}template_str="尊敬的${name},您的订单${order_id}已发货,预计${delivery_date}送达。"template=Template(template_str)# 安全替换,不会执行任意代码result=template.safe_substitute(name="李四",order_id="ORD2024001",delivery_date="2024-01-15")为什么说它安全?因为f-string和format方法在替换时,如果占位符里包含恶意代码,可能会被注入执行。而Template的safe_substitute只会做简单的字符串替换,不会解析任何Python表达式。这在处理用户输入的模板时特别重要——别问我怎么知道的,我见过有人把用户昵称直接塞进f-string,结果昵称是{__import__('os').system('rm -rf /')}。
编码:每个Python程序员都该懂的"字符战争"
编码问题是我见过最多的生产事故来源,没有之一。Python 3默认用Unicode处理字符串,这本来是好事,但很多人不理解"字符串"和"字节串"的区别。
# 字符串是给人看的,字节串是给机器看的text="你好"# 这是str类型,Unicode字符串bytes_data=text.encode("utf-8")# 这是bytes类型,字节序列print(type(text))# <class 'str'>print(type(bytes_data))# <class 'bytes'># 这里踩过坑:bytes和str不能直接拼接# result = text + bytes_data # 会报TypeError最常见的坑是:从文件读数据时,默认用系统编码打开。Windows上是GBK,Linux上是UTF-8。如果你的代码在Windows上开发,部署到Linux上,文件读写就会炸。
# 别这样写,依赖系统默认编码withopen("data.txt")asf:content=f.read()# 应该显式指定编码withopen("data.txt",encoding="utf-8")asf:content=f.read()另一个坑是:网络传输的数据通常是字节串,但很多人直接当字符串处理。比如从requests库获取响应内容:
importrequests resp=requests.get("https://api.example.com/data")# resp.text 是字符串,resp.content 是字节串# 如果API返回的是GBK编码,用resp.text会乱码# 正确做法:resp.encoding="gbk"# 手动指定编码data=resp.text10个生产技巧:从血泪史中提炼
1. 字符串拼接用join,别用+
# 别这样写,每次+都会创建新字符串,O(n²)复杂度result=""foriteminitems:result+=item+","# 应该这样写,join只创建一次新字符串result=",".join(items)2. 字符串格式化优先用f-string
# 别这样写,可读性差print("用户 %s 年龄 %d"%(name,age))# 也别这样写,虽然比%好一点print("用户 {} 年龄 {}".format(name,age))# 这样写最清晰print(f"用户{name}年龄{age}")3. 处理大字符串用io.StringIO
fromioimportStringIO# 别这样写,大量拼接会爆内存large_str=""foriinrange(100000):large_str+=f"第{i}行\n"# 应该这样写buffer=StringIO()foriinrange(100000):buffer.write(f"第{i}行\n")result=buffer.getvalue()4. 字符串切片用start:stop:step
text="Python编程"# 反转字符串reversed_text=text[::-1]# "程编nohtyP"# 每隔一个字符取一个every_other=text[::2]# "Pto编"5. 检查字符串开头结尾用startswith/endswith
# 别这样写ifurl[:7]=="http://"orurl[:8]=="https://":pass# 应该这样写ifurl.startswith(("http://","https://")):pass6. 字符串查找用in,别用find
# 别这样写iftext.find("error")!=-1:pass# 应该这样写if"error"intext:pass7. 去除空白用strip,别自己写循环
# 别这样写whiletextandtext[-1]==" ":text=text[:-1]# 应该这样写text=text.strip()# 去除两端空白text=text.lstrip()# 去除左侧空白text=text.rstrip()# 去除右侧空白8. 字符串替换用replace,别用正则
# 简单替换用replacetext="hello world"new_text=text.replace("world","python")# 只有复杂模式才用正则importre text="电话:138-1234-5678"new_text=re.sub(r"\d{3}-\d{4}-\d{4}","***-****-****",text)9. 多行字符串用三引号,别用\n拼接
# 别这样写sql="SELECT * FROM users WHERE name = '"+name+"'"# 应该这样写,而且用参数化查询防止SQL注入sql=f""" SELECT * FROM users WHERE name = %s """cursor.execute(sql,(name,))10. 字符串比较用==,别用is
# 这里踩过坑:is比较的是内存地址,不是值a="hello"b="hello"print(a==b)# True,值相等print(aisb)# True,Python会缓存短字符串,但不保证# 别依赖字符串驻留机制a="hello world"b="hello world"print(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这样能快速判断数据来源的编码,比瞎猜靠谱多了。字符串处理是门手艺活,多踩几个坑就熟练了。