Day08 · 字符串(二)
阶段:阶段一 · 核心基础 | 前置:Day07 | 技术基准:Python 3.15.0
学习目标
- 掌握三种字符串格式化方式:
%运算符、str.format()、f-string(重点推荐) - 理解对齐、填充、精度等格式控制
- 弄清编码与解码:Unicode、UTF-8、
encode()/decode(),以及乱码的成因 - 熟练使用字符串常用方法:
find/index/count/replace/split/join/strip/lower/upper/startswith/endswith等 - 对比五种字符串拼接方法,知道场景该用哪个
- 了解 f-string 在 3.12(PEP 701)后的七项现代写法,以及 3.15 新增的模板字符串 t-string(PEP 750)在 SQL/HTML 安全拼接中的思路
一、字符串格式化
把变量填进一段模板里,叫格式化。Python 有三代写法,越往后越推荐。
1.1、% 运算符(老式,C 语言风格)
类似 C 语言 printf:"模板" % (值1, 值2)。
| 格式符 | 含义 |
|---|---|
%s |
字符串 |
%d |
整数 |
%f |
浮点数 |
%.2f |
保留 2 位小数 |
%x |
十六进制 |
%% |
输出一个百分号 |
python
# Author: XiaoJia
name = "小嘉"
age = 18
print("姓名:%s,年龄:%d" % (name, age)) # 姓名:小嘉,年龄:18
print("圆周率:%.2f" % 3.14159) # 圆周率:3.14
print("百分号:100%%") # 百分号:100%
print("%-10s|" % "left") # 左对齐占 10 位
print("%10s|" % "right") # 右对齐占 10 位
%是历史写法,新项目不推荐,但读旧代码会遇到。面试或维护老项目时看到"%s" % name,要能立刻认出它等价于 f-string 的f"{name}"。
1.2、str.format()(第二代)
用 {} 占位,.format(值) 填充:
python
print("姓名:{},年龄:{}".format("小嘉", 18))
# 姓名:小嘉,年龄:18
print("{name} 正在学习 {lang}".format(name="小嘉", lang="Python"))
# 小嘉 正在学习 Python
print("{0} 学习 {1},{0}".format("小嘉", "Python"))
# 小嘉 学习 Python,小嘉
format 还支持对齐、精度、千分位:
python
print("{:>10}|".format("右对齐")) # 右对齐(占 10 位)
print("{:<10}|".format("左对齐")) # 左对齐
print("{:^10}|".format("居中")) # 居中
print("{:.2f}".format(3.14159)) # 3.14
print("{:,}".format(1234567)) # 1,234,567
1.3、f-string(第三代,强烈推荐)
Python 3.6+ 引入,在字符串前加 f,直接在 {} 里写变量甚至表达式,最简洁、最快、最好读。
python
name = "小嘉"
age = 18
print(f"姓名:{name},年龄:{age}") # 姓名:小嘉,年龄:18
print(f"明年 {age + 1} 岁") # 明年 19 岁
print(f"圆周率:{3.14159:.2f}") # 圆周率:3.14
# 对齐与填充
print(f"{'居中':^10}|") # 居中(^)
print(f"{1234567:,}") # 1,234,567
# 3.12+ f-string 还支持 = 调试写法
print(f"{age=}, {name=}") # age=18, name='小嘉'
小嘉结论:日常一律用 f-string ,
%和.format()能看懂即可。本课程后面的示例也以 f-string 为主。
1.4、f-string 现代能力(3.12+ 演进,Python 3.15 实测)
f-string 不是一成不变的。它 3.6 出生时只是"把变量塞进花括号";3.8 加了 = 调试语法;3.12 通过 PEP 701 对 f-string 的解析器做了一次全面重写,放开了一批老版本下会直接 SyntaxError 的写法。下面七项全部在本机 Python 3.15.0 实测通过------你在旧教程里看到的"f-string 不能嵌套同款引号"之类说法,3.12 之后已经过时。
① 嵌套同款引号:老规定是"f-string 外层用双引号,内层表达式就只能用单引号",反之亦然。3.12 后内外层可以用同款引号,解释器能正确配对。
python
# Author: XiaoJia
name = "小嘉"
print(f"外层{f"内层{name}"}")
# 输出:外层内层小嘉
② 表达式内允许反斜杠 :老版本在 {} 里写 '\n' 会 SyntaxError,因为反斜杠在 f-string 字面量里是非法的;3.12 后表达式区可以自由写转义符。
python
print(f"换行符:{'\n'!r}")
# 输出:换行符:'\n'
③ 字典键直写 :老版本里 f"{d['k']}" 这种"外层双引号、键用单引号"是允许的,但只要引号冲突就得先把字典项取到变量里。3.12 后任意引号组合都能直写,可读性更好。
python
user = {"name": "小嘉", "age": 18}
print(f"姓名:{user['name']},年龄:{user['age']}")
# 输出:姓名:小嘉,年龄:18
④ 表达式内可写注释 :{} 里的表达式现在允许用 # 写行内注释(注意注释必须在花括号内部)。
python
x = 10
print(f"结果 = {x + 5 # 加 5
}")
# 输出:结果 = 15
⑤ 多行表达式:花括号里的表达式可以跨行写,适合放较长的推导或函数调用。
python
print(f"求和 = {(
1 + 2 +
3 + 4
)}")
# 输出:求和 = 10
⑥ 调试语法 f"{x=}" 增强 :3.8 引入,会把"变量名=值"一起打出来,调试时极好用;3.12 后 = 两侧留白也会原样保留在输出里。
python
age = 18
city = "福州"
print(f"{age=}, {city=}")
# 输出:age=18, city='福州'
⑦ 动态格式宽度(嵌套格式说明符) :{x:{width}} 里的 width 本身可以是变量,实现"宽度由数据决定"的对齐。
python
width = 10
word = "小嘉"
print(f"[{word:>{width}}]") # 右对齐,宽 10
print(f"[{word:^{width}}]") # 居中,宽 10
# 输出:
# [ 小嘉]
# [ 小嘉 ]
版本演进脉络:
| 版本 | 里程碑 |
|---|---|
| 3.6 | PEP 498 引入 f-string,基础插值 |
| 3.8 | 增加 = 调试语法、= 后空格保留 |
| 3.12 | PEP 701 全面重写解析器,放开上述七项写法 |
| 3.15 | 本机基准,七项全部可用 |
小嘉提醒:新能力是"放开限制",不是"鼓励乱用"。两个坑要记牢------第一,绝不要用 f-string 拼接用户输入去构造 SQL ,例如
f"SELECT * FROM users WHERE name = '{user}'",用户只要输入' OR 1=1 --就被注入了;正确做法是用数据库驱动的参数化查询(Day34 数据库章节会细讲)。第二,嵌套格式说明符{x:{w}}里的引号要和外层错开,否则虽然 3.12 后能编译,读起来还是费劲,关键处建议先把宽度存成变量再传入。
1.5、模板字符串 t-string(PEP 750,Python 3.15)------安全拼接进阶
f-string 的本质是"构造时立刻把插值算好、拼成一段纯文本 "。这在打日志时很爽,但在拼 SQL、拼 HTML 这类"需要把字面文本和插值位置分开处理"的场景里反而危险------用户输入和字面文本混在一起,就容易出 SQL 注入、XSS。PEP 750 在 Python 3.15 引入了模板字符串字面量 t"..." ,它构造出来的不是一段拼好的文本,而是一个保留"字面片段 + 插值位置"结构的 string.templatelib.Template 对象。
python
# Author: XiaoJia
name = "世界"
t = t"hello {name}, sum={1 + 2}"
print(type(t).__name__)
# 输出:Template
print(str(t))
# 输出:Template(strings=('hello ', ', sum=', ''), interpolations=(Interpolation('世界', 'name', None, ''), Interpolation(3, '1 + 2', None, '')))
注意 str(t) 出来的不是拼接好的 "hello 世界, sum=3" ,而是一个描述结构的 repr:strings 是三段字面文本,interpolations 是两个插值点。这就是 t-string 和 f-string 最核心的区别------f-string 一构造就把结构抹平成纯文本,t-string 把结构留给你自己处理。
f-string vs t-string 对比:
| 维度 | f"..." | t"..." |
|---|---|---|
| 构造结果 | 拼好的 str |
string.templatelib.Template 对象 |
| 表达式求值 | 构造时立即求值、立即拼进文本 | 构造时立即求值 (非惰性),但只存进 Interpolation.value,不拼成文本 |
str(t)/format(t) |
就是拼好的字符串 | 返回结构 repr,不是拼接文本 |
| 字面与插值关系 | 已混合,不可分离 | 通过 __iter__ 交替吐出,可分离 |
| 典型用途 | 日志、UI、日常插值 | SQL 参数化、HTML 转义等安全拼接 |
Template 的迭代协议 :直接 for piece in t,它会交替 吐出字面文本(str)和插值点(Interpolation),这正是自写渲染器的抓手。
python
for piece in t:
print(type(piece).__name__, "->", repr(piece))
# 输出:
# str -> 'hello '
# Interpolation -> Interpolation('世界', 'name', None, '')
# str -> ', sum='
# Interpolation -> Interpolation(3, '1 + 2', None, '')
每个 Interpolation 在本机 3.15.0 上暴露四个属性:.value(已求值的值)、.expression(源码字符串)、.conversion(!r/!s/!a,无则 None)、.format_spec(格式说明符,无则空串)。注意 :它本身不带 convert()/format() 方法------转换与格式化由你的渲染器调用内置 repr()/str()/ascii() 与 format(value, spec) 完成,这正是 t-string"把渲染策略交给你"的设计哲学。
自写一个最小渲染器:把 Template 拼回普通字符串,体会"结构在手"的感觉。
python
import html
def apply_conv(v, conv):
if conv == 'r':
return repr(v)
if conv == 's':
return str(v)
if conv == 'a':
return ascii(v)
return str(v)
def render(tpl):
out = []
for piece in tpl:
if isinstance(piece, str):
out.append(piece) # 字面片段,原样保留
else: # Interpolation
if piece.conversion: # !r / !s / !a
out.append(apply_conv(piece.value, piece.conversion))
elif piece.format_spec: # 格式说明符作用于原始值
out.append(format(piece.value, piece.format_spec))
else:
out.append(str(piece.value))
return "".join(out)
print(render(t))
# 输出:hello 世界, sum=3
# 格式说明符与 conversion 在 t-string 里同样生效
print(render(t"pi={3.14159:.2f}"))
# 输出:pi=3.14
print(render(t"str={'hi'!r}"))
# 输出:str='hi'
安全用例一:render_sql------把插值换成占位符,防 SQL 注入 。字面 SQL 原样保留,所有用户输入的位置统一换成 %s,参数收集成 tuple 交给数据库驱动去绑定------这样用户输入永远不会拼进 SQL 文本本身。
python
def render_sql(tpl):
"""把 t-string 渲染成 (SQL 文本, 参数 tuple)。
字面文本原样保留,插值位置换成 %s,参数收集到 tuple。"""
parts, params = [], []
for piece in tpl:
if isinstance(piece, str):
parts.append(piece)
else:
parts.append("%s")
params.append(piece.value)
return "".join(parts), tuple(params)
user_input = "Alice'; DROP TABLE users;--"
age = 18
sql_t = t"SELECT * FROM users WHERE name = '{user_input}' AND age > {age}"
sql_text, args = render_sql(sql_t)
print(sql_text)
# 输出:SELECT * FROM users WHERE name = '%s' AND age > %s
print(args)
# 输出:("Alice'; DROP TABLE users;--", 18)
看到没有:恶意字符串 Alice'; DROP TABLE users;-- 没有被拼进 SQL 文本,而是乖乖待在参数 tuple 里,由数据库驱动在绑定时转义。这就是参数化查询防注入的本质,Day34 讲数据库时会反复用到这个思路。
安全用例二:render_html------用 html.escape 转义,防 XSS 。用户昵称这类输入要原样插进 HTML 时,必须先转义 < > & ' ",否则一段 <script> 就能在别人浏览器里执行。
python
def render_html(tpl):
out = []
for piece in tpl:
if isinstance(piece, str):
out.append(piece)
else:
out.append(html.escape(str(piece.value))) # 插值一律转义
return "".join(out)
nick = "<script>alert('xss')</script>"
page = t"<div>欢迎,{nick}</div>"
print(render_html(page))
# 输出:<div>欢迎,<script>alert('xss')</script></div>
<script> 被转义成 <script>,浏览器只会把它当文本显示,不会执行。这就是 Day42 Web 章节里"模板自动转义"的原理。
一个常见误解澄清:t-string 不是惰性求值 。有同学以为 t"..." 会把表达式拖到渲染时才算,实测并非如此------构造 Template 时表达式就已经求值,结果存在 Interpolation.value 里了。
python
seen = []
def track():
seen.append("eval'd")
return 999
t3 = t"v={track()}"
print(seen) # 构造完 Template,seen 已经非空
# 输出:["eval'd"]
t-string 的价值不在于"懒",而在于**"保留结构、不拼文本"**------让你能写一个对所有插值点统一做安全处理的渲染器,而不是把安全逻辑散落在每一次拼接里。
小嘉提醒:t-string 是 3.15 才落地的新特性,生态还没铺开,日常打日志、拼 UI 继续用 f-string 就好;它真正的主场是后面 Day34 数据库参数化、Day42 Web 模板转义这类"安全拼接"场景。今天先建立概念:凡是要把用户输入插进 SQL/HTML,别用 f-string 直接拼,要么用驱动自带的参数化,要么用 t-string + 自写渲染器统一转义。
二、编码与解码
2.1、为什么会有乱码
计算机只认数字。文字要存进计算机,就得给每个字符编个号,这套编号规则叫编码。常见编码:
- ASCII:只含英文字母和符号,128 个字符。
- Unicode:给世界上每个字符(含中文)一个唯一编号(码点)。
- UTF-8:Unicode 的一种存储实现,变长编码,英文字符占 1 字节、中文通常占 3 字节,是目前互联网最通用的编码。
- GBK:中文 Windows 本地编码,只兼容中英文。
乱码的本质:用 A 编码存的文字,用 B 编码去读,对不上号就花了。
2.2、str 与 bytes 的转换
Python 3 中:文本是 str(Unicode),二进制是 bytes。二者通过 encode()(编码)和 decode()(解码)互转。
python
# Author: XiaoJia
s = "你好"
b = s.encode("utf-8") # str → bytes(编码)
print(b) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(type(b)) # <class 'bytes'>
s2 = b.decode("utf-8") # bytes → str(解码)
print(s2) # 你好
# 指定编码,GBK 对比
print("你好".encode("gbk")) # b'\xc4\xe3\xba\xc3' 字节数与 utf-8 不同
python
# 常见坑:编码解码不一致 → 乱码或报错
raw = "你好".encode("utf-8")
# print(raw.decode("gbk")) # 乱码(不同字节表)
print(raw.decode("utf-8")) # 你好 编码解码用同一种就对
规则:存文件、网络传输用
bytes(指定编码 utf-8);读出来用同一种编码decode()回str。前后一致,永不乱码。
2.3、乱码是怎么产生的:一个完整案例
举个最常见的场景:你在 Windows 记事本里保存了一个中文文件,默认编码是 GBK,然后用 Python 按 UTF-8 去读,就乱了。
python
# 假设文件实际是 GBK 编码保存的
# wrong = open("note.txt", encoding="utf-8").read() # 乱码或 UnicodeDecodeError
# right = open("note.txt", encoding="gbk").read() # 用对编码就正常
排查口诀:先确认"这串字节是用什么编码写的",再用同一个编码去读。中文 Windows 老文件多为 GBK,Linux/Web 多为 UTF-8;不确定时,UTF-8 优先试,失败再试 GBK。
| 编码 | 英文占字节 | 中文占字节 | 典型场景 |
|---|---|---|---|
| ASCII | 1 | 不支持 | 早期英文系统 |
| UTF-8 | 1 | 3 | 现代互联网、Python 3 默认 |
| GBK | 1 | 2 | 中文 Windows 老文件 |
| Unicode | 2~4 | 2~4 | 内存中字符的统一编号 |
三、字符串常用方法
字符串方法极多,下面是最高频的一批(都返回新串,不改原串)。
3.1、查找与统计
python
s = "hello python"
print(s.find("python")) # 6 找到返回起始下标
print(s.find("java")) # -1 找不到返回 -1(不报错)
print(s.index("python")) # 6 类似 find,但找不到会报 ValueError
print(s.count("o")) # 2 统计 "o" 出现次数
3.2、大小写与填充
python
s = "Hello Python"
print(s.upper()) # HELLO PYTHON
print(s.lower()) # hello python
print(s.title()) # Hello Python
print(s.capitalize()) # Hello python 仅首字母大写
print("abc".center(10, "*")) # ***abc**** 居中填充
print("7".zfill(3)) # 007 左侧补零到 3 位
3.3、拆分与连接
python
# split 拆分:字符串 → 列表
print("2024-05-20".split("-")) # ['2024', '05', '20']
print("a,b,c,d".split(",", 2)) # ['a', 'b', 'c,d'] 最多拆 2 次
print("hello world".split()) # ['hello', 'world'] 默认按空白
# join 连接:可迭代对象 → 字符串
print("-".join(["2024", "05", "20"])) # 2024-05-20
print("".join(["Hello", " ", "World"]))# Hello World
print(",".join(str(i) for i in range(1, 6))) # 1,2,3,4,5
3.4、替换
python
s = "hello world"
print(s.replace("world", "python")) # hello python
print(s.replace("l", "L")) # heLLo worLd 全替换
print(s.replace("l", "L", 1)) # heLlo world 只替换前 1 个
3.5、去空白与判断
python
s = " hello python "
print("[" + s.strip() + "]") # [hello python] 去两端空白
print("[" + s.lstrip() + "]") # 去左端
print("[" + s.rstrip() + "]") # 去右端
print("###abc###".strip("#")) # abc 可指定去除字符
# 判断类(返回 bool)
print("python.py".endswith(".py")) # True
print("README".startswith("RE")) # True
print("abc".isalpha()) # True 是否全字母
print("123".isdigit()) # True 是否全数字
print(" ".isspace()) # True
strip()的实战意义:用户从键盘输入时常多打空格,input("姓名:").strip()是标配(Day10 讲 input)。注意它只去两端,中间的空格不动------"a b c".strip()还是"a b c"。
3.6、综合实战:清洗一段脏数据
把零散学到的方法组合起来,处理一条真实感很强的"脏"用户输入:
python
# Author: XiaoJia
raw = " 2024-05-20 14:30 "
# 1. 先去掉首尾空白
raw = raw.strip()
# 2. 按空白拆成两段(split 默认按任意空白,中间多个空格也能拆)
date_part, time_part = raw.split()
# 3. 把日期里的横杠换成点
date_part = date_part.replace("-", ".")
# 4. 判断是否以 2024 开头
print(date_part.startswith("2024")) # True
# 5. 拼回一句完整描述
print(f"日期 {date_part},时间 {time_part}")
输出:
text
True
日期 2024.05.20,时间 14:30
这一个小例子用到了 strip、split、replace、startswith、f-string 五个方法。真实项目里的数据清洗(Day41 数据分析实战)就是这类操作的放大版。
四、五种字符串拼接方法对比
| 方法 | 写法 | 适用场景 | 评价 |
|---|---|---|---|
+ |
a + b |
少量、简单拼接 | 直观,但每次都造新串,循环里低效 |
str.join(列表) |
"".join(parts) |
把列表/大量片段拼成一个串 | 性能最好,批量拼接首选 |
% |
"%s" % x |
老式格式化 | 可读性一般,旧代码维护 |
str.format() |
"{}".format(x) |
中等格式化 | 比 % 灵活 |
| f-string | f"{x}" |
日常插值 | 最推荐,简洁高效 |
python
# 对比:循环里拼接,+ vs join
parts = []
for i in range(5):
parts.append(f"第{i+1}项")
result = "|".join(parts) # 推荐:先收集到列表,最后一次 join
print(result) # 第1项|第2项|第3项|第4项|第5项
# 不要这样做(循环里反复 +,会产生大量中间串):
# s = ""
# for i in range(1000):
# s += str(i)
五、本章边界与回顾
Day08 把字符串从"会定义、会切片"推进到"会格式化、会编码、会加工"。至此你已经掌握字符串的全部日常操作:需要填变量用 f-string,需要清洗文本用 strip/split/replace,需要拼批量结果用 join,遇到乱码先查编码。
和 Day07 一样,所有方法都不改动原串、返回新串。这一点贯穿字符串始终,请在脑子里立成铁律。下一课 Day09 我们回到运算的通用规则------运算符与表达式。
六、小结与边界回顾
Day08 是字符串的"应用篇":格式化让变量优雅地进模板,编码解码让中文不再乱码,几十个方法让清洗文本变成拼图游戏。学完这两天,你已经能处理现实世界里绝大多数字符串任务------登录页的用户名清洗、报表里的日期拼接、日志里的字段提取,思路都一样。在格式化这条线上,你还看到了 f-string 在 3.12(PEP 701)后的七项现代写法,以及 3.15 新增的 t-string(PEP 750)------后者为后面 Day34 数据库参数化、Day42 Web 模板转义这类安全拼接场景打下概念基础。
记住三句话:日常插值用 f-string;批量拼接先收集列表再 join;遇到乱码先核对编码是否前后一致。下一课进入运算符与表达式,把今天零散的计算串成完整逻辑。
练习题
- 用三种方式格式化同一句话"姓名:小嘉,年龄:18":分别用
%、.format()、f-string。 - 把
"圆周率是3.14159"里的数字保留两位小数输出;再把数字1234567加千分位逗号。 - 写代码:
"你好"用 UTF-8 编码成 bytes,再用 GBK 解码会出现什么?用 UTF-8 解码又得到什么?为什么? - 给定
"a,b,,c,d,",用split(",")拆成列表,再用"-".join()拼回去,观察空字符串元素的影响。 - 用户输入
" 小嘉 ",用strip()处理后判断它是否等于"小嘉",体会为什么输入框一定要 strip。 - 思考题:为什么循环里拼接字符串推荐先
append到列表再join,而不是直接+=?(提示:字符串不可变) - (3.12+ 新写法)在本机 Python 3.15 上,依次写出下列 f-string 并运行验证:① 外层双引号、内层也用双引号嵌套的 f-string;②
{x=}调试语法同时打印两个变量;③{3.14159:.2f}配合动态宽度{x:>{w}}。观察它们在 3.11 及以前版本会不会报SyntaxError。 - (t-string 安全拼接)用
t"..."写一个render_sql(tpl):把模板渲染成(SQL 文本, 参数 tuple),把user = "Bob'; DROP TABLE t;--"插进去,打印 SQL 文本和参数 tuple,说明为什么恶意字符串不会进入 SQL 文本。
小结
- 格式化三代:
%(老式)→.format()(第二代)→ f-string(3.6+,推荐);f-string 支持表达式、对齐、=调试写法。 - f-string 现代能力(3.12 PEP 701 重写,3.15 实测):嵌套同款引号、表达式内反斜杠、字典键直写、表达式内注释、多行表达式、
=调试语法增强、动态格式宽度;但不要用 f-string 拼接用户输入构造 SQL。 - t-string(PEP 750,3.15):
t"..."返回string.templatelib.Template,构造时表达式立即求值但不拼文本 ,str(t)返回结构 repr;通过__iter__交替吐出字面片段与Interpolation(.value/.expression/.conversion/.format_spec),由自写渲染器做参数化(SQL)或转义(HTML)等安全拼接,呼应 Day34 数据库与 Day42 Web。 - 编码:ASCII → Unicode → UTF-8;乱码 = 编解码不一致;
str.encode("utf-8")编码为 bytes,bytes.decode("utf-8")解码回 str,前后同编码就不乱码。 - 高频方法:
find/index(查找)、count(统计)、replace(替换)、split/join(拆/连)、strip/lstrip/rstrip(去空白)、lower/upper/title(大小写)、startswith/endswith/isalpha/isdigit(判断)。 - 拼接五法:少量用
+或 f-string;批量先收集列表再join(性能最好);循环里别+=。 - 所有字符串方法都返回新串,不改原串(不可变性)。
扩展阅读
- 字符串方法完整列表:https://docs.python.org/3/library/stdtypes.html#string-methods
- 格式化字符串规范(PEP 3101):https://peps.python.org/pep-3101/
- f-string 全面重写(PEP 701):https://peps.python.org/pep-701/
- 模板字符串 t-string(PEP 750):https://peps.python.org/pep-0750/
- Unicode 与 UTF-8 入门:https://docs.python.org/3/howto/unicode.html
- 源目录教材参考:《跟小嘉学习Python 程序设计》一、Python 开发环境介绍和搭建.md 第四章 第五节(格式化、编码、字符串方法);Python字符串拼接方法详解.docx;Python字符串连接的5种方法.docx