77

在 Python 中,如何检查字符串是否只包含某些字符?

我需要检查一个仅包含 a..z、0..9 和 .. (句号),没有其他字符。

我可以遍历每个字符并检查字符是 a..z 或 0..9,还是 . 但这会很慢。

我现在不清楚如何使用正则表达式来做到这一点。

它是否正确?您能否建议一个更简单的正则表达式或更有效的方法。

#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<"
'''
4

8 回答 8

82

这是一个简单的纯 Python 实现。它应该在性能不重要时使用(包括为未来的 Google 员工)。

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

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

关于性能,迭代可能是最快的方法。正则表达式必须遍历状态机,并且集合相等解决方案必须构建一个临时集合。但是,差异不太可能很重要。如果这个功能的性能很重要,写成一个C扩展模块,带有一个switch语句(会被编译成一个跳转表)。

这是一个 C 实现,由于空间限制,它使用 if 语句。如果您绝对需要一点点额外的速度,请写出开关盒。在我的测试中,它表现得非常好(在针对正则表达式的基准测试中是 2 秒对 9 秒)。

#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);
}

将其包含在您的 setup.py 中:

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

用于:

>>> from stringcheck import check
>>> check("abc")
True
>>> check("ABC")
False
于 2009-08-24T16:24:38.530 回答
53

最终(?)编辑

答案,包含在一个函数中,带有注释的交互式会话:

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

注意:在此答案中进一步使用 re.match() 进行了比较。进一步的时间表明 match() 会以更长的字符串获胜;当最终答案为 True 时,match() 的开销似乎比 search() 大得多;这令人费解(也许这是返回 MatchObject 而不是 None 的成本)并且可能需要进一步翻找。

==== Earlier text ====

[以前] 接受的答案可以使用一些改进:

(1) Presentation 看起来像是交互式 Python 会话的结果:

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

但 match() 不返回True

(2) 与 match() 一起使用时^,模式开头的 是多余的,并且看起来比没有^

(3) 应该不假思索地自动培养对任何 re 模式的原始字符串的使用

(4)点/句号前面的反斜杠是多余的

(5)比 OP 的代码慢!

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)会产生错误的答案!!

>>> 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
于 2009-08-24T23:12:00.003 回答
42

更简单的方法?多一点Pythonic?

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

它当然不是最有效的,但它肯定是可读的。

于 2009-08-24T16:26:21.803 回答
16

编辑:更改正则表达式以排除 AZ

正则表达式解决方案是目前最快的纯python解决方案

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

与其他解决方案相比:

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

如果要允许空字符串,请将其更改为:

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

根据要求,我将返回答案的另一部分。但请注意以下接受 AZ 范围。

你可以使用isalnum

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

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

编辑使用 isalnum 比设置解决方案更有效

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

EDIT2 John 举了一个例子,上面的方法不起作用。我通过使用编码更改了解决方案以克服这种特殊情况

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

而且它仍然比设置的解决方案快近 3 倍

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

在我看来,使用正则表达式是解决这个问题的最好方法

于 2009-08-24T16:28:56.603 回答
5

这已经得到了令人满意的回答,但是对于事后遇到这个问题的人,我已经对几种不同的方法进行了一些概要分析。在我的情况下,我想要大写的十六进制数字,所以根据需要进行修改以满足您的需要。

这是我的测试实现:

import re

hex_digits = set("ABCDEF1234567890")
hex_match = re.compile(r'^[A-F0-9]+\Z')
hex_search = re.compile(r'[^A-F0-9]')

def test_set(input):
    return set(input) <= hex_digits

def test_not_any(input):
    return not any(c not in hex_digits for c in input)

def test_re_match1(input):
    return bool(re.compile(r'^[A-F0-9]+\Z').match(input))

def test_re_match2(input):
    return bool(hex_match.match(input))

def test_re_match3(input):
    return bool(re.match(r'^[A-F0-9]+\Z', input))

def test_re_search1(input):
    return not bool(re.compile(r'[^A-F0-9]').search(input))

def test_re_search2(input):
    return not bool(hex_search.search(input))

def test_re_search3(input):
    return not bool(re.match(r'[^A-F0-9]', input))

以及测试,在 Mac OS X 上的 Python 3.4.0 中:

import cProfile
import pstats
import random

# generate a list of 10000 random hex strings between 10 and 10009 characters long
# this takes a little time; be patient
tests = [ ''.join(random.choice("ABCDEF1234567890") for _ in range(l)) for l in range(10, 10010) ]

# set up profiling, then start collecting stats
test_pr = cProfile.Profile(timeunit=0.000001)
test_pr.enable()

# run the test functions against each item in tests. 
# this takes a little time; be patient
for t in tests:
    for tf in [test_set, test_not_any, 
               test_re_match1, test_re_match2, test_re_match3,
               test_re_search1, test_re_search2, test_re_search3]:
        _ = tf(t)

# stop collecting stats
test_pr.disable()

# we create our own pstats.Stats object to filter 
# out some stuff we don't care about seeing
test_stats = pstats.Stats(test_pr)

# normally, stats are printed with the format %8.3f, 
# but I want more significant digits
# so this monkey patch handles that
def _f8(x):
    return "%11.6f" % x

def _print_title(self):
    print('   ncalls     tottime     percall     cumtime     percall', end=' ', file=self.stream)
    print('filename:lineno(function)', file=self.stream)

pstats.f8 = _f8
pstats.Stats.print_title = _print_title

# sort by cumulative time (then secondary sort by name), ascending
# then print only our test implementation function calls:
test_stats.sort_stats('cumtime', 'name').reverse_order().print_stats("test_*")

结果如下:

         50335004 次函数调用在 13.428 秒内

   排序依据:累计时间、函数名
   由于限制,名单从 20 个减少到 8 个

   ncalls tottime percall cumtime percall filename:lineno(function)
    10000 0.005233 0.000001 0.367360 0.000037 :1(test_re_match2)
    10000 0.006248 0.000001 0.378853 0.000038 :1(test_re_match3)
    10000 0.010710 0.000001 0.395770 0.000040 :1(test_re_match1)
    10000 0.004578 0.000000 0.467386 0.000047 :1(test_re_search2)
    10000 0.005994 0.000001 0.475329 0.000048 :1(test_re_search3)
    10000 0.008100 0.000001 0.482209 0.000048 :1(test_re_search1)
    10000 0.863139 0.000086 0.863139 0.000086:1(测试集)
    10000 0.007414 0.000001 9.962580 0.000996:1(test_not_any)

在哪里:

通话
该函数被调用的次数
总时间
在给定函数中花费的总时间,不包括子函数的时间
电话
tottime 除以 ncalls 的商
业余时间
在此和所有子功能中花费的累积时间
电话
cumtime 除以原始调用的商

我们真正关心的列是 cumtime 和 percall,因为它向我们展示了从函数进入到退出的实际时间。正如我们所看到的,正则表达式匹配和搜索并没有太大的不同。

如果您每次都编译正则表达式,那么不用费心编译它会更快。编译一次比每次快 7.5%,但编译比不编译快 2.5%。

test_set 比 re_search 慢两倍,比 re_match 慢三倍

test_not_any 比 test_set 慢了一个数量级

TL;DR:使用 re.match 或 re.search

于 2014-07-15T22:15:35.973 回答
2

当您需要比较 hm... 数据集时,请使用 python Sets。字符串可以非常快地表示为字符集。在这里我测试是否允许字符串电话号码。允许第一个字符串,第二个不允许。工作快速简单。

In [17]: timeit.Timer("allowed = set('0123456789+-() ');p = set('+7(898) 64-901-63 ');p.issubset(allowed)").timeit()

Out[17]: 0.8106249139964348

In [18]: timeit.Timer("allowed = set('0123456789+-() ');p = set('+7(950) 64-901-63 фыв');p.issubset(allowed)").timeit()

Out[18]: 0.9240323599951807

如果可以避免,切勿使用正则表达式。

于 2019-01-10T07:36:23.247 回答
0

一种不同的方法,因为在我的情况下,我还需要检查它是否包含某些单词(如本例中的“测试”),而不是单独的字符:

input_string = 'abc test'
input_string_test = input_string
allowed_list = ['a', 'b', 'c', 'test', ' ']

for allowed_list_item in allowed_list:
    input_string_test = input_string_test.replace(allowed_list_item, '')

if not input_string_test:
    # test passed

因此,允许的字符串(char 或 word)从输入字符串中删除。如果输入字符串只包含允许的字符串,它应该留下一个空字符串,因此应该通过if not input_string.

于 2020-03-12T15:35:25.130 回答
-2

由于版本 re 3,4 它更容易。使用fullmatch功能。

import re
----
pattern = r'[^\.a-z0-9]'
result = re.fullmatch(pattern,string)
if result:
   return True
else
   return False
于 2021-05-22T10:26:08.200 回答