基础篇 / 第 4 章
第 4 章 · 字符串详解
本章导读:字符串是编程中使用频率最高的数据类型——用户输入、文件内容、日志、HTML、JSON 全都是文本。本章讲透索引与切片、常用方法、f-string 格式化、不可变性的后果、转义与编码,以及中文开发者最关心的 Unicode 与正则基础。前置知识:会索引取值(第 2 章)、会写循环(第 3 章)。学完本章,你能熟练地对文本做增删改查与格式化,并在中文环境下避免编码陷阱。
字符串的本质
Python 的字符串(string)是不可变的 Unicode 字符序列。这句话里每个词都很重要:
| 性质 | 含义 | 影响 |
|---|---|---|
| 不可变(immutable) | 创建后不能修改内容 | 所有「修改」方法都返回新串 |
| Unicode | 一个字符就是一个 Unicode 码点 | 中文、emoji 都能正常处理 |
| 序列(sequence) | 有顺序、可索引、可切片 | 支持 len()、索引、in |
| 可哈希 | 能作为字典键 | 可做去重依据 |
字符串的创建方式
# 1. 引号(单/双等价)
s1 = 'Hello'
s2 = "World"
# 2. 三引号:多行
s3 = """第一行
第二行"""
# 3. 前缀修饰符
s4 = r"路径\n不是换行" # r = raw 原始字符串,反斜杠不转义
s5 = f"今天是{1 + 1}号" # f = formatted 格式化字符串
s6 = b"bytes data" # b = bytes 二进制串(不是 str)
print(s6.decode("utf-8")) # bytes data
s7 = u"unicode" # u 前缀在 Python 3 中是多余的
# 4. 从其他对象转换
print(str(123)) # '123'
print(str(3.14)) # '3.14'
print(str([1, 2])) # '[1, 2]'
print(repr("hi")) # "'hi'" 带引号的表示形式(调试用)
要点
str() 给「人能读的描述」,repr() 给「Python 语法的表示形式」。调试时用 repr() 更能看清类型,比如 str 和 repr 对同一个字符串输出分别是 hi 和 'hi'。索引与切片
2.1 索引:从 0 开始的编号
正数与负数索引
s = "Python"
# 0 1 2 3 4 5
# P y t h o n
# -6 -5 -4 -3 -2 -1
print(s[0]) # P 首字符
print(s[2]) # t
print(s[-1]) # n 末字符
print(s[-2]) # o 倒数第二个
print(len(s)) # 6 长度
越界会怎样
print(s[6]) # IndexError: string index out of range
print(s[-7]) # IndexError: 负数越界同样报错
# 安全取值
print(s[0:2]) # Py 用切片安全获取
常见坑
len(s) - 1 是最后一个合法下标(不是 len(s))。很多初学者写 range(len(s)) 时会误以为要减 1,其实不需要——range(len(s)) 正好覆盖 0 ~ len(s)-1。2.2 切片:截取子串
切片语法 s[start:stop:step],规则与 range 一致:左闭右开,start 默认 0,stop 默认到末尾,step 默认为 1。
切片的六种形态
s = "abcdefgh"
print(s[2:5]) # cde 从索引 2 取到 5(不含 5)
print(s[:4]) # abcd 前 4 个
print(s[4:]) # efgh 从 4 到末尾
print(s[:]) # abcdefgh 完整复制
print(s[-3:]) # fgh 最后 3 个
print(s[::2]) # aceg 步长 2
print(s[::-1]) # hgfedcba 反转!
print(s[::3]) # adgh 每隔 3 个取一个
切片边界的安全特性
s = "hello"
print(s[10:20]) # '' 超出范围也不报错,返回空串
print(s[-100:100]) # 'hello' 负数超界会被 Python 自动修正
print(s[3:1]) # '' start > stop 返回空串
# 这是切片比索引「友好」的地方:越界不报错
# 所以切片常用来做「安全截取」
要点索引越界会抛
IndexError,切片越界只会返回空串或被自动修正。因此在处理不确定长度的文本时,用切片比反复判断下标安全。2.3 不可变性的后果
为什么不能 s[0] = "X"
s = "hello"
# 错误:字符串不可变,不能赋值
# s[0] = "H" # TypeError: 'str' object does not support item assignment
# 正确:重新生成一个新字符串
s = "H" + s[1:] # "Hello"
print(s)
# 或者用专门的方法
s2 = "hello"
s3 = s2.capitalize() # "Hello",返回新串
print(s2, s3) # hello Hello(原串未变)
# 多个字符替换时,切片拼接
def replace_at(s, index, char):
"""把字符串第 index 个字符替换成 char。"""
return s[:index] + char + s[index + 1:]
print(replace_at("hello", 0, "H")) # Hello
2.4 多维切片与步长进阶
用切片实现实用的小工具
s = " Hello World "
print(s.strip()) # 'Hello World' 去掉首尾空白
print(s.strip(" ")) # 同上,显式指定去掉的字符
print(s.lstrip()) # 'Hello World ' 只去左侧
print(s.rstrip()) # ' Hello World' 只去右侧
print(s.upper()) # ' HELLO WORLD '
print(s.lower()) # ' hello world '
print(s.title()) # ' Hello World '
# 密码脱敏
phone = "13812345678"
masked = phone[:3] + "*" * 4 + phone[-4:]
print(masked) # 138****5678
# 每隔 n 个字符插入分隔符
csv = "abcdefg"
print(csv[::2]) # 'aceg' 交叉取值
常用字符串方法
Python 的字符串方法非常丰富,这里按用途分类整理。
3.1 查询类方法
| 方法 | 作用 | 示例 |
|---|---|---|
len(s) | 长度 | len("你好") → 2 |
s.count(x) | 出现次数 | "banana".count("a") → 3 |
s.find(x) | 首次出现位置,找不到返回 -1 | "hi".find("i") → 1 |
s.index(x) | 首次位置,找不到报错 | "hi".index("z") → ValueError |
x in s | 是否包含 | "a" in "banana" → True |
s.startswith(x) | 是否以…开头 | "py".startswith("p") → True |
s.endswith(x) | 是否以…结尾 | "py".endswith(".py") |
s.isdigit() | 是否全是数字 | "123".isdigit() → True |
s.isalpha() | 是否全是字母 | "abc".isalpha() → True |
s.isspace() | 是否全是空白 | " \t".isspace() → True |
s.isupper() | 是否全大写 | "AB".isupper() → True |
查询类的实战用法
text = "Python 3.12 is awesome"
print(text.count("o")) # 2
print(text.find("3")) # 7
print(text.find("Java")) # -1 找不到返回 -1
# print(text.index("Java")) # ValueError index 找不到会报错
# find 用来「安全查找」:-1 表示没找到
if text.find("Java") == -1:
print("不包含 Java")
elif text.find("Java") >= 0:
print("包含 Java")
# 批量判断
print("12345".isdigit()) # True
print("12a45".isdigit()) # False
print("".isdigit()) # False 空串返回 False
print(" ".isdigit()) # False
# 输入校验的常用组合
user_input = "abc123"
print(user_input.isalnum()) # True 只含字母和数字
print("a b".isalnum()) # False 有空格
# startswith 判断文件类型
filename = "report.pdf"
if filename.endswith((".pdf", ".docx")):
print("是文档文件")
if filename.startswith("report"):
print("是报表")
要点
find 返回 -1 不报错,index 找不到直接抛 ValueError。日常「判断是否存在」用 in 或 find,需要位置时两者都可以,index 常用于「缺失就是逻辑错误」的场景。3.2 修改类方法(都返回新串)
| 方法 | 作用 | 示例 |
|---|---|---|
s.replace(a, b) | 替换 | "a-b".replace("-", "+") |
s.split(sep) | 切成列表 | "a,b".split(",") |
s.join(list) | 列表拼成串 | ",".join(["a","b"]) |
s.strip/chomp | 去空白 | " x ".strip() |
s.upper/lower() | 大小写 | "aB".upper() |
s.capitalize() | 首字母大写 | "hello world".capitalize() |
s.zfill(n) | 左侧补零 | "7".zfill(3) → "007" |
replace 的第三个参数与 split 的多种用法
# replace 支持指定替换次数
s = "a-b-c-d"
print(s.replace("-", "+")) # a+b+c+d 默认全部替换
print(s.replace("-", "+", 2)) # a+b+c-d 只替换前 2 个
# split 的三种模式
csv = "name,age,city"
print(csv.split(",")) # ['name', 'age', 'city']
line = "苹果 香蕉 梨"
print(line.split()) # ['苹果','香蕉','梨'] 默认按任意空白切,且忽略首尾空白
path = "a.b.c"
print(path.split(".", 1)) # ['a', 'b.c'] 只切第一个
# join:把列表拼成字符串(用分隔符.join(列表))
print("-".join(["a", "b", "c"])) # a-b-c
print("".join(["1", "2"])) # 12
print(", ".join(f"{i}分" for i in [90, 85])) # 90分, 85分
# 注意:join 的调用者是「分隔符」,不是列表
print(", ".join(["a", "b"])) # 正确:逗号是分隔符
# print(["a", "b"].join(",")) # 错误:列表没有 join 方法
常见坑
join 的用法是分隔符.join(可迭代对象),不是列表.join(分隔符)。记不清就想想「用逗号把 ['a','b'] 连起来」——逗号在前,列表在后。3.3 判断类:验证输入的标准三连
写一个健壮的输入验证器
def validate_input(text: str) -> bool:
"""判断字符串是否为合法的正整数(允许前后空格)。"""
s = text.strip()
return s.isdigit() and int(s) > 0
tests = ["123", " 456 ", "abc", "-1", "0", ""]
for t in tests:
print(f"{t!r:10} -> {validate_input(t)}")
# '123' -> True
# ' 456 ' -> True
# 'abc' -> False
# '-1' -> False
# '0' -> False
# '' -> False
技巧
isdigit() 对全角数字 "123" 也返回 True,但 int() 能正确转换;而 "1.5".isdigit() 是 False。需要处理小数时要用 float() 配合 try/except。f-string:现代格式化首选
f-string(格式化字符串字面量)是 Python 3.6 引入的语法,在 {} 里直接写表达式,是目前最简洁、最快、最推荐的格式化方式。
f-string 的全部用法
name = "小明"
age = 18
score = 95.678
pi = 3.14159265
# 1. 基本用法
print(f"你好,{name}!") # 你好,小明!
print(f"{name}今年{age}岁") # 小明今年18岁
# 2. 里面可以直接写表达式(这比 .format() 强大之处)
print(f"明年{age + 1}岁") # 明年19岁
print(f"姓名首字母:{name[0]}") # 姓名首字母:小
print(f"{score:.1f}") # 95.7 保留 1 位小数
# 3. 格式说明符(冒号后面)
print(f"{pi:.3f}") # 3.142 定点保留 3 位
print(f"{pi:10.2f}") # 右对齐到 10 宽,保留 2 位
print(f"{pi:.2e}") # 3.14e+00 科学计数法
print(f"{1234567:,}") # 1,234,567 千位分隔
print(f"{1234567:_}") # 1_234_567 下划线分隔
print(f"{0.256:.1%}") # 25.6% 百分比
print(f"{255:#x}") # 0xff 十六进制
print(f"{255:#o}") # 0o377 八进制
print(f"{255:#b}") # 0b11111111 二进制
print(f"{42:>10}") # 右对齐 10 宽
# 4. 对齐符号
print(f"{'左':<10}|") # 左左左左左左左左左左|
print(f"{'中':^10}|") # 中 |
print(f"{'右':>10}|") # 右|
# 5. 嵌套表达式与引号(3.12 起同种引号也可嵌套)
d = {"k": "value"}
print(f"字典取值:{d['k']}") # 字典取值:value
print(f"内嵌 f-string: {f'{1+1}'}") # 内嵌 f-string: 2
# 6. = 号调试器(3.8+):同时打印表达式和值
x, y = 3, 4
print(f"{x=}, {y=}") # x=3, y=4
print(f"{x + y = }") # x + y = 7
4.1 三种格式化方式对比
| 方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 百分号 %(最老) | "%s %.1f" % (name, score) | 老代码里常见 | 类型必须匹配,易错 |
| .format() | "{:.1f}".format(score) | 可重用模板 | 要数花括号位置 |
| f-string(推荐) | f"{name} {score:.1f}" | 可写任意表达式,最快 | 3.6 以下不支持 |
同一种输出的三种写法
name, score = "小明", 95.678
# 百分号
print("%s 得了 %.1f 分" % (name, score))
# 小明 得了 95.7 分
# .format()
print("{} 得了 {:.1f} 分".format(name, score))
# 小明 得了 95.7 分
# f-string(推荐)
print(f"{name} 得了 {score:.1f} 分")
# 小明 得了 95.7 分
常见坑f-string 里的表达式不能含反斜杠(3.12 前),比如
f"{'\n'}" 会报错。3.12 起放宽了这条限制,但仍建议避免。另外 f-string 中引号嵌套要注意——外层用单引号时内层只能用双引号(或反过来)。技巧调试时用
f"{变量=}" 可以一次性看到「表达式=值」,比写 print("x =", x) 快得多,是排查问题的利器。4.2 实战:格式化一张成绩单
用 f-string 输出对齐的表格
students = [
("张三", 89.5),
("李四", 76.0),
("王小明", 92.3),
("赵六", 58.8),
]
print(f"{'姓名':<10}{'分数':>8}{'等级':>8}")
print("-" * 26)
for name, score in students:
level = "优秀" if score >= 90 else ("良好" if score >= 80 else "及格")
print(f"{name:<10}{score:>8.1f}{level:>8}")
print("-" * 26)
avg = sum(s for _, s in students) / len(students)
print(f"{'平均分':<10}{avg:>8.2f}")
# 姓名 分数 等级
# --------------------------
# 张三 89.5 良好
# 李四 76.0 及格
# 王小明 92.3 优秀
# 赵六 58.8 及格
# --------------------------
# 平均分 79.15
转义字符与原始字符串
5.1 常用转义字符
| 转义 | 含义 | 输出 |
|---|---|---|
\n | 换行符 | 另起一行 |
\t | 制表符 | 对齐用空格 |
\r | 回车 | 回到行首 |
\\ | 反斜杠本身 | \ |
\' \" | 引号 | ' " |
\0 | 空字符 | 不可见 |
\a | 响铃 | 终端会「滴」一声 |
\b | 退格 | 回到前一格 |
\f | 换页 | 打印时换一页 |
\x41 | 十六进制字符码 | A |
\u4f60 | Unicode 转义 | 你 |
转义字符实战
# 换行与制表
print("第一行\n第二行")
print("姓名\t年龄\t城市")
print("张三\t18\t北京")
# 引号嵌套
print("他说:\"你好\"") # 他说:"你好"
print('It\'s Python') # It's Python
# 路径中的反斜杠(Windows)
path = "C:\\Users\\name" # C:\Users\name
path2 = r"C:\Users\name" # 同上,原始字符串写法更直观
# 制表对齐的常见用法
print("名称\t数量\t单价\t总价")
print("苹果\t3\t5.00\t15.00")
print("香蕉\t12\t2.50\t30.00")
# Unicode 转义(处理特殊字符)
print("\u4f60\u597d") # 你好
print("A") # A
# 多行字符串
text = """这是一段
多行文本"""
print(text)
5.2 原始字符串与三引号
什么时候该用 r 前缀
# 原始字符串:反斜杠不解释,\n 就是两个字符
s = r"a\nb"
print(s) # a\nb (长度 4,不是换行)
print(len(s)) # 4
# 路径、正则表达式必须用原始字符串
regex = r"\d+\.\d*" # 正则里的 \d 不能被当成转义
winpath = r"C:\new\test" # 路径里的 \n 不该变成换行
# 三引号:既是多行,又常用来写文档字符串
doc = """
这是一个多行字符串。
常用于函数/类的说明文档。
"""
print(doc)
常见坑
r 前缀字符串不能以反斜杠结尾:r"C:\path\" 是语法错误(末尾的 \" 会转义引号)。需要以反斜杠结尾时用 r"C:\path" + "\\" 或 "C:\\path\\"。要点记忆口诀:涉及「路径」和「正则」就加 r 前缀。这两个场景里反斜杠有特殊含义,绝不能被 Python 先翻译一遍。
编码:Unicode、UTF-8 与 bytes
这一节是中文开发者的必修课。Python 3 之后,字符串默认就是 Unicode,但和「字节」是两回事。
| 类型 | 本质 | 示例 |
|---|---|---|
str | Unicode 字符,Python 内部统一用它 | "你好" |
bytes | 原始字节串,存网络/文件数据 | b"\xe4\xbd\xa0" |
| 编码 | str → bytes | "你".encode("utf-8") |
| 解码 | bytes → str | b"\xe4\xbd\xa0".decode("utf-8") |
编码与解码
s = "你好"
# str -> bytes:编码
b = s.encode("utf-8")
print(b) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(type(b)) # <class 'bytes'>
print(len(s)) # 2 两个字符
print(len(b)) # 6 六个字节(每个汉字 3 字节)
# bytes -> str:解码
back = b.decode("utf-8")
print(back) # 你好
print(back == s) # True
# 其他编码
print("你好".encode("gbk")) # b'\xc4\xe3\xba\xc3' 汉字 2 字节
print("你好".encode("utf-16")) # 汉字 2 字节,含 BOM
# ASCII 和数字
print(ord("A")) # 65 字符 -> 码点
print(chr(65)) # 'A' 码点 -> 字符
print(ord("你")) # 20320
print(chr(20320)) # 你
# bytes 与数字
print(b"ABC"[0]) # 65 取到的是字节的整数值
print(list(b"ABC")) # [65, 66, 67]
常见坑Windows 中文系统默认编码可能是
gbk,Linux/macOS 通常是 utf-8。用 open() 读写文本时若不指定 encoding,跨机器就可能出现 UnicodeDecodeError。第 8 章会详细讲文件读写,这里先记住:永远显式指定 encoding="utf-8"。实际读写文件时的编码
# 写入 UTF-8 文本(推荐)
with open("data.txt", "w", encoding="utf-8") as f:
f.write("你好,世界")
# 读取
with open("data.txt", "r", encoding="utf-8") as f:
print(f.read()) # 你,世界
# 写 CSV / JSON 也要指定编码
with open("out.json", "w", encoding="utf-8") as f:
f.write('{"name": "小明"}')
# 如果拿到的是 bytes,想显示中文用 decode
data = b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(data.decode("utf-8")) # 你好
要点Python 3 的字符串默认就是 Unicode,源码文件默认 UTF-8,所以中文可以直接写在代码里。只有在和外部系统(文件、网络、数据库)交换时,才需要显式编码解码。
字符串的常用操作
7.1 遍历与分割
字符串的遍历与切割
s = "Python"
# 遍历字符
for ch in s:
print(ch, end=" ") # P y t h o n
print()
# 带下标遍历
for i, ch in enumerate(s):
print(f"{i}:{ch}", end=" ")
print()
# 按字符切割
print(list(s)) # ['P','y','t','h','o','n']
print(tuple(s)) # 变成元组
# 按分隔符切割
csv = "a,b,c,d"
parts = csv.split(",")
print(parts) # ['a','b','c','d']
# 按行切割(多行文本)
text = "第一行\n第二行\n第三行"
lines = text.splitlines()
print(lines) # ['第一行','第二行','第三行'] 不含换行符
# 注意 split("\n") 和 splitlines() 的区别
print(text.split("\n")) # 同上,但会保留空行信息
print("a\n\nb".splitlines()) # ['a','','b'] 空行也算一行
print("a\n\nb".split("\n")) # ['a','','b'] 结果相同
7.2 字符串的连接与性能
连接大量字符串的正确方式
# 错误:循环里用 += 拼接,效率低
result = ""
for i in range(10000):
result += str(i) # 每次都创建一个新字符串
# 推荐:用 join
result = "".join(str(i) for i in range(10000))
# 或用 f-string 构造单个
result = "".join(f"{i}," for i in range(5))
# "0,1,2,3,4,"
# 少量拼接用 + 或 f-string 没问题
a, b = "hello", "world"
print(a + " " + b)
print(f"{a} {b}")
# += 对不可变字符串的优化:CPython 有局部优化,影响不大
技巧把
"".join(生成器) 记成「用空串把所有元素粘起来」。这个模式在处理大量文本拼接时能快几十倍,是必须掌握的优化点。7.3 字符串与列表互转
split 和 join 的配合
# 列表 -> 字符串
words = ["Python", "is", "awesome"]
sentence = " ".join(words)
print(sentence) # Python is awesome
# 字符串 -> 列表
words = sentence.split(" ")
print(words) # ['Python','is','awesome']
# 链式操作:反转句子里的词序
reversed_sentence = " ".join(sentence.split(" ")[::-1])
print(reversed_sentence) # awesome is Python
# 字符频率统计(把字符串当序列处理)
from collections import Counter
print(Counter("mississippi").most_common(2))
# [('i', 4), ('s', 4)]
实战:文本处理小工具
综合运用本章知识,写一个文本分析工具集。
text_toolkit.py
"""一组实用的字符串处理函数,全部可直接调用。"""
import re
def word_frequency(text: str) -> dict:
"""统计每个单词出现的次数(忽略大小写和标点)。"""
cleaned = re.sub(r"[^\w\s]", " ", text.lower()) # 非字母数字 -> 空格
words = [w for w in cleaned.split() if w]
result = {}
for w in words:
result[w] = result.get(w, 0) + 1
return result
def title_case(text: str) -> str:
"""把每个单词首字母大写(不是 str.title 的粗暴版)。"""
return " ".join(w[:1].upper() + w[1:].lower() for w in text.split())
def truncate(text: str, length: int = 20, suffix: str = "...") -> str:
"""超长则截断并加省略号。"""
return text if len(text) <= length else text[:length - len(suffix)] + suffix
def mask_phone(phone: str) -> str:
"""手机号脱敏:13812345678 -> 138****5678"""
digits = "".join(c for c in phone if c.isdigit())
return f"{digits[:3]}{'*' * 4}{digits[-4:]}" if len(digits) == 11 else phone
def snake_case(text: str) -> str:
"""驼峰转下划线:myVarName -> my_var_name"""
result = []
for i, ch in enumerate(text):
if ch.isupper() and i > 0:
result.append("_")
result.append(ch.lower())
return "".join(result)
def parse_tags(text: str) -> list:
"""从 HTML 片段中提取所有 <标签名>,去重后按出现顺序返回。"""
tags = re.findall(r"<(\w+)>", text)
return list(dict.fromkeys(tags))
# ---- 测试 ----
sample = "Python is great. Python is fun, and Python is powerful."
freq = word_frequency(sample)
print("词频 top3:", sorted(freq.items(), key=lambda kv: -kv[1])[:3])
# 词频 top3: [('python', 3), ('is', 3), ('great', 1)]
print(title_case("hELLO wORLD")) # Hello World
print(truncate("abcdefghij", 6)) # abc...
print(mask_phone("138-1234-5678")) # 138****5678
print(snake_case("myVarName")) # my_var_name
print(parse_tags("<p><span>text</span></p>")) # ['p', 'span']
技巧
dict.fromkeys(tags) 是「去重且保持原顺序」的经典一行写法:字典保留插入顺序,所以重复的标签只会保留第一次出现的位置。正则表达式入门(字符串处理的瑞士军刀)
import re
text = "订单号 A2024-001,金额 199.50 元,手机 13812345678"
# 查找所有数字
print(re.findall(r"\d+", text)) # ['2024', '001', '199', '50', '13812345678']
# 提取带格式的编号
print(re.findall(r"[A-Z]\d{4}-\d{3}", text)) # ['A2024-001']
# 匹配手机号(简化版)
phone = re.search(r"1[3-9]\d{9}", text)
print(phone.group() if phone else None) # 13812345678
# 提取金额
amount = re.search(r"(\d+)\.(\d{2})", text)
if amount:
print(float(amount.group())) # 199.5
# 替换
print(re.sub(r"\d+", "*", text)) # 把所有数字变成 *
# 分割
print(re.split(r"[,,]", text)) # 按中英文逗号切
# 常用元字符:
# . 任意字符(除换行) \d 数字 \D 非数字
# \w 字母数字下划线 \s 空白 \S 非空白
# ^ 行首 $ 行尾
# * 0次+ + 1次+ ? 0或1次 {m,n} m到n次
# ( ) 分组 [ ] 字符集 | 或
字符串的常见坑
| 坑 | 症状 | 解决 |
|---|---|---|
| 切片右边界越界 | 漏掉最后一个字符 | 牢记左闭右开 |
s[0] = "X" | TypeError | 字符串不可变,用切片或方法 |
join 用反 | AttributeError | "分隔符".join(列表) |
.format() 数错位置 | IndexError / 顺序错 | 改用 f-string 或命名占位符 |
| f-string 中用反斜杠 | SyntaxError | 提前算好变量再插入 |
| 混淆 str 与 bytes | TypeError | 外部数据先明确是文本还是字节 |
| 不指定文件编码 | UnicodeDecodeError | 永远 encoding="utf-8" |
循环里 += 拼接 | 大列表下极慢 | 用 "".join(...) |
r 串以 \ 结尾 | SyntaxError | 转义写成 "\\\\" |
| 字符串当数字比较 | 结果反直觉 | 先 int() 再比 |
把坑写成断言,逐条自测
# 切片是左闭右开
assert "hello"[1:3] == "el"
assert "hello"[-2:] == "lo"
# 字符串不可变,「修改」产生新对象
s = "hello"
assert s.upper() == "HELLO"
assert s == "hello" # 原串未变
# join 的方向
assert "-".join(["a", "b"]) == "a-b"
# find 返回 -1,index 报错
assert "abc".find("z") == -1
try:
"abc".index("z")
except ValueError:
pass # 预期内
# 格式化
assert f"{3.14159:.2f}" == "3.14"
assert f"{1234567:,}" == "1,234,567"
# 编码解码可逆
assert "你好".encode("utf-8").decode("utf-8") == "你好"
# 中文长度与字节长度不同
assert len("你好") == 2
assert len("你好".encode("utf-8")) == 6
print("字符串自测全部通过!")
本章小结
- 字符串是「不可变的 Unicode 序列」:不可改、有序、可切片、可哈希。
- 索引从 0 开始,
[-1]是末字符,len(s)-1是最后一个合法下标。 - 切片
s[start:stop:step]左闭右开;越界返回空串而不报错;[::-1]是反转。 - 不可变的后果:所有修改方法返回新串;改单个字符要
s[:i] + c + s[i+1:]。 - 查询类:
count/find/index/in/startswith/endswith/isdigit/isalpha。 - 修改类:
replace(带次数)/split/join/strip/upper/lower/capitalize。 join的方向是「分隔符.join(列表)」,调用者是分隔符。- f-string 是首选格式化:
f"{expr:.2f}"可写任意表达式;{x=}是调试利器。 - 格式说明符:
.2f精度、,千位、%百分比、>/</^对齐、#x/#o/#b进制。 - 转义与原始字符串:
\n \t \\ \" \u4f60;路径和正则一律加r前缀。 - 编码:str ↔ bytes 用
encode/decode;文件读写永远显式encoding="utf-8"。 - 中文注意:
len("你好") == 2但 UTF-8 编码后是 6 字节;ord/chr查码点。 - splitlines() 处理多行文本最方便,比
split("\n")更贴合语义。 - 大量拼接用 join,别在循环里
+=。 - 正则
re是提取/校验/替换复杂文本模式的标准工具,先掌握findall/search/sub/split/match。
练习题
- 反转字符串的四种方法:分别用切片
[::-1]、join(reversed())、reversed()迭代、以及「取首字符+递归」实现字符串反转,比较可读性与性能差异。 - 实现字符串工具函数:写三个函数并测试:①
is_palindrome(s)判断回文(忽略大小写和非字母字符);②longest_word(s)返回最长单词;③char_histogram(s)返回字符计数字典。要求不使用re模块。 - 验证回文数:判断输入是否为回文数(正整数),例如
121是、1231不是。要求不把数字转成字符串(用数学方法取首尾位)。 - 模板引擎:实现一个简易模板替换器,把
"Hello {name}, you are {age}"中的占位符替换为给定字典的值。未提供的占位符保留原文。要求用 f-string 或.format都不能直接做,请自行实现。 - CSV 行解析:给定一行 CSV 文本
'name,age,"Beijing, China",score'(字段可能含逗号,但用双引号包裹),解析成字典列表。需要正确处理引号内的逗号。 - 密码强度检测器:写一个函数判断密码强度并返回建议。要求包含:长度 ≥8、含大小写字母、含数字、含特殊字符,按缺失项给出具体改进建议。
re和纯 Python 两种方式各写一遍。 - 格式化对齐的报表:给定一组销售数据(商品名、数量、单价),用 f-string 输出一张右对齐的表格,包含合计行,金额保留 2 位小数并加千位分隔符。列宽固定,不足补空格。
- 编码转换实战:写一个函数
detect_and_convert(data: bytes) -> str,尝试按 utf-8、gbk、latin-1 的顺序解码,返回第一个成功的 str 和用到的编码名。都失败则返回None。再用它处理b'\xe4\xbd\xa0\xe5\xa5\xbd'和b'\xc4\xe3\xba\xc3'。 - 正则提取与清洗:给定一段含 HTML 的文本,① 提取所有标签名;② 去除所有标签只留文字;③ 提取所有网址;④ 把连续空白压缩成单个空格。要求用
re实现。 - 综合:日志分析器:读入一段多行日志文本,每行形如
"2024-01-15 10:30:00 INFO 用户登录成功"。统计各级别(INFO/WARN/ERROR)条数,找出最早和最晚的时间,提取所有用户 ID(形如user_123),并输出一份摘要。要求综合使用splitlines/split/f-string/字典/re。
参考答案要点
s = "hello world"
print(s[::-1]) # 方法1:切片(最快)
print("".join(reversed(s))) # 方法2:join + reversed
print("".join(s)) # 方法3:直接 join(逐字符)
# 方法4 递归:def rev(s): return s[-1] + rev(s[:-1]) if s else ""
# 递归版最直观但性能最差,深度受限于字符串长度(可能栈溢出)。
参考答案要点
def is_palindrome(s):
t = [c.lower() for c in s if c.isalnum()]
return t == t[::-1]
def longest_word(s):
return max(s.split(), key=len)
def char_histogram(s):
d = {}
for c in s:
d[c] = d.get(c, 0) + 1
return d
参考答案要点
def is_palindrome_num(n: int) -> bool:
if n < 0:
return False
if n % 10 == 0 and n != 0:
return False # 末位为 0 且非 0 的数不可能是回文
rev = 0
while n > rev:
rev = rev * 10 + n % 10
n //= 10
return n == rev or n == rev // 10
参考答案要点
import re
def render(template: str, ctx: dict) -> str:
# 用正则找到所有 {key},逐一替换
return re.sub(r"\{(\w+)\}",
lambda m: str(ctx.get(m.group(1), m.group(0))),
template)
print(render("Hello {name}, you are {age}", {"name": "小明", "age": 18}))
# Hello 小明, you are 18
print(render("Hello {name}, {missing}", {"name": "小红"}))
# Hello 小红, {missing} ← 缺失的保留原文
参考答案要点标准库
csv 模块是最优解:
import csv, io
row = next(csv.reader(io.StringIO('name,age,"Beijing, China",score')))
# ['name', 'age', 'Beijing, China', 'score']
print(dict(zip(["name","age","city","score"], row)))
手写解析需要状态机跟踪「是否在引号内」,代码量大且容易出错,这正是用标准库的价值。
参考答案要点
import re
def check(pwd: str) -> list:
tips = []
if len(pwd) < 8:
tips.append("长度至少 8 位")
if not re.search(r"[a-z]", pwd):
tips.append("需要小写字母")
if not re.search(r"[A-Z]", pwd):
tips.append("需要大写字母")
if not re.search(r"\d", pwd):
tips.append("需要数字")
if not re.search(r"[^\w\s]", pwd):
tips.append("需要特殊字符")
return tips or ["密码强度合格"]
参考答案要点
rows = [("键盘", 15, 89.9), ("鼠标", 30, 45.5), ("显示器", 8, 1299.0)]
print(f"{'商品':<8}{'数量':>6}{'单价':>10}{'小计':>12}")
print("-" * 36)
total = 0
for name, qty, price in rows:
sub = qty * price
total += sub
print(f"{name:<8}{qty:>6}{price:>10.2f}{sub:>12,.2f}")
print("-" * 36)
print(f"{'合计':<8}{'':>6}{'':>10}{total:>12,.2f}")
参考答案要点
def detect_and_convert(data: bytes):
for enc in ("utf-8", "gbk", "latin-1"):
try:
return data.decode(enc), enc
except UnicodeDecodeError:
continue
return None, None
print(detect_and_convert(b'\xe4\xbd\xa0\xe5\xa5\xbd'))
# ('你好', 'utf-8')
print(detect_and_convert(b'\xc4\xe3\xba\xc3'))
# ('你好', 'gbk')
注意:latin-1 几乎不会解码失败(0x00–0xFF 全部有效),所以实际上它永远兜底成功——真实场景应把它放最后并说明其「永不失败」的特性。
参考答案要点
import re
html = '<div class="a">访问 <a href="https://example.com">示例</a> 站点</div>'
tags = re.findall(r"<(\w+)", html) # ['div','a','a']
text = re.sub(r"<[^>]+>", "", html) # 去除标签
urls = re.findall(r"https?://[^\s\"'>]+", html) # 提取网址
clean = re.sub(r"\s+", " ", text).strip() # 压缩空白
参考答案要点
import re
from collections import Counter
text = """2024-01-15 10:30:00 INFO 用户 user_1 登录成功
2024-01-15 11:00:00 WARN 磁盘使用率 85%
2024-01-15 12:15:00 ERROR 用户 user_2 登录失败"""
lines = text.splitlines()
levels = Counter(line.split()[2] for line in lines)
print("级别统计:", dict(levels))
print("最早:", min(l[:19] for l in lines))
print("最晚:", max(l[:19] for l in lines))
print("用户:", re.findall(r"user_\d+", text))
print(f"共 {len(lines)} 条日志,错误 {levels['ERROR']} 条")