views:

9577

answers:

15

What is the easiest way to compare strings in Python, ignoring case?

Of course one can do (str1.lower() <= str2.lower()), etc., but this created two additional temporary strings (with the obvious alloc/g-c overheads).

I guess I'm looking for an equivalent to C's stricmp().

[Some more context requested, so I'll demonstrate with a trivial example:]

Suppose you want to sort a looong list of strings. You simply do theList.sort(). This is O(n * log(n)) string comparisons and no memory management (since all strings and list elements are some sort of smart pointers). You are happy.

Now, you want to do the same, but ignore the case (let's simplify and say all strings are ascii, so locale issues can be ignored). You can do theList.sort(key=lambda s: s.lower()), but then you cause two new allocations per comparison, plus burden the garbage-collector with the duplicated (lowered) strings. Each such memory-management noise is orders-of-magnitude slower than simple string comparison.

Now, with an in-place stricmp()-like function, you do: theList.sort(cmp=stricmp) and it is as fast and as memory-friendly as theList.sort(). You are happy again.

The problem is any Python-based case-insensitive comparison involves implicit string duplications, so I was expecting to find a C-based comparisons (maybe in module string).

Could not find anything like that, hence the question here. (Hope this clarifies the question).

A: 

I'm pretty sure you either have to use .lower() or use a regular expression. I'm not aware of a built-in case-insensitive string comparison function.

Mark Biek
+1  A: 

This is how you'd do it with re:

import re
p = re.compile('^hello$', re.I)
p.match('Hello')
p.match('hello')
p.match('HELLO')
Moses Ting
Case-insensitive regular expressions can only be used for equality tests (True/False), not comparison (less than/equal/greater than)
ΤΖΩΤΖΙΟΥ
A: 

You could subclass str and create your own case-insenstive string class but IMHO that would be extremely unwise and create far more trouble than it's worth.

Dave Webb
A: 

For occasional or even repeated comparisons, a few extra string objects shouldn't matter as long as this won't happen in the innermost loop of your core code or you don't have enough data to actually notice the performance impact. See if you do: doing things in a "stupid" way is much less stupid if you also do it less.

If you seriously want to keep comparing lots and lots of text case-insensitively you could somehow keep the lowercase versions of the strings at hand to avoid finalization and re-creation, or normalize the whole data set into lowercase. This of course depends on the size of the data set. If there are a relatively few needles and a large haystack, replacing the needles with compiled regexp objects is one solution. If It's hard to say without seeing a concrete example.

+1  A: 

There's no built in equivalent to that function you want.

You can write your own fuction that converts to .lower() each carachter at a time to avoid duplicating both strings, but I'm sure it will very cpu-intensive and extremely inefficient.

Unless you are working with extremely long strings (so long that can cause a memory problem if duplicated) then I would keep it simple and use

str1.lower() == str2.lower()

You'll be ok

Ricardo Reyes
"Never say never" :) "There is no built in equivalent" is absolute; "I know of no built in equivalent" would be closer to the truth. locale.strcoll, given a case-insensitive LC_COLLATE (as 'en_US' is), is a built-in.
ΤΖΩΤΖΙΟΥ
+4  A: 

Are you using this compare in a very-frequently-executed path of a highly-performance-sensitive application? Alternatively, are you running this on strings which are megabytes in size? If not, then you shouldn't worry about the performance and just use the .lower() method.

The following code demonstrates that doing a case-insensitive compare by calling .lower() on two strings which are each almost a megabyte in size takes about 0.009 seconds on my 1.8GHz desktop computer:

from timeit import Timer

s1 = "1234567890" * 100000 + "a"
s2 = "1234567890" * 100000 + "B"

code = "s1.lower() < s2.lower()"
time = Timer(code, "from __main__ import s1, s2").timeit(1000)
print time / 1000   # 0.00920499992371 on my machine

If indeed this is an extremely significant, performance-critical section of code, then I recommend writing a function in C and calling it from your Python code, since that will allow you to do a truly efficient case-insensitive search. Details on writing C extension modules can be found here: http://www.python.org/doc/ext/intro.html

Eli Courtwright
+1  A: 

I can't find any other built-in way of doing case-insensitive comparison: The python cook-book recipe uses lower().

However you have to be careful when using lower for comparisons because of the Turkish I problem. Unfortunately Python's handling for Turkish Is is not good. ı is converted to I, but I is not converted to ı. İ is converted to i, but i is not converted to İ.

Douglas Leeder
A: 

An alternative way to do this in place is to iterate over each character in the first string and compare it to the same character in the second string (make sure you check the sizes match first.. if they don't the strings are not the same) and then just lowercase each character individually. This will prevent having to make a new copy of the string. (this is psuedo real python code i don't know python very well and am too lazy to look the exact syntax up)

if string1.size <> string2.size return false 
for char in string1: y = char
    for char in string2: x = char  
        if x.lower() <> y.lower() then 
        return false 
return true
Nicholas
unfortunately, this is at least as bad as the .lower() solution, since you duplicate each char
Paul Oyster
A: 

You could translate each string to lowercase once --- lazily only when you need it, or as a prepass to the sort if you know you'll be sorting the entire collection of strings. There are several ways to attach this comparison key to the actual data being sorted, but these techniques should be addressed in a separate issue.

Note that this technique can be used not only to handle upper/lower case issues, but for other types of sorting such as locale specific sorting, or "Library-style" title sorting that ignores leading articles and otherwise normalizes the data before sorting it.

Dale Wilson
the question is more general than the example itself (actually, in real life scenarios you don't want to be bothered by attaching a lowercase version to every string that might need icmp() later), but even in this trivial example, you don't want to double the memory only to be able to sort...
Paul Oyster
+2  A: 

In response to your clarification...

You could use ctypes to execute the c function "strcasecmp". Ctypes is included in Python 2.5. It provides the ability to call out to dll and shared libraries such as libc. Here is a quick example (Python on Linux; see link for Win32 help):

from ctypes import *
libc = CDLL("libc.so.6")  // see link above for Win32 help
libc.strcasecmp("THIS", "this") // returns 0
libc.strcasecmp("THIS", "THAT") // returns 8

may also want to reference strcasecmp documentation

Not really sure this is any faster or slower (have not tested), but it's a way to use a C function to do case insensitive string comparisons.

~~~~~~~~~~~~~~

ActiveState Code - Recipe 194371: Case Insensitive Strings is a recipe for creating a case insensitive string class. It might be a bit over kill for something quick, but could provide you with a common way of handling case insensitive strings if you plan on using them often.

patrickyoung
i know this recipe well, but behind the scenes it simply have a lowercased duplicate for every string, which is no good (as explained in the trivial example I added)
Paul Oyster
The ctype solution is what I was looking for, thanks. For reference, here is the win32 code:from ctypes import *clib = cdll.LoadLibrary("msvcrt")theList = ["abc","ABC","def","DEF"] * 1000000 theList.sort(cmp = clib._stricmp)
Paul Oyster
this is much slower. see my answer!
hop
I believe this gives the wrong answer for strings with nulls in them.
Darius Bacon
+1  A: 

The recommended idiom to sort lists of values using expensive-to-compute keys is to the so-called "decorated pattern". It consists simply in building a list of (key, value) tuples from the original list, and sort that list. Then it is trivial to eliminate the keys and get the list of sorted values:

>>> original_list = ['a', 'b', 'A', 'B']
>>> decorated = [(s.lower(), s) for s in original_list]
>>> decorated.sort()
>>> sorted_list = [s[1] for s in decorated]
>>> sorted_list
['A', 'a', 'B', 'b']

Or if you like one-liners:

>>> sorted_list = [s[1] for s in sorted((s.lower(), s) for s in original_list)]
>>> sorted_list
['A', 'a', 'B', 'b']

If you really worry about the cost of calling lower(), you can just store tuples of (lowered string, original string) everywhere. Tuples are the cheapest kind of containers in Python, they are also hashable so they can be used as dictionary keys, set members, etc.

Antoine P.
tupples are cheap, but the duplication of strings is not...
Paul Oyster
this is also what python's sort with the key= argument does.
hop
+4  A: 

Your question implies that you don't need Unicode. Try the following code snippet; if it works for you, you're done:

Python 2.5.2 (r252:60911, Aug 22 2008, 02:34:17)
[GCC 4.3.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_COLLATE, "en_US")
'en_US'
>>> sorted("ABCabc", key=locale.strxfrm)
['a', 'A', 'b', 'B', 'c', 'C']
>>> sorted("ABCabc", cmp=locale.strcoll)
['a', 'A', 'b', 'B', 'c', 'C']

Clarification: in case it is not obvious at first sight, locale.strcoll seems to be the function you need, avoiding the str.lower or locale.strxfrm "duplicate" strings.

ΤΖΩΤΖΙΟΥ
The global setting of locale.setlocale() is obviously an overkill (way too global).
Paul Oyster
I don't know what the "obvious overkill" is, and the "global" setting can be as localized as you like (except if you work with threads and need some threads localized and some not, for some reason).
ΤΖΩΤΖΙΟΥ
+19  A: 

The proposed (and accepted) solution of calling into clib is quite stupid. Here is a simple benchmark:

#/usr/bin/env python2.5
import time
import random

from ctypes import *
libc = CDLL("libc.so.6")

words = [word.rstrip() for word in
          open('/usr/share/dict/words', 'r').readlines()]
random.shuffle(words)

print '%i words in list' % len(words)

def test(args):
    t = time.time()
    lst = list(words)
    lst.sort(**args)
    return "%.3f" % (time.time()-t)

print "simple sort:", test({})
print "sort with key=str.lower:", test({'key': str.lower})
print "sort with cmp=libc.strcasecmp:", test({'cmp': libc.strcasecmp})

typical times on a PII@250Mhz (!)

98569 words in list
simple sort: 1.382
sort with key=str.lower: 2.648
sort with cmp=libc.strcasecmp: 17.582

So, str.lower is not only the fastest by far, but also the most portable and pythonic of all the proposed solutions here. The lower() string method also has the advantage of being locale-dependent. Something you will probably not getting right when writing your own "optimised" solution.

I have not profiled memory usage, but the original poster has still not given a compelling reason to worry about it. Also, who says that a call into the libc module doesn't duplicate any strings?

hop
Of course memory is an issue, since more than 99.9% of the .lower() time is memory allocation. Also, on the (windows) machines I checked, the key=_stricmp approach was 4-5 times faster, and with no memory pnalty.
Paul Oyster
4-5 times faster than the .lower-method would mean that it is 2 times faster than the simple sort case. how can that be?!?
hop
@hop all the words in the word list you test on are already lowercased. That might give you results that are far from Paul's.
Virgil Dupras
@hop again: nevermind. I tried sorting the same list with str.upper, and the results are about the same.
Virgil Dupras
@virgil: also, in my locale more than two thirds of all words start with a capital letter ;)
hop
A: 

Just use the str().lower() method, unless high-performance is important - in which case write that sorting method as a C extension.

"How to write a Python Extension" seems like a decent intro..

More interestingly, This guide compares using the ctypes library vs writing an external C module (the ctype is quite-substantially slower than the C extension).

dbr
A: 

I like the regular expression solution. Here's a function you can copy and paste into any function, thanks to python's block structure support.

def equals_ignore_case(str1, str2):
    import re
    return re.match(re.escape(str1) + r'\Z', str2, re.I) is not None

Since I used match instead of search, I didn't need to add a caret (^) to the regular expression.

Ben Atkin
[I wish there was a virtual rubber-stamp for this] Don't use `$`, use `\Z`. Read the fantastic manual to find out what `$` actually does; don't rely on legend or guesswork or whatever.
John Machin
I changed it. I also turned on the community wiki feature for my answer. Thanks.
Ben Atkin