【Python Cookbook】字符串和文本(二)

字符串和文本(二)

6.字符串忽略大小写的搜索替换

你需要以忽略大小写的方式搜索与替换文本字符串。

为了在文本操作时忽略大小写,你需要在使用 re 模块的时候给这些操作提供 re.IGNORECASE 标志参数。比如:

python 复制代码
>>> text = 'UPPER PYTHON, lower python, Mixed Python'
>>> re.findall('python', text, flags=re.IGNORECASE)
['PYTHON', 'python', 'Python']
>>> re.sub('python', 'snake', text, flags=re.IGNORECASE)
'UPPER snake, lower snake, Mixed snake'

最后的那个例子揭示了一个小缺陷,替换字符串并不会自动跟被匹配字符串的大小写保持一致。为了修复这个,你可能需要一个辅助函数,就像下面的这样:

python 复制代码
def matchcase(word):
    def replace(m):
        text = m.group()
        if text.isupper():
            return word.upper()
        elif text.islower():
            return word.lower()
        elif text[0].isupper():
            return word.capitalize()
        else:
            return word
    return replace
  • matchcase(word) 函数:
    • 这是一个高阶函数,它返回另一个函数 replace(m)
    • word 参数是要替换的目标词(这里是 'snake')。
  • replace(m) 函数:
    • m 是一个正则匹配对象。
    • m.group() 返回匹配到的完整文本(例如 'Python', 'PYTHON', 'python' 等)。
    • 这个函数检查匹配文本的大小写格式,并返回 word 的相应大小写形式:
      • 如果匹配文本是全大写(如 'PYTHON'),返回 word.upper()(即 'SNAKE')。
      • 如果匹配文本是全小写(如 'python'),返回 word.lower()(即 'snake')。
      • 如果匹配文本是首字母大写(如 'Python'),返回 word.capitalize()(即 'Snake')。
      • 否则,直接返回 word

下面是使用上述函数的方法:

python 复制代码
>>> re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
'UPPER SNAKE, lower snake, Mixed Snake'
  • matchcase('snake') 先执行,返回 replace 函数,此时 word = 'snake'
  • re.sub 遍历 text,找到所有匹配 'python'(不区分大小写)的子串。
  • 对每个匹配项,调用 replace(m),并根据匹配文本的大小写格式返回 'snake' 的对应形式。
  • re.sub 把所有匹配项替换成 replace 返回的结果。

🚀 matchcase('snake') 返回了一个回调函数(参数必须是 match 对象,前面一节提到过,sub() 函数除了接受替换字符串外,还能接受一个回调函数。

对于一般的忽略大小写的匹配操作,简单的传递一个 re.IGNORECASE 标志参数就已经足够了。但是需要注意的是,这个对于某些需要大小写转换的 Unicode 匹配可能还不够。

7.最短匹配模式

你正在试着用正则表达式匹配某个文本模式,但是它找到的是模式的最长可能匹配。而你想修改它变成查找最短的可能匹配。

这个问题一般出现在需要匹配一对分隔符之间的文本的时候(比如引号包含的字符串)。为了说明清楚,考虑如下的例子:

python 复制代码
>>> str_pat = re.compile(r'"(.*)"')
>>> text1 = 'Computer says "no."'
>>> str_pat.findall(text1)
['no.']
>>> text2 = 'Computer says "no." Phone says "yes."'
>>> str_pat.findall(text2)
['no." Phone says "yes.']

在这个例子中,模式 r'\"(.*)\"' 的意图是 匹配被双引号包含的文本 。 但是在正则表达式中 * 操作符是贪婪的,因此匹配操作会查找最长的可能匹配。于是在第二个例子中搜索 text2 的时候返回结果并不是我们想要的。

为了修正这个问题,可以在模式中的 * 操作符后面加上 ? 修饰符,就像这样:

python 复制代码
>>> str_pat = re.compile(r'"(.*?)"')
>>> str_pat.findall(text2)
['no.', 'yes.']

这样就使得匹配变成非贪婪模式,从而得到最短的匹配,也就是我们想要的结果。

这一节展示了在写包含点 . 字符的正则表达式的时候遇到的一些常见问题。在一个模式字符串中,点 . 匹配除了换行外的任何字符。然而,如果你将点 . 号放在开始与结束符(比如引号)之间的时候,那么匹配操作会查找符合模式的最长可能匹配。这样通常会导致很多中间的被开始与结束符包含的文本被忽略掉,并最终被包含在匹配结果字符串中返回。通过在 * 或者 + 这样的操作符后面添加一个 ? 可以强制匹配算法改成寻找最短的可能匹配。

8.多行匹配模式

你正在试着使用正则表达式去匹配一大块的文本,而你需要跨越多行去匹配。

这个问题很典型的出现在当你用点 . 去匹配任意字符的时候,忘记了点 . 不能匹配换行符的事实。比如,假设你想试着去匹配 C 语言分割的注释(即 /* ... */ 之间的内容):

python 复制代码
>>> comment = re.compile(r'/\*(.*?)\*/')
>>> text1 = '/* this is a comment */'
>>> text2 = '''/* this is a
... multiline comment */
... '''
>>>
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]
  • /\* → 匹配注释的开始符号 /*
    • / 匹配字面量 /
    • \* 匹配字面量 ** 在正则中有特殊含义,所以需要转义 \*)。
  • (.*?) → 非贪婪匹配:
    • . 匹配任意字符(除换行符 \n,除非使用 re.DOTALL 标志)。
    • *? 表示 前一个字符 . 可以出现 0 0 0 次或多次,但尽可能少匹配(非贪婪模式)。
    • ( ) 表示捕获分组,方便提取注释内容。
  • \*/ → 匹配注释的结束符号 */
    • \* 匹配字面量 *
    • / 匹配字面量 /

为了修正这个问题,你可以修改模式字符串,增加对换行的支持。比如:

python 复制代码
>>> comment = re.compile(r'/\*((?:.|\n)*?)\*/')
>>> comment.findall(text2)
[' this is a\n multiline comment ']

在这个模式中, (?:.|\n) 指定了一个非捕获组(也就是它定义了一个仅仅用来做匹配,而不能通过单独捕获或者编号的组)。

  • (?:...) 是一个非捕获分组(仅用于分组,不捕获匹配内容)。
  • .|\n 匹配任意字符 . 或换行符 \n,确保可以跨行匹配。

re.compile() 函数接受一个标志参数叫 re.DOTALL,在这里非常有用。它可以让正则表达式中的点 . 匹配包括换行符在内的任意字符。比如:

python 复制代码
>>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a\n multiline comment ']

对于简单的情况使用 re.DOTALL 标记参数工作的很好,但是如果模式非常复杂或者是为了构造字符串令牌而将多个模式合并起来, 这时候使用这个标记参数就可能出现一些问题。如果让你选择的话,最好还是定义自己的正则表达式模式,这样它可以在不需要额外的标记参数下也能工作的很好。

9.将 Unicode 文本标准化

你正在处理 Unicode 字符串,需要确保所有字符串在底层有相同的表示。

在 Unicode 中,某些字符能够用多个合法的编码表示。为了说明,考虑下面的这个例子:

python 复制代码
>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> s1
'Spicy Jalapeño'
>>> s2
'Spicy Jalapeño'
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15

这里的文本 Spicy Jalapeño 使用了两种形式来表示。 第一种使用整体字符 ñU+00F1),第二种使用拉丁字母 n 后面跟一个 ~ 的组合字符(U+0303)。

在需要比较字符串的程序中使用字符的多种表示会产生问题。为了修正这个问题,你可以使用 unicodedata 模块先将文本标准化:

python 复制代码
>>> import unicodedata
>>> t1 = unicodedata.normalize('NFC', s1)
>>> t2 = unicodedata.normalize('NFC', s2)
>>> t1 == t2
True
>>> print(ascii(t1))
'Spicy Jalape\xf1o'
>>> t3 = unicodedata.normalize('NFD', s1)
>>> t4 = unicodedata.normalize('NFD', s2)
>>> t3 == t4
True
>>> print(ascii(t3))
'Spicy Jalapen\u0303o'

normalize() 第一个参数指定字符串标准化的方式。

  • NFC 表示字符应该是整体组成(比如可能的话就使用单一编码)。
  • NFD 表示字符应该分解为多个组合字符表示。

Python 同样支持扩展的标准化形式 NFKCNFKD,它们在处理某些字符的时候增加了额外的兼容特性。比如:

python 复制代码
>>> s = '\ufb01' # A single character
>>> s
'fi'
>>> unicodedata.normalize('NFD', s)
'fi'
# Notice how the combined letters are broken apart here
>>> unicodedata.normalize('NFKD', s)
'fi'
>>> unicodedata.normalize('NFKC', s)
'fi'

标准化对于任何需要以一致的方式处理 Unicode 文本的程序都是非常重要的。当处理来自用户输入的字符串而你很难去控制编码的时候尤其如此。

在清理和过滤文本的时候字符的标准化也是很重要的。比如,假设你想清除掉一些文本上面的变音符的时候(可能是为了搜索和匹配):

python 复制代码
>>> t1 = unicodedata.normalize('NFD', s1)
>>> ''.join(c for c in t1 if not unicodedata.combining(c))
'Spicy Jalapeno'

最后一个例子展示了 unicodedata 模块的另一个重要方面,也就是测试字符类的工具函数。combining() 函数可以测试一个字符是否为和音字符。 在这个模块中还有其他函数用于查找字符类别,测试是否为数字字符等等。

10.在正则式中使用 Unicode

你正在使用正则表达式处理文本,但是关注的是 Unicode 字符处理。

默认情况下 re 模块已经对一些 Unicode 字符类有了基本的支持。比如,\\d 已经匹配任意的 Unicode 数字字符了:

python 复制代码
>>> import re
>>> num = re.compile('\d+')
>>> # ASCII digits
>>> num.match('123')
<_sre.SRE_Match object at 0x1007d9ed0>
>>> # Arabic digits
>>> num.match('\u0661\u0662\u0663')
<_sre.SRE_Match object at 0x101234030>

如果你想在模式中包含指定的 Unicode 字符,你可以使用 Unicode 字符对应的转义序列(比如 \uFFF 或者 \UFFFFFFF)。比如,下面是一个匹配几个不同阿拉伯编码页面中所有字符的正则表达式:

python 复制代码
>>> arabic = re.compile('[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff]+')

当执行匹配和搜索操作的时候,最好是先标准化并且清理所有文本为标准化格式。但是同样也应该注意一些特殊情况,比如在忽略大小写匹配和大小写转换时的行为。

python 复制代码
>>> pat = re.compile('stra\u00dfe', re.IGNORECASE) # \u00df 是 'ß' 的 Unicode 编码
>>> s = 'straße' # 德语单词 "Straße"(街道)的小写形式
>>> pat.match(s) # Matches
<_sre.SRE_Match object at 0x10069d370>
>>> pat.match(s.upper()) # Doesn't match
>>> s.upper() # Case folds
'STRASSE'
  • re.IGNORECASEUnicode 字符
    • re.IGNORECASE 使正则表达式 不区分大小写 ,例如 A 可以匹配 a
    • ß 是一个特殊情况:
      • 它的 大写形式是 SS(根据德语正字法规则)。
      • 因此,'straße'.upper() 会变成 'STRASSE',而不仅仅是简单的单个字符大小写转换。
  • 为什么 pat.match(s) 成功,但 pat.match(s.upper()) 失败?
    • pat.match(s)
      • 正则表达式模式是 stra\u00dfe(即 straße),且 re.IGNORECASE 启用。
      • 直接匹配 s = 'straße' 时,完全一致(不区分大小写),所以匹配成功。
    • pat.match(s.upper())
      • s.upper() 返回 'STRASSE'(因为 ßSS)。
      • 但正则表达式模式是 straße,它的大写形式应该是 STRASSE,而模式本身并未包含 SS
      • 因此,正则引擎无法将 SSß 视为等效(尽管人类知道它们是同一字母的不同形式),导致匹配失败。
  • 根本原因:
    • re.IGNORECASE 在 Python 的 re 模块中默认不处理 Unicode 的特殊大小写映射(如 ßSS)。
    • 如果要正确处理 Unicode 大小写折叠(case folding),需要使用 re.UNICODE 标志(但在 Python 中,re.IGNORECASE 已隐含 Unicode 支持,但对 ß 仍无效)。
    • 更彻底的解决方案是使用 casefold() 方法。

混合使用 Unicode 和正则表达式通常会让你抓狂。如果你真的打算这样做的话,最好考虑下安装第三方正则式库,它们会为 Unicode 的大小写转换和其他大量有趣特性提供全面的支持,包括模糊匹配。

相关推荐
代码AC不AC1 分钟前
【数据结构】栈 与【LeetCode】20.有效的括号详解
数据结构·学习·leetcode·练习·
Phoebe鑫1 分钟前
数据结构每日一题day5(顺序表)★★★★★
数据结构·算法
Vitalia2 分钟前
并查集(Union-Find)数据结构详解
数据结构·并查集
风笑谷11 分钟前
视频字幕python自动提取
python·音视频·字幕翻译·字幕提取·配音
<但凡.43 分钟前
C++修炼:string类的使用
开发语言·c++·算法
你觉得2051 小时前
山东大学:《DeepSeek应用与部署》|附PPT下载方法
大数据·人工智能·python·机器学习·ai·aigc·内容运营
HR Zhou1 小时前
群体智能优化算法-大猩猩部落优化算法(Gorilla Troops Optimizer, GTO,含Matlab源代码)
算法·机器学习·数学建模·matlab·群体智能优化
老马啸西风1 小时前
Neo4j GDS-06-neo4j GDS 库中社区检测算法介绍
网络·算法·云原生·中间件·neo4j
go54631584651 小时前
使用Python和PyTorch库实现基于DNN、CNN、LSTM的极化码译码器模型的代码示例
pytorch·python·dnn
地平线开发者2 小时前
精度调优|conv+depth2space 替换 resize 指导
算法·自动驾驶