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

目录
案例
目录
案例
字符串和文本(一)1.使用多个界定符分割字符串
2.字符串开头或结尾匹配
3.用 Shell 通配符匹配字符串
4.字符串匹配和搜索
5.字符串搜索和替换
字符串和文本(三)11.删除字符串中不需要的字符
12.审查清理文本字符串
13.字符串对齐
14.合并拼接字符串
15.字符串中插入变量
字符串和文本(二)6.字符串忽略大小写的搜索替换
7.最短匹配模式
8.多行匹配模式
9.将 Unicode 文本标准化
10.在正则式中使用 Unicode
字符串和文本(四)

字符串和文本(五)
(四)16.以指定列宽格式化字符串
(四)17.在字符串中处理 html 和 xml
(四)18.字符串令牌解析
(四)19.字节字符串上的字符串操作
(五)20.实现一个简单的递归下降分析器

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

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

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

>>> 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'

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

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

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

>>> 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.最短匹配模式

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

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

>>> 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 的时候返回结果并不是我们想要的。

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

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

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

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

8.多行匹配模式

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

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

>>> 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 次或多次,但尽可能少匹配(非贪婪模式)。
    • ( ) 表示捕获分组,方便提取注释内容。
  • \*/ → 匹配注释的结束符号 */
    • \* 匹配字面量 *
    • / 匹配字面量 /

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

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

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

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

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

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

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

9.将 Unicode 文本标准化

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

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

>>> 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 模块先将文本标准化:

>>> 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,它们在处理某些字符的时候增加了额外的兼容特性。比如:

>>> 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 文本的程序都是非常重要的。当处理来自用户输入的字符串而你很难去控制编码的时候尤其如此。

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

>>> 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 数字字符了:

>>> 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)。比如,下面是一个匹配几个不同阿拉伯编码页面中所有字符的正则表达式:

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

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

>>> 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 的大小写转换和其他大量有趣特性提供全面的支持,包括模糊匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

G皮T

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值