03 · 字符串(String)

📅 预计 60 分钟 | ⭐ = 高频考点
✍️ 本讲配套练习:本地 python grader.py 03,或网页练习


3.1 字符串基础

把字符串想象成一串糖葫芦:一颗一颗的字符,被一根签子串起来。签子上第一颗是 0 号,第二颗是 1 号……这个"第几号"就是索引(index)。Python 里造字符串有几种姿势,单引号 'abc'、双引号 "abc" 都行,三引号还能换行:

s1 = "Hello"        # 双引号
s2 = '你好'          # 单引号,中文没问题
s3 = """第一行
第二行"""            # 三引号:可以跨多行
print(s3)
# 输出:
# 第一行
# 第二行

不可变(immutable) ⭐ 是字符串最重要的脾气:一旦创建,糖葫芦上的糖就不能抠下来换一颗。所有看起来是"改"的操作,其实都是造了一串新的给你:

s = "abc"
# s[0] = "x"      # ❌ TypeError: 'str' object does not support item assignment
s2 = s.upper()    # ✅ 返回新字符串 "ABC",s 本身还是 "abc"
print(s)          # abc
print(s2)         # ABC

为什么不可变反而好?因为不可变意味着安全:字符串在字典里做键、在多线程里共享,都不会被意外改坏,Python 内部还能复用内存,省下一大块空间。

⚠️ 常见错误

  • 试图 s[0] = "x" 原地修改 → 报 TypeError。正确姿势是造新的:s = "x" + s[1:]
  • 引号嵌套出错:'it's' 会语法错误,要写成 "it's" 或用转义 'it\'s'

3.2 索引与切片 ⭐

索引:正着数 + 倒着数

正索引从 0 开始;负索引从 -1 开始(-1 是最后一个字符)。两种数法互为镜像,谁也绕不开:

s = "Python"
#  正索引:  P(0)  y(1)  t(2)  h(3)  o(4)  n(5)
#  负索引:  P(-6) y(-5) t(-4) h(-3) o(-2) n(-1)
print(s[0])     # P     第一个字符
print(s[-1])    # n     最后一个字符 ⭐ 高频
print(s[-2])    # o     倒数第二个

切片:左闭右开

切片 s[start:end:step] 像切西瓜:从 start 下刀,切到 end 前停下——含 start,不含 end(左闭右开)end 省略就是切到尾,start 省略就是从头上切。step 是步长,-1 就是倒着走。

s = "Python"
print(s[0:3])     # Pyt   索引 0、1、2,不含 3
print(s[2:])      # thon  从 2 到末尾
print(s[:3])      # Pyt   从头到 3(不含 3)
print(s[::2])     # Pto   步长 2:取 0、2、4
print(s[::-1])    # nohtyP 反转整串 ⭐ 常用
print(s[4:2])     # ''    左边比右边大?空串,不报错

⭐ 切片越界不报错"abc"[1:10]"bc",顶多截到边界。但单个索引越界会 IndexError"abc"[5] 直接崩。

⚠️ 常见错误

  • 把左闭右开记成"两头都含":s[0:3] 取 3 个字符(P、y、t),不是 4 个。口诀:含头不含尾。
  • 混用正负索引切片:s[-3:0] 是空串,因为 -3 在 0 的左边,方向不对。要取倒数 3 个写 s[-3:]
  • 忘了切片越界安全、索引越界危险,两个行为完全不一样。

3.3 大小写与字符判断

大小写家族

方法 作用 示例
.upper() 全部转大写 "AbC".upper()"ABC"
.lower() 全部转小写 "AbC".lower()"abc"
.swapcase() 大小写互换 "AbC".swapcase()"aBc"
.title() 每个单词首字母大写 "hello world".title()"Hello World"
.capitalize() 整串首字母大写 "hello world".capitalize()"Hello world"
print("hello world".title())       # Hello World  按单词拆开再逐个首字母大写
print("hello world".capitalize())  # Hello world  只把整串第一个字母大写
print("AbC".swapcase())            # aBc

字符判断家族

这类方法返回 True / False,是"筛选字符"的得力干将:

print("abc".isalpha())    # True   纯字母
print("abc123".isalpha()) # False  混了数字就不是纯字母
print("123".isdigit())    # True   纯数字
print("12.5".isdigit())   # False  小数点不是数字
print("abc123".isalnum()) # True   字母或数字都算
print(" ".isspace())      # True   空白(空格/换行/制表)
print("  \n\t".isspace()) # True   全是空白
print("AbC".isupper())    # False  有大写但不是全大写
print("ABC".isupper())    # True

⭐ 高频组合套路:if ch.isdigit(): 逐个筛数字,if ch.isalpha(): 筛字母,if ch.isspace(): 跳空白。

⚠️ 常见错误

  • "AbC".isupper() 是 False——"全大写"不是"有大写"。
  • 空串 "".isdigit() 是 False,"".isalpha() 也是 False。空串"什么都是假的"。
  • 中文的坑:"你好".isalpha() 返回 True。isalpha 判断的是"字母",Unicode 里汉字算字母。要严格判断英文用 ch.isascii() and ch.isalpha()

3.4 查找、替换与去除空白

查找:find / rfind / index / count

find(sub) 从左边找,返回第一次出现的位置,找不到返回 -1rfind(sub) 从右边找,返回最后一次出现的位置。index(sub) 跟 find 一样找位置,但找不到抛 ValueErrorcount(sub) 数子串出现次数。

s = "abracadabra"
print(s.find("a"))     # 0    第一次出现 a 的位置
print(s.rfind("a"))    # 10   最后一次出现 a 的位置
print(s.find("z"))     # -1   找不到返回 -1,不报错
print(s.index("a"))    # 0    和 find 一样
# print(s.index("z"))  # ❌ ValueError: substring not found
print(s.count("a"))    # 5    a 出现了 5 次

什么时候用 find、什么时候用 index?——你想自己处理"找不到"就用 find(不会崩);你觉得"这里必须存在,不存在就是 bug",就用 index 早点把问题炸出来。

替换:replace

replace(old, new, [count]) 把 old 全部换成 new;第三个参数 count 限制最多换前几个。

print("hello world hello".replace("hello", "hi"))
# hi world hi          全部替换
print("aaaa".replace("a", "b", 2))
# bbaa                 只换前 2 个

去除空白:strip / lstrip / rstrip

strip() 去掉两端空白;lstrip() 只去左边;rstrip() 只去右边。传一个字符串参数时,把两端属于这个字符串的字符全部剥掉:

s = "  \t你好 \n"
print(repr(s.strip()))    # '你好'
print(repr(s.lstrip()))   # '你好 \n'   右边还留着
print(repr(s.rstrip()))   # '  \t你好'

print("---hello---".strip("-"))   # hello   两端所有 - 都被剥掉
print("aaahelloaaa".strip("a"))   # hello

前缀后缀:startswith / endswith

判断字符串以什么开头 / 结尾,支持传元组一次判断多个:

print("app.py".endswith(".py"))            # True
print("app.py".endswith((".py", ".txt")))  # True
print(".gitignore".startswith("."))        # True

⚠️ 常见错误

  • find 找不到返回 -1,index 找不到抛异常——选错会崩。
  • replace 不传第三参数替换全部,只想换第一个要 replace(old, new, 1)
  • strip() 只去两端不去中间:"a b ".strip()"a b",中间空格动不了。
  • strip() 传参是按"字符集合"剥,不是按"子串"剥:"ababxabab".strip("ab")"x",会把两端所有 a 和 b 全剥掉。

3.5 拆分与合并(split / join)⭐

split(sep) 按分隔符把字符串拆成列表。不传参数时按"任意空白"拆,连续多个空格/换行/制表都会被当作一个分隔符。join 是它的逆运算:把列表用分隔符串起来。「拆开 → 处理 → 拼回」是数据处理最常用的套路。

s = "a,b,c"
print(s.split(","))                # ['a', 'b', 'c']   按逗号拆
print("2026-08-02".split("-"))     # ['2026', '08', '02']

t = "  python   is  great "
print(t.split())                   # ['python', 'is', 'great']  按空白拆,自动合并连续空白
print(t.split(" "))                # ['', '', 'python', '', '', 'is', '', 'great', '']  手写空格拆会留下空串!

print("-".join(["a", "b", "c"]))   # a-b-c  列表在前,分隔符在括号里
print("".join(["h", "i"]))         # hi

为什么手写 split(" ") 会留空串?因为它严格按"一个空格"切,两个空格中间就切出一个空串。而 split()(不带参数)会智能地把连续空白当成整体。

⚠️ 常见错误

  • "a,b,c".split() 不传参数 → ['a,b,c'] 一整块。要按逗号拆必须写 split(",")
  • join 的方向写反:正确的是 ",".join(list),不是 list.join(",")
  • split()split(" ") 结果可能天差地别(一个合并空白、一个留空串),别混用。

3.6 格式化输出(f-string 是主角)⭐

Python 有 3 种格式化:老式 %.format()、f-string。f-string 最推荐:字符串前加 f,用 {} 直接塞变量和表达式,读起来最像人话。

name = "小明"
score = 88.5
print(f"{name} 考了 {score} 分")    # 小明 考了 88.5 分
print(f"{score:.1f} 分")            # 88.5 分  保留 1 位小数

高级格式(重点考点)

x = 1234567.891
print(f"{x:,.2f}")     # 1,234,567.89  千分位 + 两位小数 ⭐
print(f"{x:.0f}")      # 1234568       四舍五入到整数

price = 99
print(f"{price:05d}")  # 00099         宽度 5,不足补 0

s = "hi"
print(f"[{s:<6}]")     # [hi    ]  左对齐,占 6 位
print(f"[{s:>6}]")     # [    hi]  右对齐 ⭐
print(f"[{s:^6}]")     # [  hi  ]  居中

w = 10
print(f"[{s:>{w}}]")   # [        hi]  宽度可以用变量 ⭐ 动态对齐

对齐、补零、千分位是格式化三大高频考点,> < ^ 三个符号和 , 千分位要记牢。

⚠️ 常见错误

  • 忘写 f 前缀:"{name}" 会原样输出 {name} 这几个字,不替换。
  • :.2f四舍五入不是截断:{2.345:.2f} 一般得 2.35(浮点误差偶尔让你看到 2.34,不必纠结)。
  • 对齐宽度是"总宽度"不是"补几个空格":f"{'hi':>6}" 是补 4 个空格(总长 6)。
  • 中文对齐会"看着没对齐":中文字符显示更宽,f"{'你好':<6}" 视觉上比英文窄一截,别被误导。

3.7 字符编码:ord 与 chr

计算机只认识数字,所以每个字符都对应一个整数码点(code point)。ord(字符) 查码点,chr(数字) 反查字符。英文 26 个字母的码点是连续的,这是很多算法的突破口:

print(ord("A"))    # 65
print(ord("a"))    # 97
print(ord("中"))   # 20013
print(chr(65))     # 'A'
print(chr(97))     # 'a'
print(chr(20013))  # '中'

字母序号换算:ord(ch) - ord("A") 得到 0~25 的位置。在这个位置基础上加 1 再 chr 回来,就是"下一个字母"。

经典应用:凯撒加密

把每个字母往后挪 shift 位,挪到末尾就绕回开头。绕回靠取模 % 26

def caesar(s, shift):
    out = []
    for ch in s:
        if ch.islower():
            base = ord("a")
            out.append(chr((ord(ch) - base + shift) % 26 + base))
        elif ch.isupper():
            base = ord("A")
            out.append(chr((ord(ch) - base + shift) % 26 + base))
        else:
            out.append(ch)   # 非字母原样保留
    return "".join(out)

print(caesar("xyz", 3))    # abc   x→a、y→b、z→c,绕回
print(caesar("Hello", 1))  # Ifmmp 大小写都保留

⚠️ 常见错误

  • 取模前忘了减 base:(ord(ch) + shift) % 26 得到 0~25,直接当字符用会乱码。必须是 (ord(ch) - base + shift) % 26 + base
  • 大写 base 是 65、小写 base 是 97,混着算会错位('A' 挪 1 会跑进小写区)。
  • 忘了处理非字母字符,空格和标点也被"加密",结果对不上。

3.8 遍历与综合套路

字符串可以直接 for ch in s 逐字符遍历,不需要索引。四大高频套路:

① 计数:遍历 + 字符判断方法累加。
② 回文判断s == s[::-1],正读反读一样就是回文。
③ 分词统计s.split() 拆词,len(words) 数词数。
④ 提取连续片段:遍历时用一个"临时变量"累积,遇到不匹配的字符就"结算"一次。易错点:遍历结束后还要再结算一次,否则最后一个片段会被丢掉。

# ④ 提取所有连续数字子串
def extract_numbers(s):
    result, cur = [], ""
    for ch in s:
        if ch.isdigit():
            cur += ch        # 数字就累积
        else:
            if cur:          # 遇到非数字,结算当前片段
                result.append(cur)
                cur = ""
    if cur:                  # ⭐ 别忘了收尾结算
        result.append(cur)
    return result

print(extract_numbers("ab12cd345"))   # ['12', '345']
print(extract_numbers("123 456"))     # ['123', '456']

# ② 回文(忽略大小写更严谨)
s = "AbA"
print(s.lower() == s.lower()[::-1])   # True

⚠️ 常见错误

  • 提取连续片段时忘了收尾结算,最后一个数字/单词丢了——这是最经典的漏 bug。
  • 回文判断大小写不一致:"AbA" 直接 == 反转会得 False,先 lower() 再比。
  • 空串直接 for 循环一次都不进,累加变量保持初始值——初始值要合理(数字 0、空字符串、空列表)。

💡 记忆口诀

  • 切片:含头不含尾;步长为负倒着走。
  • split 与 join:一对逆运算,拆开再拼回;join 是"列表在前、分隔符在括号里"。
  • 字符串:改就是造新的(不可变),别想原地动它。
  • 格式化:f 开头,花括号塞变量> < ^ 对齐、0 补零、, 千分位。
  • 编码:ord 查码、chr 反查;字母移位记得 % 26 绕圈。

📌 双语术语表(本讲)

中文 English
拼接 concatenation
切片 slicing
索引 index
不可变 immutable
转义字符 escape character
子串 substring
反转 reverse
大小写 case(upper/lower/swapcase)
查找 find
替换 replace
去除空白 strip
拆分 split
合并 join
格式化 format / f-string
编码 encoding / code point
回文 palindrome

⭐ 本讲考点清单

  1. 切片 s[start:end:step] 左闭右开,负索引从尾;s[::-1] 反转
  2. split() / join() 互为逆运算;split() 按空白合并拆、split(" ") 会留空串
  3. find(找不到 -1)vs index(找不到抛异常);rfind 从右找
  4. replace(old, new, count) 全部 / 限次替换
  5. strip() / lstrip() / rstrip() 去首尾空白,可传字符集合
  6. startswith / endswith 支持元组多后缀判断
  7. 大小写家族:upper / lower / swapcase / title / capitalize
  8. 字符判断:isalpha / isdigit / isalnum / isspace / isupper / islower;中文 isalpha 为 True
  9. f-string:{x:,.2f} 千分位、{s:<10} / {s:>10} / {s:^10} 对齐、{x:05d} 补零、宽度用变量
  10. ord() / chr() 码点互转;凯撒加密用 % 26 绕回,大小写 base 不同
  11. 字符串不可变——"改"都返回新串
  12. 综合套路:回文、分词统计、提取连续数字(记得收尾结算)