views:

3507

answers:

5

In Python, how to check if a string only contains certain characters?

I need to check a string containing only a..z, 0..9, and . (period) and no other character.

I could iterate over each character and check the character is a..z or 0..9, or . but that would be slow.

I am not clear now how to do it with a regular expression.

Is this correct? Can you suggest a simpler regular expression or a more efficient approach.

#Valid chars . a-z 0-9 
def check(test_str):
    import re
    #http://docs.python.org/library/re.html
    #re.search returns None if no position in the string matches the pattern
    #pattern to search for any character other then . a-z 0-9
    pattern = r'[^\.a-z0-9]'
    if re.search(pattern, test_str):
        #Character other then . a-z 0-9 was found
        print 'Invalid : %r' % (test_str,)
    else:
        #No character other then . a-z 0-9 was found
        print 'Valid   : %r' % (test_str,)

check(test_str='abcde.1')
check(test_str='abcde.1#')
check(test_str='ABCDE.12')
check(test_str='_-/>"!@#12345abcde<')

'''
Output:
>>> 
Valid   : "abcde.1"
Invalid : "abcde.1#"
Invalid : "ABCDE.12"
Invalid : "_-/>"!@#12345abcde<"
'''
+12  A: 

Here's a simple, pure-Python implementation. It should be used when performance is not critical (included for future Googlers).

import string
allowed = set(string.ascii_lowercase + string.digits + '.')

def check(test_str):
    set(test_str) <= allowed


Regarding performance, iteration will probably be the fastest method. Regexes have to iterate through a state machine, and the set equality solution has to build a temporary set. However, the difference is unlikely to matter much. If performance of this function is very important, write it as a C extension module with a switch statement (which will be compiled to a jump table).

Here's a C implementation, which uses if statements due to space constraints. If you absolutely need the tiny bit of extra speed, write out the switch-case. In my tests, it performs very well (2 seconds vs 9 seconds in benchmarks against the regex).

#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *check(PyObject *self, PyObject *args)
{
        const char *s;
        Py_ssize_t count, ii;
        char c;
        if (0 == PyArg_ParseTuple (args, "s#", &s, &count)) {
                return NULL;
        }
        for (ii = 0; ii < count; ii++) {
                c = s[ii];
                if ((c < '0' && c != '.') || c > 'z') {
                        Py_RETURN_FALSE;
                }
                if (c > '9' && c < 'a') {
                        Py_RETURN_FALSE;
                }
        }

        Py_RETURN_TRUE;
}

PyDoc_STRVAR (DOC, "Fast stringcheck");
static PyMethodDef PROCEDURES[] = {
        {"check", (PyCFunction) (check), METH_VARARGS, NULL},
        {NULL, NULL}
};
PyMODINIT_FUNC
initstringcheck (void) {
        Py_InitModule3 ("stringcheck", PROCEDURES, DOC);
}

Include it in your setup.py:

from distutils.core import setup, Extension
ext_modules = [
    Extension ('stringcheck', ['stringcheck.c']),
],

Use as:

>>> from stringcheck import check
>>> check("abc")
True
>>> check("ABC")
False
John Millikin
@sth: Oh, of course. Sorry about that.
John Millikin
Interesting!1) Would set(test_str) get all the characters in same order as 'allowed'?2) I will have to check to speed of 'set(test_str) == allowed' as compared to re.search later but is this faster? any idea?
Ingenutrix
Looks like you update while I was adding my comment!
Ingenutrix
sets are unordered. Since they are implemented using `hash()`, checking for set membership is probably the fastest pure-Python solution. As mentioned, if you need better performance, use switch-case in C.
John Millikin
-1 I'm sorry but your solution is slower than isalnum
Nadia Alramli
@Nadia: your solution is incorrect. If I wanted results which are fast and wrong, I would ask my cat.
John Millikin
@John, please you don't have to be rude about it. So far there are 2 solutions that are about 3 times faster than this one.
Nadia Alramli
@John, and by the way your solution is incorrect too. check('') => True
Nadia Alramli
I can't say that I like downvoting a solution as a reaction to "it's **slower** than my/another solution". If it's **wrong**, downvoting makes sense. But even in "code golf" questions, any answer that's not the smallest doesn't get downvoted, it just won't get as many upvotes over time.
Adam V
@Nadia: I don't understand. Are you saying check("") returns False in your tests? I think it ought to return True -- an empty string, obviously, does not contain any invalid characters.
John Millikin
@Adam, you are correct. I felt that I had to downvote it because unfortunately most users have the instinct to blindly upvote solutions just because they are on the top without reading others. Just look at Mark's solution which is obviously very slow
Nadia Alramli
Since performance is apparently such an issue, I've updated my answer to contain the fastest method I can think of (a C extension module).
John Millikin
@John, since you added a faster method. I'll take back the -1.
Nadia Alramli
Ingenutrix
@John Millikin, Thanks for your solution. Not only the solution, but also seeing the steps to final solution was enlightening.
Ingenutrix
@John Millikin: -1 Your solution doesn't check for '.' AND it fails if the input contains '\x00'. What was that about your cat?
John Machin
The solution will not fail in the presence of a '\x00' byte -- Python will throw an exception when it encounters a rogue '\x00'. I've modified it to return False in that case, since other commenters seem to agree with you, but I doubt think that case will ever occur in real life unless a program accepts input from the user without verifying that it's valid text.
John Millikin
@John Millikin: The exception constitutes a failure. The purpose of the function is to verify that the text is valid!!! Users are unlikely to be the problem; more likely to result from some act of a careless programmer -- go looking in free-text "comment" or "note" columns in databases if you want an introduction to "real life".
John Machin
A failure would be if the function returned "true" for invalid text. An exception is unexpected, but does not allow execution to proceed along the code path for a correct string, and is thus not a failure. If data is pulled into the program from an external source, such as from a file or database, it is user input and should be checked before use. That includes checking that a string is valid UTF-8 (or whatever encoding is used for storage).
John Millikin
+1  A: 

Definitely looks correct; should be efficient too as it's early-out (as opposed to checking every letter to see if they are all valid, it finds the earliest invalid character).

Walt W
+5  A: 

Simpler approach? A little more Pythonic?

>>> ok = "0123456789abcdef"
>>> all(c in ok for c in "123456abc")
True
>>> all(c in ok for c in "hello world")
False

It certainly isn't the most efficient, but it's sure readable.

Mark Rushakoff
`ok = dict.fromkeys("012345789abcdef")` might speed it up without hurting readability much.
J.F. Sebastian
+8  A: 

EDIT: Changed the regular expression to exclude A-Z

Regular expression solution is the fastest pure python solution so far

reg=re.compile('^[a-z0-9\.]+$')
>>>reg.match('jsdlfjdsf12324..3432jsdflsdf')
True
>>> timeit.Timer("reg.match('jsdlfjdsf12324..3432jsdflsdf')", "import re; reg=re.compile('^[a-z0-9\.]+$')").timeit()
0.70509696006774902

Compared to other solutions:

>>> timeit.Timer("set('jsdlfjdsf12324..3432jsdflsdf') <= allowed", "import string; allowed = set(string.ascii_lowercase + string.digits + '.')").timeit()
3.2119350433349609
>>> timeit.Timer("all(c in allowed for c in 'jsdlfjdsf12324..3432jsdflsdf')", "import string; allowed = set(string.ascii_lowercase + string.digits + '.')").timeit()
6.7066690921783447

If you want to allow empty strings then change it to:

reg=re.compile('^[a-z0-9\.]*$')
>>>reg.match('')
False


Under request I'm going to return the other part of the answer. But please note that the following accept A-Z range.

You can use isalnum

test_str.replace('.', '').isalnum()

>>> 'test123.3'.replace('.', '').isalnum()
True
>>> 'test123-3'.replace('.', '').isalnum()
False

EDIT Using isalnum is much more efficient than the set solution

>>> timeit.Timer("'jsdlfjdsf12324..3432jsdflsdf'.replace('.', '').isalnum()").timeit()
0.63245487213134766

EDIT2 John gave an example where the above doesn't work. I changed the solution to overcome this special case by using encode

test_str.replace('.', '').encode('ascii', 'replace').isalnum()

And it is still almost 3 times faster than the set solution

timeit.Timer("u'ABC\u0131\u0661'.encode('ascii', 'replace').replace('.','').isalnum()", "import string; allowed = set(string.ascii_lowercase + string.digits + '.')").timeit()
1.5719811916351318

In my opinion using regular expressions is the best to solve this problem

Nadia Alramli
Looks like this doesn't work properly: u"ABC\u0131\u0661".replace('.','').isalnum() -> True, but should be False for the OP's test
John Millikin
Very interesting! Thx for speed detailsbtw, uppercase check should fail, but that is a minor issue>>> 'A.a'.lower().replace('.', '').isalnum()TrueCan you please update your non-encode, encode and regex solutions to exclude A-Z. (minor issue but you guys seem to be so way ahead on this then I am, I don't want to place .lower(). at wrong place and mess up the answer)My primary concern was to be sure my solution is correct but I am sure glad I posted the problem here as speed is very important. This check will be done a few million times, and having seen the speed results, it does matter!
Ingenutrix
!! I think I was wrong about A.a'.lower().replace('.', '').isalnum()..this best left to you experts.
Ingenutrix
@Ingenutrix, I updated the regular expression to exclude A-Z
Nadia Alramli
Nadia, your earlier detailed post was far more informative and educational, (even if it deviated a bit from the question). If you can restore it, please do. Just reading thru it helps newbies like me.
Ingenutrix
If you do decide to go with this approach, one other performance note is that you should probably compile the regexp once and then re-use the compiled version instead of compiling it everytime you call the function. Compiling a regexp is a pretty time consuming process.
Brent Nash
@Nadia, Thanks for taking time and effort to provide your solution. I do feel you may want to have left your detailed post as is. As I watched you and John, update your answers, it was more informative then just the final solution. Special thanks for providing the timing details.
Ingenutrix
@Ingenutrix, I returned the rest of the answer as requested. And as Brent said you need to compile the regular expression only once.
Nadia Alramli
@Nadia, can you please look into John Machin's answer. He mentions there is something amiss with your solution.
Ingenutrix
@Nadia ........ Thanks
Ingenutrix
+2  A: 

Final(?) edit

Answer, wrapped up in a function, with annotated interactive session:

>>> import re
>>> def special_match(strg, search=re.compile(r'[^a-z0-9.]').search):
...     return not bool(search(strg))
...
>>> special_match("")
True
>>> special_match("az09.")
True
>>> special_match("az09.\n")
False
# The above test case is to catch out any attempt to use re.match()
# with a `$` instead of `\Z` -- see point (6) below.
>>> special_match("az09.#")
False
>>> special_match("az09.X")
False
>>>

Note: There is a comparison with using re.match() further down in this answer. Further timings show that match() would win with much longer strings; match() seems to have a much larger overhead than search() when the final answer is True; this is puzzling (perhaps it's the cost of returning a MatchObject instead of None) and may warrant further rummaging.

==== Earlier text ====

The [previously] accepted answer could use a few improvements:

(1) Presentation gives the appearance of being the result of an interactive Python session:

reg=re.compile('^[a-z0-9\.]+$')
>>>reg.match('jsdlfjdsf12324..3432jsdflsdf')
True

but match() doesn't return True

(2) For use with match(), the ^ at the start of the pattern is redundant, and appears to be slightly slower than the same pattern without the ^

(3) Should foster the use of raw string automatically unthinkingly for any re pattern

(4) The backslash in front of the dot/period is redundant

(5) Slower than the OP's code!

prompt>rem OP's version -- NOTE: OP used raw string!

prompt>\python26\python -mtimeit -s"t='jsdlfjdsf12324..3432jsdflsdf';import
re;reg=re.compile(r'[^a-z0-9\.]')" "not bool(reg.search(t))"
1000000 loops, best of 3: 1.43 usec per loop

prompt>rem OP's version w/o backslash

prompt>\python26\python -mtimeit -s"t='jsdlfjdsf12324..3432jsdflsdf';import
re;reg=re.compile(r'[^a-z0-9.]')" "not bool(reg.search(t))"
1000000 loops, best of 3: 1.44 usec per loop

prompt>rem cleaned-up version of accepted answer

prompt>\python26\python -mtimeit -s"t='jsdlfjdsf12324..3432jsdflsdf';import
re;reg=re.compile(r'[a-z0-9.]+\Z')" "bool(reg.match(t))"
100000 loops, best of 3: 2.07 usec per loop

prompt>rem accepted answer

prompt>\python26\python -mtimeit -s"t='jsdlfjdsf12324..3432jsdflsdf';import
re;reg=re.compile('^[a-z0-9\.]+$')" "bool(reg.match(t))"
100000 loops, best of 3: 2.08 usec per loop

(6) Can produce the wrong answer!!

>>> import re
>>> bool(re.compile('^[a-z0-9\.]+$').match('1234\n'))
True # uh-oh
>>> bool(re.compile('^[a-z0-9\.]+\Z').match('1234\n'))
False
John Machin
+1 Thanks for correcting my answer. I forgot that match checks for a match only at the beginning of the string. Ingenutrix, I think you should select this answer as accepted.
Nadia Alramli
WOW. Getting another solution after accepting one. @John Machin, thanks for taking this up. Could you please just put the final cleaned up solution at top of your post. All these different (though great posts) will probably be confusing for another newbie who comes here searching for the final solution. Please do not change or remove anything in your post, it is great to see your explanation thru your steps. They are very informative. Thanks.
Ingenutrix
@Nadia: That was very gracious of you. Thanks!@Ingenutrix: Cleaned up as requested.
John Machin
Hopefully final!
Ingenutrix