Comment faire une comparaison de chaînes insensible à la casse?

573

Comment puis-je faire une comparaison de chaînes insensible à la casse en Python?

Je voudrais encapsuler la comparaison d'une chaîne régulière à une chaîne de référentiel en utilisant de manière très simple et Pythonic. Je voudrais également avoir la possibilité de rechercher des valeurs dans un dict haché par des chaînes en utilisant des chaînes python normales.

Kozyarchuk
la source

Réponses:

596

En supposant des chaînes 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)")
Harley Holcombe
la source
71
Ça ne marche pas toujours. Considérez par exemple qu'il y a deux sigmas grecs, un seulement utilisé à la fin. La chaîne Σίσυφος ("Sísyphos", ou mieux "Síſyphos") a les trois: majuscule à l'avant, finale minuscule à la fin et minuscule non finale à la troisième position. Si vos deux chaînes sont Σίσυφοςet ΣΊΣΥΦΟΣ, alors votre approche échoue, car celles-ci sont censées être le même cas de manière insensible.
tchrist
52
@ Les deux derniers commentateurs: Je pense qu'il est juste de supposer que les deux chaînes sont des chaînes ascii. Si vous cherchez une réponse à quelque chose d'un peu plus excitant, je suis sûr que c'est là-bas (ou vous pouvez le demander).
Harley Holcombe
16
Problème: 'ß'.lower() == 'SS'.lower()est faux.
kennytm
11
Les lettres grecques ne sont pas le seul cas spécial! En anglais américain, le caractère "i" (\ u0069) est la version en minuscule du caractère "I" (\ u0049). Cependant, l'alphabet turc ("tr-TR") comprend un caractère "I avec un point" "©" (\ u0130), qui est la version majuscule de "i" et "I" est la version captive de "i sans un point "caractère", "ı" (\ u0131).
Gqqnbig
20
@HarleyHolcombe comment est-il sûr (ou juste) de supposer que les chaînes sont ascii? La question n'a pas précisé, et si les chaînes sont à tout moment entrées ou montrées à un utilisateur, alors vous devriez soutenir l'internationalisation. Quoi qu'il en soit, les nouveaux programmeurs liront ceci et nous devrions leur donner la réponse vraiment correcte.
Ethan Reesor
529

Comparer des chaînes de façon insensible à la casse semble trivial, mais ce n'est pas le cas. J'utiliserai Python 3, car Python 2 est sous-développé ici.

La première chose à noter est que les conversions de suppression de cas dans Unicode ne sont pas triviales. Il existe des textes pour lesquels text.lower() != text.upper().lower(), tels que "ß":

"ß".lower()
#>>> 'ß'

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

Mais disons que vous vouliez comparer sans casse "BUSSE" et "Buße". Heck, vous voulez probablement aussi comparer "BUSSE"et "BUẞE"égaliser - c'est la nouvelle forme de capital. La méthode recommandée consiste à utiliser casefold:

str. casefold ()

Renvoie une copie de la chaîne de caractères. Des cordes pliées peuvent être utilisées pour l'appariement sans boîtier.

Le casefolding est similaire à la minuscule mais plus agressif car il est destiné à supprimer toutes les distinctions de casse dans une chaîne. [...]

N'utilisez pas seulement lower . Si casefoldn'est pas disponible, faire .upper().lower()aide (mais seulement un peu).

Ensuite, vous devriez considérer les accents. Si votre rendu de police est bon, vous pensez probablement"ê" == "ê" - mais ce n'est pas le cas:

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

En effet, l'accent sur ce dernier est un caractère combinatoire.

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

La façon la plus simple de résoudre ce problème est unicodedata.normalize. Vous souhaitez probablement utiliser la normalisation NFKD , mais n'hésitez pas à consulter la documentation. Alors on fait

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

Pour finir, cela s'exprime ici en fonctions:

import unicodedata

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

def caseless_equal(left, right):
    return normalize_caseless(left) == normalize_caseless(right)
Veedrac
la source
8
Une meilleure solution consiste à normaliser toutes vos chaînes à la consommation, alors vous pouvez simplement le faire x.casefold() == y.casefold()pour des comparaisons non sensibles à la casse (et, plus important encore, x == ypour les casse).
abarnert
3
@abarnert En effet, selon le contexte - il est parfois préférable de laisser la source intacte, mais la normalisation initiale peut également simplifier le code ultérieur.
Veedrac
3
@Veedrac: Vous avez raison, ce n'est pas toujours approprié; si vous devez être en mesure de sortir la source d'origine inchangée (par exemple, parce que vous avez affaire à des noms de fichiers sous Linux, où NKFC et NKFD sont tous deux autorisés et explicitement censés être différents), vous ne pouvez évidemment pas le transformer en entrée…
abarnert
7
La section 3.13 de la norme Unicode a deux autres définitions pour les comparaisons sans cas: (D146, canonique) des NFD(toCasefold(NFD(str)))deux côtés et (D147, compatibilité) des NFKD(toCasefold(NFKD(toCasefold(NFD(X)))))deux côtés. Il indique que l'intérieur NFDest uniquement destiné à gérer un certain caractère d'accent grec. Je suppose que c'est tout sur les cas de bord.
2
Et un peu de plaisir avec l'alphabet Cherokee, où casefold () va en majuscule: >>> "ᏚᎢᎵᎬᎢᎬᏒ". Upper () 'ᏚᎢᎵᎬᎢᎬᏒ' >>> "ᏚᎢᎵᎬᎢᎬᏒ". Lower () 'ꮪꭲꮅꭼꭲꭼꮢ' >>> "ᏚᎢᎵᎬᎢᎬᏒ" .casefold () 'ᏚᎢᎵᎬᎢᎬᏒ' >>>
bortzmeyer
60

Utilisation de Python 2, appel .lower()à chaque chaîne ou objet Unicode ...

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

... fonctionnera la plupart du temps, mais ne fonctionne pas dans les situations décrites par @tchrist .

Supposons que nous ayons un fichier appelé unicode.txtcontenant les deux chaînes Σίσυφοςet ΣΊΣΥΦΟΣ. Avec 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

Le caractère Σ a deux formes minuscules, ς et σ, et .lower()n'aidera pas à les comparer sans tenir compte de la casse.

Cependant, à partir de Python 3, les trois formulaires se résoudront en ς et l'appel de lower () sur les deux chaînes fonctionnera correctement:

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

Donc, si vous vous souciez des cas marginaux comme les trois sigmas en grec, utilisez Python 3.

(Pour référence, Python 2.7.3 et Python 3.3.0b1 sont indiqués dans les impressions d'interpréteur ci-dessus.)

Nathan Craike
la source
20
Pour rendre la comparaison encore plus robuste, à partir de Python 3.3, vous pouvez utiliser casefold (par exemple, first.casefold () == second.casefold ()). Pour Python 2, vous pouvez utiliser PyICU (voir aussi: icu-project.org/apiref/icu4c/… )
kgriffs
42

La section 3.13 de la norme Unicode définit des algorithmes de correspondance sans cas.

X.casefold() == Y.casefold() en Python 3 implémente la "correspondance par défaut sans cas" (D144).

Le casefolding ne préserve pas la normalisation des chaînes dans tous les cas et la normalisation doit donc être effectuée ( 'å'vs. 'å'). D145 introduit "l'appariement canonique sans cas":

import unicodedata

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

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

NFD() est appelé deux fois pour les cas de bord très peu fréquents impliquant le caractère U + 0345.

Exemple:

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

Il existe également une compatibilité de correspondance sans casse (D146) pour des cas tels que '㎒'(U + 3392) et "correspondance sans casse d'identifiant" pour simplifier et optimiser la correspondance sans casse des identifiants .

jfs
la source
3
C'est la meilleure réponse pour Python 3, car Python 3 utilise des chaînes Unicode et la réponse décrit comment la norme Unicode définit la correspondance des chaînes sans cas.
SergiyKolesnikov
Malheureusement, à partir de Python 3.6, la casefold()fonction n'implémente pas le traitement de cas particulier des majuscules I et des majuscules en pointillés I comme décrit dans Propriétés de pliage de cas . Par conséquent, la comparaison peut échouer pour les mots des langues turques qui contiennent ces lettres. Par exemple, canonical_caseless('LİMANI') == canonical_caseless('limanı')doit revenir True, mais il revient False. Actuellement, la seule façon de gérer cela en Python est d'écrire un wrapper de dossier ou d'utiliser une bibliothèque Unicode externe, telle que PyICU.
SergiyKolesnikov
@SergiyKolesnikov .casefold () se comporte comme il se doit pour autant que je sache. De la norme: "les opérations de casse par défaut sont destinées à être utilisées en l' absence de personnalisation pour des langages et des environnements particuliers" . Les règles de casse pour la capitale turque en pointillé I et la petite i sans point sont dans SpecialCasing.txt. "Pour les langues non turques, ce mappage n'est normalement pas utilisé." De la FAQ Unicode: Q: Pourquoi n'y a-t-il pas de caractères supplémentaires encodés pour prendre en charge le casse indépendant des paramètres régionaux pour le turc?
jfs
1
@ jf-sebastian Je n'ai pas dit que casefold () se conduisait mal. Ce serait juste pratique s'il implémentait un paramètre facultatif qui permettait le traitement spécial des majuscules et des majuscules pointées I. Par exemple, la façon dont le foldCase () dans la bibliothèque de l'ICU le fait : "Le pliage de casse est indépendant des paramètres régionaux et non du contexte -sensible, mais il existe une option pour inclure ou exclure les mappages pour I pointé et i sans point marqués avec "T" dans CaseFolding.txt. "
SergiyKolesnikov
6

J'ai vu cette solution ici en utilisant regex .

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

Il fonctionne bien avec des accents

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

Cependant, cela ne fonctionne pas avec les caractères unicode insensibles à la casse. Merci @Rhymoid d'avoir souligné que, si je comprends bien, il a besoin du symbole exact, pour que le cas soit vrai. La sortie est la suivante:

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)
....:
Shiwangi
la source
4
Le fait que ßne se trouve pas à l' intérieur SSavec la recherche insensible à la casse est la preuve qu'il ne fonctionne pas travailler avec des caractères Unicode du tout .
3

L'approche habituelle consiste à mettre les chaînes en majuscules ou à les mettre en minuscules pour les recherches et les comparaisons. Par exemple:

>>> "hello".upper() == "HELLO".upper()
True
>>> 
Andru Luvisi
la source
2

Que diriez-vous de convertir en minuscules en premier? vous pouvez utiliser string.lower().

Camilo Díaz Repka
la source
4
Vous ne pouvez pas comparer leurs cartes minuscules: Σίσυφοςet ΣΊΣΥΦΟΣne serait pas tester équivalent, mais devrait.
tchrist
-2
def insenStringCompare(s1, s2):
    """ Method that takes two strings and returns True or False, based
        on if they are equal, regardless of case."""
    try:
        return s1.lower() == s2.lower()
    except AttributeError:
        print "Please only pass strings into this method."
        print "You passed a %s and %s" % (s1.__class__, s2.__class__)
Patrick Harrington
la source
3
Vous remplacez une exception par un message imprimé sur stdout, puis retournez None, ce qui est faux. C'est très inutile dans la pratique.
gerrit
-2

Tout ce que vous aurez à faire est de convertir les deux chaînes en minuscules (toutes les lettres deviennent minuscules), puis de les comparer (en supposant que les chaînes sont des chaînes ASCII).

Par exemple:

string1 = "Hello World"
string2 = "hello WorlD"

if string1.lower() == string2.lower():
    print("The two strings are the same.")
else:
    print("The two strings are not the same.")
Rohit Karthik
la source
Cette réponse n'ajoute aucune nouvelle information. De plus, c'est presque la même chose que la réponse acceptée .
Georgy
-3

Ceci est un autre regex que j'ai appris à aimer / détester au cours de la dernière semaine, alors importez généralement comme (dans ce cas oui) quelque chose qui reflète comment je me sens! faire une fonction normale .... demander l'entrée, puis utiliser .... quelque chose = re.compile (r'foo * | spam * ', yes.I) ...... re.I (yes.I ci-dessous) est identique à IGNORECASE mais vous ne pouvez pas faire autant d'erreurs en l'écrivant!

Vous recherchez ensuite votre message à l'aide de regex, mais honnêtement, cela devrait être quelques pages en soi, mais le fait est que foo ou spam sont pipés ensemble et que la casse est ignorée. Ensuite, si l'un ou l'autre est trouvé, lost_n_found affichera l'un d'eux. si ni l'un ni l'autre, lost_n_found est égal à None. Si ce n'est pas égal à aucun, retournez user_input en minuscules en utilisant "return lost_n_found.lower ()"

Cela vous permet de faire correspondre beaucoup plus facilement tout ce qui va être sensible à la casse. Enfin (NCS) signifie "personne ne se soucie sérieusement ...!" ou pas sensible à la casse .... selon

si quelqu'un a des questions, n'hésitez pas à me contacter.

    import re as yes

    def bar_or_spam():

        message = raw_input("\nEnter FoO for BaR or SpaM for EgGs (NCS): ") 

        message_in_coconut = yes.compile(r'foo*|spam*',  yes.I)

        lost_n_found = message_in_coconut.search(message).group()

        if lost_n_found != None:
            return lost_n_found.lower()
        else:
            print ("Make tea not love")
            return

    whatz_for_breakfast = bar_or_spam()

    if whatz_for_breakfast == foo:
        print ("BaR")

    elif whatz_for_breakfast == spam:
        print ("EgGs")
Ali Paul
la source