如何进行不区分大小写的字符串比较?

775

如何在Python中以不区分大小写的方式比较字符串?

我希望使用简单且符合Python习惯的代码封装普通字符串与存储库字符串之间的比较。我还希望能够使用普通Python字符串在由字符串哈希的字典中查找值。

15个回答

797
假设字符串是ASCII编码:

假设字符串是ASCII编码:

string1 = 'Hello'
string2 = 'hello'

if string1.lower() == string2.lower():
    print("The strings are the same (case insensitive)")
else:
    print("The strings are NOT the same (case insensitive)")

从Python 3.3开始,casefold()是更好的选择:

string1 = 'Hello'
string2 = 'hello'

if string1.casefold() == string2.casefold():
    print("The strings are the same (case insensitive)")
else:
    print("The strings are NOT the same (case insensitive)")

如果您需要处理更复杂的Unicode比较问题,想要一个更全面的解决方案,请参阅其他答案。


89
有时候那并不起作用。例如考虑希腊字母表中有两个西格玛(sigma),其中一个只用于单词末尾。字符串 Σίσυφος(“Sísyphos”,或更好的“Síſyphos”)包含全部三种情况:大写在前面、小写在末尾以及非末尾的小写字母在第三个位置。如果你的两个字符串分别是 ΣίσυφοςΣΊΣΥΦΟΣ,那么你的方法会失败,因为它们应该是大小写不敏感的相同单词。 - tchrist
66
最后两位评论者:我认为可以合理地假设这两个字符串都是ASCII字符串。如果你在寻找一些更令人兴奋的答案,我相信它们已经存在了(或者你可以提出问题)。 - Harley Holcombe
28
问题:'ß'.lower() == 'SS'.lower() 为假。 - kennytm
13
希腊字母并不是唯一的特殊情况!在美式英语中,字符"i"(\u0069)是字符"I"(\u0049)的小写版本。然而,土耳其("tr-TR")字母表包含一个带点的"I"字符 "İ"(\u0130),它是"i"的大写版本,而且"I"是没有点的"i"字符,即"ı" (\u0131)的大写版本。 - Gqqnbig
34
@HarleyHolcombe,如何安全(或公平)地假设字符串是ASCII码?该问题并没有具体说明,如果这些字符串在任何时候被用户输入或显示,那么你应该支持国际化。无论如何,新手程序员将会阅读这篇文章,我们应该给他们真正正确的答案。 - Ethan Reesor
显示剩余12条评论

714

在不区分大小写的情况下比较字符串似乎很简单,但实际上并非如此。我将使用Python 3,因为Python 2在这方面不够发达。

首先要注意的是,在Unicode中进行大小写转换并不是一件轻松的事情。有些文本的 text.lower() != text.upper().lower(),例如"ß":

>>> "ß".lower()
'ß'
>>> "ß".upper().lower()
'ss'

假设你想要忽略大小写地比较 "BUSSE""Buße"。实际上,你可能还想要将 "BUSSE" 和新的大写形式 "BUẞE" 视为相等。建议的方法是使用casefold:

str.casefold()

返回字符串的 casefolded 副本。Casefolded 字符串可用于无大小写区分的匹配。

casefold 与 lowercasing 类似,但更为激进,因为它旨在消除字符串中所有的大小写区分。[...]

不要仅仅使用 lower。如果没有 casefold,使用 .upper().lower() 也有所帮助(但只是有些帮助)。

接下来需要考虑重音符号。如果你的字体呈现器好的话,你可能认为 "ê" == "ê" - 但实际上并不相等:

>>> "ê" == "ê"
False

这是因为后者的重音是一个组合字符。

>>> import unicodedata
>>> [unicodedata.name(char) for char in "ê"]
['LATIN SMALL LETTER E WITH CIRCUMFLEX']
>>> [unicodedata.name(char) for char in "ê"]
['LATIN SMALL LETTER E', 'COMBINING CIRCUMFLEX ACCENT']

处理这个问题最简单的方法是使用unicodedata.normalize函数。你可能想要使用NFKD规范化,但可以查看文档进行确认。然后执行以下操作:

>>> unicodedata.normalize("NFKD", "ê") == unicodedata.normalize("NFKD", "ê")
True

为了结束,这里将它表述为函数:

import unicodedata

def normalize_caseless(text):
    return unicodedata.normalize("NFKD", text.casefold())

def caseless_equal(left, right):
    return normalize_caseless(left) == normalize_caseless(right)

16
更好的解决方案是在输入时对所有字符串进行标准化处理,这样您就可以使用 x.casefold() == y.casefold() 进行不区分大小写的比较(更重要的是,使用 x == y 进行区分大小写的比较)。 - abarnert
7
确实,根据情境而定——有时候保留原文是更好的选择,但提前进行规范化也能使后续代码更加简单。 - Veedrac
5
@Veedrac: 你是对的,这种方法并不总是适用的;如果你需要能够输出原始源代码而不改变它(例如,因为你正在处理 Linux 系统上的文件名,其中 NKFC 和 NKFD 都被允许且明确应该是不同的),显然你不能在输入时进行转换... - abarnert
8
Unicode标准第3.13节对无大小写区分比较有两个其他定义:(D146,规范化)在两侧使用NFD(toCasefold(NFD(str)))和(D147,兼容性)在两侧使用NFKD(toCasefold(NFKD(toCasefold(NFD(X)))))。它指出内部的NFD仅用于处理特定的希腊重音字符。我猜这都是关于边缘情况的。 - user2379410
2
在切诺基字母表中玩一下,使用casefold()将其转换为大写字母:
"ᏚᎢᎵᎬᎢᎬᏒ".upper() 'ᏚᎢᎵᎬᎢᎬᏒ' "ᏚᎢᎵᎬᎢᎬᏒ".lower() 'ꮪꭲꮅꭼꭲꭼꮢ' "ᏚᎢᎵᎬᎢᎬᏒ".casefold() 'ᏚᎢᎵᎬᎢᎬᏒ'
- bortzmeyer
显示剩余6条评论

65

在Python 2中,对每个字符串或Unicode对象调用 .lower()...

string1.lower() == string2.lower()

......将在大多数情况下有效,但在@tchrist描述的情况下确实无法正常工作。

假设我们有一个名为unicode.txt的文件,其中包含两个字符串ΣίσυφοςΣΊΣΥΦΟΣ。使用Python 2:

>>> utf8_bytes = open("unicode.txt", 'r').read()
>>> print repr(utf8_bytes)
'\xce\xa3\xce\xaf\xcf\x83\xcf\x85\xcf\x86\xce\xbf\xcf\x82\n\xce\xa3\xce\x8a\xce\xa3\xce\xa5\xce\xa6\xce\x9f\xce\xa3\n'
>>> u = utf8_bytes.decode('utf8')
>>> print u
Σίσυφος
ΣΊΣΥΦΟΣ

>>> first, second = u.splitlines()
>>> print first.lower()
σίσυφος
>>> print second.lower()
σίσυφοσ
>>> first.lower() == second.lower()
False
>>> first.upper() == second.upper()
True

Σ字符有两种小写形式,分别是ς和σ,而.lower()方法无法做到不区分大小写的比较。

然而,在Python 3中,这三种形式都将解析为ς,并在两个字符串上调用lower()方法将会正确地工作:

>>> s = open('unicode.txt', encoding='utf8').read()
>>> print(s)
Σίσυφος
ΣΊΣΥΦΟΣ

>>> first, second = s.splitlines()
>>> print(first.lower())
σίσυφος
>>> print(second.lower())
σίσυφος
>>> first.lower() == second.lower()
True
>>> first.upper() == second.upper()
True

如果你关心像希腊字母的三个标准差这样的边缘情况,请使用Python 3。

(参考:上面的解释器打印输出显示了Python 2.7.3和Python 3.3.0b1。)


20
为了让比较更加健壮,从Python 3.3开始,您可以使用casefold(例如,first.casefold() == second.casefold())。对于Python 2,您可以使用PyICU(请参见:http://icu-project.org/apiref/icu4c/classicu_1_1UnicodeString.html#a76f9027fbe4aa6f5b863c2a4a7148078)。 - kgriffs

59

Unicode 标准第 3.13 节 定义了无大小写匹配的算法。

Python 3 中 X.casefold() == Y.casefold() 实现了“默认无大小写匹配”(D144)。

在某些情况下,大小写折叠不会保留字符串的标准化,因此需要进行标准化处理('å' vs. 'å')。D145 引入了“规范无大小写匹配”:

import unicodedata

def NFD(text):
    return unicodedata.normalize('NFD', text)

def canonical_caseless(text):
    return NFD(NFD(text).casefold())

NFD() 在涉及 U+0345 字符的非常罕见的边缘情况下会被调用两次。

例如:

>>> 'å'.casefold() == 'å'.casefold()
False
>>> canonical_caseless('å') == canonical_caseless('å')
True

还有兼容性不区分大小写匹配(D146),用于像'㎒'(U + 3392)这样的情况,以及“标识符不区分大小写匹配”,以简化和优化标识符的不区分大小写匹配。(Unicode技术报告#31)


6
这是Python 3最佳答案,因为Python 3使用Unicode字符串,并且答案描述了Unicode标准如何定义无大小写区分的字符串匹配。 - SergiyKolesnikov
很遗憾,自Python 3.6起,casefold()函数未实现大写字母I和带点的大写字母I的特殊情况处理,如大小写折叠属性所述。因此,对于包含这些字母的突厥语言单词,比较可能会失败。例如,canonical_caseless('LİMANI') == canonical_caseless('limanı')应返回True,但实际上返回了False。目前,在Python中处理这个问题的唯一方法是编写一个casefold包装器或使用外部Unicode库,例如PyICU。 - SergiyKolesnikov
@SergiyKolesnikov .casefold() 表现得尽我所知应该是正确的。根据标准:“默认大小写操作旨在用于特定语言和环境未进行调整的情况下”。土耳其点形大写字母 I 和无点小写字母 i 的大小写规则在 SpecialCasing.txt 中。 “对于非突厥语言,通常不使用此映射。” 根据 Unicode FAQ: 问:为什么没有额外的字符编码来支持土耳其语的区域无关大小写? - jfs
1
@j-f-sebastian我并没有说casefold()有误,但如果它实现了一个可选参数来启用特殊处理大写字母和带点的大写字母I,那将非常实用。例如,ICU库中的foldCase()所做的方式:"大小写折叠与语言环境无关,并且不会考虑上下文,但有一个选项可以选择是否包括或排除标有'T'的带点i和无点i的映射。" - SergiyKolesnikov
@jfs 感谢分享这个解决方案。它对我很有用。 - Lead Developer

10

您可以使用casefold()方法。 casefold()方法在比较时忽略大小写。

firstString = "Hi EVERYONE"
secondString = "Hi everyone"

if firstString.casefold() == secondString.casefold():
    print('The strings are equal.')
else:
    print('The strings are not equal.')

输出:

The strings are equal.

9

我在这里看到了一个解决方案,使用正则表达式来匹配字符串中的子串,忽略大小写。具体解决方案请参考此处

import re
if re.search('mandy', 'Mandy Pande', re.IGNORECASE):
# is True

它能很好地识别口音

In [42]: if re.search("ê","ê", re.IGNORECASE):
....:        print(1)
....:
1

然而,它不支持针对Unicode字符大小写不敏感的情况。感谢@Rhymoid指出这一点,因为我认为它需要精确的符号才能成立。输出如下:

In [36]: "ß".lower()
Out[36]: 'ß'
In [37]: "ß".upper()
Out[37]: 'SS'
In [38]: "ß".upper().lower()
Out[38]: 'ss'
In [39]: if re.search("ß","ßß", re.IGNORECASE):
....:        print(1)
....:
1
In [40]: if re.search("SS","ßß", re.IGNORECASE):
....:        print(1)
....:
In [41]: if re.search("ß","SS", re.IGNORECASE):
....:        print(1)
....:

7
“ß”在不区分大小写的搜索中无法在“SS”中找到,这表明它在Unicode字符中根本无法工作 - user824425

4
通常的做法是将字符串全部大写或小写以进行查找和比较。例如:
>>> "hello".upper() == "HELLO".upper()
True
>>> 

2
我发现了一个简洁的解决方案,其中我正在处理一些常量文件扩展名。
from pathlib import Path


class CaseInsitiveString(str):
   def __eq__(self, __o: str) -> bool:
      return self.casefold() == __o.casefold()

GZ = CaseInsitiveString(".gz")
ZIP = CaseInsitiveString(".zip")
TAR = CaseInsitiveString(".tar")

path = Path("/tmp/ALL_CAPS.TAR.GZ")

GZ in path.suffixes, ZIP in path.suffixes, TAR in path.suffixes, TAR == ".tAr"

# (True, False, True, True)

1
谢谢!这是一个很棒的技巧,可以让 Python 的“内置函数”像 list.index() 和 "in list" 正常工作。 - aghast
作为字典键,如果要使不区分大小写的字符串正常工作,是否需要实现其他内容? - Ryan Leach
是的,你需要定义__hash__方法,这种情况下最好使用class StrEnum(str, Enum):... - Jason Leaver

2

首先,您可以使用string.lower()将文本转换为小写字母。


5
你不能将它们的小写映射进行比较:ΣίσυφοςΣΊΣΥΦΟΣ虽然应该测试结果相同,但实际上并不相同。 - tchrist

0
def search_specificword(key, stng):
    key = key.lower()
    stng = stng.lower()
    flag_present = False
    if stng.startswith(key+" "):
        flag_present = True
    symb = [',','.']
    for i in symb:
        if stng.find(" "+key+i) != -1:
            flag_present = True
    if key == stng:
        flag_present = True
    if stng.endswith(" "+key):
        flag_present = True
    if stng.find(" "+key+" ") != -1:
        flag_present = True
    print(flag_present)
    return flag_present

输出: search_specificword("经济适用房", "到欧洲经济适用房的核心") False

search_specificword("经济适用房", "到欧洲经济适用房的核心") True


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接