JEPA4Japan · 教程

字符串与文本处理

1,602字 5分钟阅读 #Python

在不忽视 Unicode 的前提下,对文本进行切片、搜索、规范化和格式化。

课程进度 课程大纲 已发布 24/24 课

文本是一排小方块

Python 字符串就是放在引号中的文本,例如 "Python"、"こんにちは" 或 "👍🏽"。想象一下,它的各个组成部分排成一行。Python 把这些部分称为 Unicode 码点。

  1. 获取文本 姓名、笔记或标签
  2. 清理文本 处理空白和大小写形式
  3. 搜索文本 找到有用的部分
  4. 生成新文本 原字符串保持不变
字符串让 Python 能够检查并重新构建文本。

Python 从 0 开始计算位置。len() 计算的是码点数量,而不是字节数,也不一定等于人眼所看到的符号数量。

word = "Python"

print(len(word))
print(word[0])
print(word[-1])
print(word[1:4])
6
P
n
yth

word[0] 是第一个方块,word[-1] 是最后一个。切片包含起始位置,但会在结束位置之前停止,因此 [1:4] 会取得位置 1、2 和 3。

索引、切片、拆分与连接

使用单个索引时,对应的位置必须存在。word[99] 会引发 IndexError。切片则更宽容:word[:99] 只会返回 word 的全部内容。空字符串没有第一个或最后一个位置,因此在使用索引之前要先检查它:

name = "".strip()

if name:
    print(name[0])
else:
    print("No first character")

split() 把文本拆成一个列表。join() 使用选定的分隔符,把多个字符串片段连接起来:

messy = "  read   code\tplay  "
pieces = messy.split()

print(pieces)
print(" ".join(pieces))
['read', 'code', 'play']
read code play
  1. 拆分 文本变成多个片段
  2. 列表 片段按顺序排列
  3. 连接 片段重新变成文本
" ".join(text.split()) 会合并意外出现的多余空白。

使用 split(",") 时,逗号就是确切的分隔符。两个紧挨着的逗号会产生一个空片段。另外,传给 join() 的每一项都必须是字符串。

清理方法会返回新字符串

字符串是不可变的:Python 不能替换现有字符串中的某一个方块。下面的代码会故意触发错误:

word = "python"
word[0] = "P"

它会引发 TypeError。应当改为构建一个新字符串:

word = "python"
title = "P" + word[1:]

print(word)
print(title)
python
Python

字符串方法也遵循同样的规则。strip() 会删除两端的空白,replace() 会替换完全匹配的片段,而 casefold() 会生成适合 Unicode 文本搜索的不区分大小写键。每个方法都会返回新文本。

raw = "  Python makes TEXT  "
clean = raw.strip().replace("makes", "cleans")
searchable = clean.casefold()

print(clean)
print("python" in searchable)
print(searchable.count("text"))
print(searchable.find("cleans"))
Python cleans TEXT
True
1
7

如果想问“它在里面吗?”,就使用 in;如果想问“有多少个互不重叠的匹配?”,就使用 count();如果想知道第一次出现的位置,就使用 find()(返回 -1 表示没有找到)。绝对不要使用 if text.find(term)::位于位置 0 的匹配会被当作假,而表示未找到的 -1 反而会被当作真。

一个图形可以使用多个码点

两个字符串看起来可能完全相同,却由不同的码点序列组成。Python 标准库中的 unicodedata 模块可以把规范等价的文本转换成相同的 Unicode 形式。

  1. 一个方块 一个预组合字母
  2. 两个方块 一个字母加一个重音符号
  3. NFC 让等价的序列能够匹配
外观相同并不一定意味着码点相同。
import unicodedata

one = "café"
two = "cafe\u0301"

print(one == two)
print(len(one), len(two))
print(one == unicodedata.normalize("NFC", two))
print(len("👍🏽"))
False
4 5
True
2

这个表情符号看起来像一个符号,但实际上使用了两个码点。基本索引操作可能会把它拆开。因此,字符串位置既不是字节位置,也不能可靠地表示“可见字符”的边界。

小项目:学习笔记清理器

创建 note_cleaner.py。这个完整程序只使用标准库。它会规范化 Unicode、合并空白,并统计一个非空关键词出现的次数,同时不会改变原始字符串。

import unicodedata


def search_key(text):
    return unicodedata.normalize("NFC", text).strip().casefold()


def clean_note(text):
    normalized = unicodedata.normalize("NFC", text)
    return " ".join(normalized.split())


raw_note = "  CAFÉ   practice, then cafe\u0301 review.  "
raw_keyword = " Café "

note = clean_note(raw_note)
keyword = search_key(raw_keyword)
matches = search_key(note).count(keyword)

print(f"Original: <{raw_note}>")
print(f"Clean: <{note}>")
print(f"Matches: {matches}")
print(f"Original unchanged: {raw_note.startswith('  ')}")

运行 python3 note_cleaner.py 并检查:

Original: <  CAFÉ   practice, then café review.  >
Clean: <CAFÉ practice, then café review.>
Matches: 2
Original unchanged: True

边界规则:关键词不能为空。text.count("") 会统计字符之间的空隙,并返回 len(text) + 1,这对于统计学习词汇没有实际意义。

三个小任务

  1. 安全提取首尾字符。 去除输入单词两端的空白。仅当它不为空时,才打印它的第一个和最后一个码点。
  2. 简短预览。 使用 text[:limit] 编写 preview(text, limit)。分别测试 0、3,以及一个大于文本长度的限制值。
  3. 搜索侦探。 查找一个位于位置 0 的词,以及一个不存在的词。正确使用 in,或者将 find() 的结果与 -1 比较。

做到这些,你就可以开始第 6 章了……

  • 你会使用 len()、正索引、负索引和切片;
  • 你能解释字符串是不可变的;
  • 你会使用 strip()、split()、join() 和 casefold() 清理文本;
  • 你能根据想解决的问题选择 in、count() 或 find();
  • 你会使用 unicodedata.normalize() 规范化等价的 Unicode 文本;
  • 你记得码点、可见符号和字节是不同的东西;
  • 你能运行学习笔记清理器,并让四项输出检查全部匹配。

接下来,你会把 split() 生成的片段放进列表,然后比较可修改的列表和固定的元组。