Python: 字符串进阶
第06课我们学了字符串的基础操作,这一课来深入挖掘。字符串的不可变性到底意味着什么?除了基础方法还有哪些好用但不为人知的操作?中文字符在计算机里是怎么存的?学完这课,你对字符串的理解会上一个台阶。
1. 字符串的不可变性
第06课 FAQ 里提过——字符串一旦创建就不能修改。这里用具体例子来看"不可变"到底是什么意思。
text = "Hello"
# ❌ 不能这样修改——这行代码会报错
# text[0] = "h" # TypeError: 'str' object does not support item assignment
那如果我想把首字母小写怎么办?答案是创建一个新字符串:
text = "Hello"
new_text = "h" + text[1:] # 创建新字符串
print(new_text) # hello
print(text) # Hello——原字符串没变
(1) 不可变性的好处
# 安全的共享引用
a = "Python"
b = a # b 和 a 指向同一个字符串
# 不管怎么操作 a,b 都不会受影响
a = a.upper()
print(a) # PYTHON
print(b) # Python——b 还是原来的值
(2) 不可变性的代价
每次"修改"字符串都会创建新对象,如果在一个循环里反复拼接大量字符串,性能会很差:
# ❌ 不推荐:循环中拼接大量字符串
result = ""
for i in range(10000):
result += str(i) + ","
# 这样会创建 10000 个临时字符串对象
# ✅ 推荐:用列表收集 + join() 合并
parts = []
for i in range(10000):
parts.append(str(i))
result = ",".join(parts)
+= 完全没问题。
▶ 示例:模拟字符串方法的工作原理(难度⭐⭐)
# 自己实现一个"首字母大写"(模拟 capitalize 方法)
def my_capitalize(text):
if not text:
return text
first = text[0].upper()
rest = text[1:].lower()
return first + rest
print(my_capitalize("hello WORLD")) # Hello world
print(my_capitalize("python")) # Python
# 自己实现一个"替换"(模拟 replace 方法)
def my_replace(text, old, new):
parts = text.split(old) # 用 old 分割,得到中间的部分
return new.join(parts) # 用 new 重新组合
print(my_replace("one, two, one", "one", "1")) # 1, two, 1
输出:
# 函数定义成功
2. 字符串的查询方法
除了第06课学的 find() 和 count(),还有几个好用的查询方法。
(1) startswith() 和 endswith()
# 检查字符串以什么开头/结尾
url = "https://www.example.com"
print(url.startswith("https")) # True——是 HTTPS 协议
print(url.endswith(".com")) # True——是 .com 域名
print(url.endswith(".org")) # False
# 可以同时检查多个
print(url.endswith((".com", ".org", ".cn"))) # True——匹配了 .com
(2) 实际应用
# 文件扩展名检查
filename = "report.pdf"
if filename.endswith((".pdf", ".doc", ".docx")):
print("文档文件")
elif filename.endswith((".jpg", ".png", ".gif")):
print("图片文件")
elif filename.endswith((".py", ".js", ".html")):
print("代码文件")
else:
print("其他文件")
# URL 协议检查
link = "ftp://files.example.com"
if link.startswith(("http://", "https://")):
print("网页链接")
elif link.startswith("ftp://"):
print("FTP 链接")
运行结果:
文档文件
FTP 链接
(3) removeprefix() 和 removesuffix()(Python 3.9+)
这两个是 Python 3.9 新增的方法,用来去掉前缀或后缀:
url = "https://www.example.com"
# 去掉协议头
domain = url.removeprefix("https://")
print(domain) # www.example.com
# 去掉域名后缀
site_name = url.removesuffix(".com")
print(site_name) # https://www.example
# 如果字符串不以指定内容开头/结尾,返回原字符串
print(url.removeprefix("ftp://")) # https://www.example.com(没变)
url[len("https://"):] 或者 url.split("://", 1)[1]。removeprefix() 和 removesuffix() 让代码更直观。
3. split() 的高级用法
第06课学过基础的分割,这里学几个更强大的用法。
(1) 指定分割次数
# 限制分割次数——maxsplit 参数
text = "one,two,three,four,five"
print(text.split(",", 1)) # ['one', 'two,three,four,five']
print(text.split(",", 2)) # ['one', 'two', 'three,four,five']
print(text.split(",", 3)) # ['one', 'two', 'three', 'four,five']
(2) rsplit():从右往左分割
text = "one,two,three,four,five"
print(text.rsplit(",", 1)) # ['one,two,three,four', 'five']
print(text.rsplit(",", 2)) # ['one,two,three', 'four', 'five']
(3) splitlines():按行分割
multiline = """第一行
第二行
第三行"""
lines = multiline.splitlines()
print(lines) # ['第一行', '第二行', '第三行']
# 保留换行符
lines = multiline.splitlines(True)
print(lines) # ['第一行\n', '第二行\n', '第三行']
(4) partition() 和 rpartition()
partition() 不仅分割字符串,还保留分隔符的位置信息:
text = "user@example.com"
# 按 @ 分成三部分:(左边, 分隔符, 右边)
parts = text.partition("@")
print(parts) # ('user', '@', 'example.com')
# 如果没找到分隔符,返回 (原字符串, '', '')
print("hello".partition("@")) # ('hello', '', '')
# rpartition 从右边开始找
path = "/home/user/documents/file.txt"
last_slash = path.rpartition("/")
print(last_slash) # ('/home/user/documents', '/', 'file.txt')
partition() 比 split() 更适合"提取"场景——它明确告诉你三部分各自是什么。
▶ 示例:解析 URL(难度⭐⭐)
# 用 partition 解析 URL
url = "https://www.example.com:8080/path/page.html?name=test&page=1"
# 拆协议
proto, _, rest = url.partition("://")
print(f"协议:{proto}") # https
# 拆域名和路径
host_part, _, path_and_query = rest.partition("/")
print(f"主机:{host_part}") # www.example.com:8080
# 拆端口
host, _, port = host_part.partition(":")
print(f"域名:{host}") # www.example.com
print(f"端口:{port}") # 8080
# 拆路径和查询参数
path, _, query = path_and_query.partition("?")
print(f"路径:/{path}") # /path/page.html
print(f"查询:{query}") # name=test&page=1
运行结果:
协议:https
主机:www.example.com:8080
域名:www.example.com
端口:8080
路径:/path/page.html
查询:name=test&page=1
4. 字符串的对齐和填充
Python 提供了一组方法让字符串在指定宽度内左对齐、右对齐或居中对齐。
text = "Python"
print(text.ljust(10)) # 'Python '(左对齐,右边补空格)
print(text.rjust(10)) # ' Python'(右对齐,左边补空格)
print(text.center(10)) # ' Python '(居中,两边补空格)
# 可以指定填充字符
print(text.ljust(10, "-")) # 'Python----'
print(text.rjust(10, "-")) # '----Python'
print(text.center(10, "-")) # '--Python--'
(1) zfill():用零填充
常用于数字编号:
print("42".zfill(5)) # 00042
print("-42".zfill(5)) # -0042——负号在前面
print("3.14".zfill(8)) # 00003.14
# 生成三位数编号
for i in range(1, 11):
filename = f"photo_{str(i).zfill(3)}.jpg"
print(filename, end=" ")
运行结果:
photo_001.jpg photo_002.jpg photo_003.jpg photo_004.jpg photo_005.jpg
photo_006.jpg photo_007.jpg photo_008.jpg photo_009.jpg photo_010.jpg
▶ 示例:生成对齐报表(难度⭐⭐)
# 用 rjust 和 ljust 对齐表格
items = [
("苹果", 5.0, 3),
("香蕉", 3.5, 5),
("牛奶", 12.0, 2),
]
# 表头
header_name = "商品".ljust(8)
header_price = "单价".rjust(6)
header_qty = "数量".rjust(4)
header_total = "总价".rjust(6)
print(f"{header_name}{header_price}{header_qty}{header_total}")
print("-" * 28)
# 数据行
for name, price, qty in items:
total = price * qty
name_col = name.ljust(8)
price_col = f"{price:.1f}".rjust(6)
qty_col = str(qty).rjust(4)
total_col = f"{total:.1f}".rjust(6)
print(f"{name_col}{price_col}{qty_col}{total_col}")
运行结果:
商品 单价 数量 总价
────────────────────────────
苹果 5.0 3 15.0
香蕉 3.5 5 17.5
牛奶 12.0 2 24.0
5. 字符编码:ord() 和 chr()
每个字符在计算机里都对应一个数字(码点)。ord() 获取字符的数字编码,chr() 反过来把数字转成字符。
# 查看字符的 Unicode 码点
print(ord("A")) # 65
print(ord("中")) # 20013
print(ord("\u2764")) # 10084(❤ single codepoint)
# 根据码点获取字符
print(chr(65)) # A
print(chr(20013)) # 中
print(chr(10084)) # ❤
# 生成所有大写字母
for code in range(ord("A"), ord("Z") + 1):
print(chr(code), end=" ")
# 输出:A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
(1) 什么是 Unicode
简单来说,Unicode 是一个全球统一的字符编码标准——它给世界上所有文字(中文、英文、阿拉伯文、日文……)的每个字符都分配了一个唯一的数字编号。Python 3 的字符串内部就是用 Unicode 存储的,所以它天生支持多语言。
# Python 3 的字符串天然支持 Unicode
text = "Hello,你好,مرحبا,こんにちは"
print(text) # 全部正常显示
print(len(text)) # 28——每个字符算一个长度
# 用 \u 转义符也可以表示字符
print("\u0048") # H
print("\u4e2d\u6587") # 中文
str(字节)和 unicode(文字),处理中文经常出错。Python 3 统一了——所有字符串都是 Unicode,中英文一视同仁。这是 Python 3 做得最好的设计之一。
(2) 编码和解码
虽然 Python 内部用 Unicode,但文件存储和网络传输用的是字节(bytes)。所以需要编码(encode)和解码(decode):
text = "你好,世界"
# 编码:字符串 → 字节
utf8_bytes = text.encode("utf-8")
print(utf8_bytes) # b'\xe4\xbd\xa0\xe5\xa5\xbd...'
print(len(utf8_bytes)) # 15(UTF-8 下每个中文字符占 3 字节)
gbk_bytes = text.encode("gbk")
print(len(gbk_bytes)) # 10(GBK 下每个中文字符占 2 字节)
# 解码:字节 → 字符串
decoded = utf8_bytes.decode("utf-8")
print(decoded) # 你好,世界
# 如果编码和解码方式不匹配,会报错
# gbk_bytes.decode("utf-8") # UnicodeDecodeError!
encoding="utf-8" 基本能解决。
6. 字符串格式化补充
除了 f-string,Python 还有两种老的格式化方式。虽然 f-string 是现在的主流,但你可能会在旧代码中看到它们。
(1) format() 方法(Python 3.0+)
# 按位置
print("我叫{},今年{}岁。".format("Charlie", 18))
# 我叫Charlie,今年18岁。
# 按名称
print("我叫{name},今年{age}岁。".format(name="Diana", age=22))
# 我叫Diana,今年22岁。
# 格式化数字
print("圆周率:{:.3f}".format(3.14159))
# 圆周率:3.142
(2) % 格式化(Python 2 时代的老写法)
# 你可能会在旧代码中看到这种写法
print("我叫%s,今年%d岁。" % ("Alice", 25))
# 我叫Alice,今年25岁。
print("圆周率:%.2f" % 3.14159)
# 圆周率:3.14
format() 在需要"延迟格式化"(比如先定义模板后面再填值)时还有用。% 格式化看到认识就行,新代码不要用了。
7. 常见应用场景
- 文件名批量处理:用
endswith()筛选特定格式的文件,用zfill()生成统一编号。 - URL 解析:用
partition()和removeprefix()解析 URL 的协议、域名、路径。 - CSV 数据处理:用
split()加maxsplit控制分割次数,避免字段内的逗号干扰。 - 日志格式化:用
center()和ljust()生成对齐的日志输出,便于阅读。 - 编码转换:处理不同来源的文本文件时,用
encode()和decode()确保编码一致。 - 数据验证:用
startswith()和endswith()检查输入格式(邮箱、电话、链接)。
❓ 常见问题
bytes 类型单独处理。这让字符串操作变得直观——len("中") 在任何系统上都是 1。代价是需要手动编码/解码才能读写文件或网络。split() 和 partition() 什么时候用哪个?split(),需要保留分隔符信息的用 partition()。partition() 总是返回三个元素(左边、分隔符、右边),可以用元组解包直接赋值:left, sep, right = text.partition(":")。而 split() 返回列表,数量不确定。如果你确定分隔符只出现一次并且需要提取分隔符前后的内容,partition() 是最佳选择。ljust() 和 f-string 的 {value:10} 有什么区别?text.ljust(10) 等价于 f"{text:<10}"。f-string 的写法更灵活(可以同时控制对齐和数字格式),而方法调用更直观。个人偏好问题,选一种保持一致就好。f-string 的对齐符号:< 左对齐、> 右对齐、^ 居中对齐:f"{text:^10}"。📖 小节
- 字符串不可变:每次"修改"都创建新对象;大量拼接用
join()而非+= - 查询方法:
startswith()/endswith()检查首尾,支持元组参数同时检查多个 - Python 3.9+:
removeprefix()/removesuffix()优雅去掉前缀/后缀 - 高级分割:
split(maxsplit=N)限制次数,rsplit()从右开始,splitlines()按行分割 partition()保留分隔符信息,适合结构化解析- 对齐方法:
ljust()/rjust()/center()/zfill(),也可以指定填充字符 ord()获取字符 Unicode 码点,chr()将码点转回字符- Python 3 字符串内部使用 Unicode,编码/解码用
encode()/decode(),推荐 UTF-8
📝 作业
-
基础题(难度⭐):给定文件名列表
files = ["report.pdf", "photo.jpg", "script.py", "notes.txt", "index.html"],用endswith()筛选出:- 所有图片文件(.jpg、.png、.gif)
- 所有文档文件(.pdf、.doc、.docx、.txt)
- 所有代码文件(.py、.js、.html、.css)
-
进阶题(难度⭐⭐):写一个"文件名批量格式化器"。给定一个列表
photos = ["IMG_1.jpg", "IMG_2.jpg", ..., "IMG_12.jpg"],将其重命名为photo_001.jpg、photo_002.jpg、...、photo_012.jpg格式。提示:用partition()分离文件名和扩展名,用zfill()补零。 -
挑战题(难度⭐⭐⭐):写一个"简易模板引擎"。给定一个模板字符串
template和一个包含变量的字典data,将模板中的{变量名}替换为对应的值。TEXT 📖 仅展示template = "您好,{name}!您的订单 {order_id} 已{status}。预计送达时间:{delivery_time}。" data = { "name": "Alice", "order_id": "20260623001", "status": "发货", "delivery_time": "3个工作日内" } # 你的代码:实现 replace_template(template, data) 函数 # 输出:您好,Alice!您的订单 20260623001 已发货。预计送达时间:3个工作日内。额外要求:如果模板中的变量在 data 中不存在,保留
{变量名}原样输出(不替换)。提示:遍历data字典的每一对键值,对 template 执行replace()。