本章

基础篇 / 第 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
\u4f60Unicode 转义你

转义字符实战

# 换行与制表
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,但和「字节」是两回事。

类型本质示例
strUnicode 字符,Python 内部统一用它"你好"
bytes原始字节串,存网络/文件数据b"\xe4\xbd\xa0"
编码str → bytes"你".encode("utf-8")
解码bytes → strb"\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 与 bytesTypeError外部数据先明确是文本还是字节
不指定文件编码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. 反转字符串的四种方法:分别用切片 [::-1]、join(reversed())、reversed() 迭代、以及「取首字符 + 递归」实现字符串反转,比较可读性与性能差异。
  2. 参考答案要点
    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 ""
    # 递归版最直观但性能最差,深度受限于字符串长度(可能栈溢出)。
  3. 实现字符串工具函数:写三个函数并测试:① is_palindrome(s) 判断回文(忽略大小写和非字母字符);② longest_word(s) 返回最长单词;③ char_histogram(s) 返回字符计数字典。要求不使用 re 模块。
  4. 参考答案要点
    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
  5. 验证回文数:判断输入是否为回文数(正整数),例如 121 是、1231 不是。要求不把数字转成字符串(用数学方法取首尾位)。
  6. 参考答案要点
    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
  7. 模板引擎:实现一个简易模板替换器,把 "Hello {name}, you are {age}" 中的占位符替换为给定字典的值。未提供的占位符保留原文。要求用 f-string 或 .format 都不能直接做,请自行实现。
  8. 参考答案要点
    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}   ← 缺失的保留原文
  9. CSV 行解析:给定一行 CSV 文本 'name,age,"Beijing, China",score'(字段可能含逗号,但用双引号包裹),解析成字典列表。需要正确处理引号内的逗号。
  10. 参考答案要点标准库 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)))
    手写解析需要状态机跟踪「是否在引号内」,代码量大且容易出错,这正是用标准库的价值。
  11. 密码强度检测器:写一个函数判断密码强度并返回建议。要求包含:长度 ≥8、含大小写字母、含数字、含特殊字符,按缺失项给出具体改进建议。re 和纯 Python 两种方式各写一遍。
  12. 参考答案要点
    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 ["密码强度合格"]
  13. 格式化对齐的报表:给定一组销售数据(商品名、数量、单价),用 f-string 输出一张右对齐的表格,包含合计行,金额保留 2 位小数并加千位分隔符。列宽固定,不足补空格。
  14. 参考答案要点
    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}")
  15. 编码转换实战:写一个函数 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'。
  16. 参考答案要点
    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 全部有效),所以实际上它永远兜底成功——真实场景应把它放最后并说明其「永不失败」的特性。
  17. 正则提取与清洗:给定一段含 HTML 的文本,① 提取所有标签名;② 去除所有标签只留文字;③ 提取所有网址;④ 把连续空白压缩成单个空格。要求用 re 实现。
  18. 参考答案要点
    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()                   # 压缩空白
  19. 综合:日志分析器:读入一段多行日志文本,每行形如 "2024-01-15 10:30:00 INFO 用户登录成功"。统计各级别(INFO/WARN/ERROR)条数,找出最早和最晚的时间,提取所有用户 ID(形如 user_123),并输出一份摘要。要求综合使用 splitlines/split/f-string/字典/re。
  20. 参考答案要点
    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']} 条")
Python 完全教程 · 第 4 章