字符串和文本(二)
- 6.字符串忽略大小写的搜索替换
- 7.最短匹配模式
- 8.多行匹配模式
- [9.将 Unicode 文本标准化](#9.将 Unicode 文本标准化)
- [10.在正则式中使用 Unicode](#10.在正则式中使用 Unicode)
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 同样支持扩展的标准化形式 NFKC
和 NFKD
,它们在处理某些字符的时候增加了额外的兼容特性。比如:
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.IGNORECASE
和Unicode
字符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 的大小写转换和其他大量有趣特性提供全面的支持,包括模糊匹配。