在 Python 中,如何检查字符串是不是只包含某些字符?
Posted
技术标签:
【中文标题】在 Python 中,如何检查字符串是不是只包含某些字符?【英文标题】:In Python, how to check if a string only contains certain characters?在 Python 中,如何检查字符串是否只包含某些字符? 【发布时间】:2010-11-22 08:12:44 【问题描述】:在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<"
'''
【问题讨论】:
对我来说看起来不错。您不需要在 .如果您在字符类中,但这只是节省一个字符;) @Ingenutrix,约翰确实在我的回答中发现了一个错误。我认为他的解决方案是最好的。 将接受的答案从 Nadia's 更改为 John Machin。 另见 Tim Peters 对此问题的回答:How to check if a string contains only characters from a given set in python 【参考方案1】:这是一个简单的纯 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
【讨论】:
@Nadia:您的解决方案不正确。如果我想要快速且错误的结果,我会问我的猫。 我不能说我喜欢对解决方案投反对票作为对“它比我/另一个解决方案慢”的反应。如果它错误,那么投票是有道理的。但即使在“代码高尔夫”问题中,任何不是最小的答案都不会被否决,随着时间的推移,它不会得到那么多的赞成。 @Adam,你是对的。我觉得我不得不对它投反对票,因为不幸的是,大多数用户本能地盲目地支持解决方案,只是因为他们在没有阅读其他人的情况下处于领先地位。看看 Mark 的解决方案,显然很慢 @John Millikin:-1 您的解决方案不检查“。”如果输入包含'\x00',它会失败。你的猫是怎么回事? 如果函数对无效文本返回“true”,则会失败。异常是意料之外的,但不允许沿着代码路径继续执行以获取正确的字符串,因此不是失败。如果数据是从外部来源(例如文件或数据库)中拉入程序的,则它是用户输入的,应在使用前进行检查。这包括检查字符串是否为有效的 UTF-8(或任何用于存储的编码)。【参考方案2】:最终(?)编辑
答案,封装在一个函数中,带有注释的交互式会话:
>>> 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
【讨论】:
+1 感谢您纠正我的回答。我忘记了匹配只在字符串的开头检查匹配。 Ingenutrix,我认为您应该选择此答案作为已接受。 哇。接受一个后得到另一个解决方案。 @John Machin,感谢您接受这个。您能否将最终清理后的解决方案放在您的帖子顶部。所有这些不同的(尽管很棒的帖子)可能会让另一个来这里寻找最终解决方案的新手感到困惑。请不要更改或删除您帖子中的任何内容,很高兴通过您的步骤看到您的解释。他们信息量很大。谢谢。 @Nadia:你太客气了。谢谢! @Ingenutrix:按要求清理。【参考方案3】:更简单的方法?更 Pythonic 一点?
>>> ok = "0123456789abcdef"
>>> all(c in ok for c in "123456abc")
True
>>> all(c in ok for c in "hello world")
False
它当然不是最有效的,但它肯定是可读的。
【讨论】:
ok = dict.fromkeys("012345789abcdef")
可能会在不影响可读性的情况下加快速度。
@J.F.Sebastian:在我的系统上,使用 dict.fromkeys 和使用长短测试字符串的技巧只快 1% 到 3%。 (使用python 3.3)
@erik:使用bytes.translate
来提高速度。见the discussion in the comments and the performance comparison in the answer【参考方案4】:
编辑:更改正则表达式以排除 A-Z
正则表达式解决方案是目前最快的纯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
根据要求,我将返回答案的另一部分。但请注意,以下接受 A-Z 范围。
您可以使用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 约翰举了一个例子,上面的方法不起作用。我通过使用编码更改了解决方案以克服这种特殊情况
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
我认为使用正则表达式是解决这个问题的最好方法
【讨论】:
看起来这不能正常工作:u"ABC\u0131\u0661".replace('.','').isalnum() -> True,但对于 OP 来说应该是 False测试 非常有趣!顺便说一句,谢谢速度细节,大写检查应该失败,但这是一个小问题 >>> 'A.a'.lower().replace('.', '').isalnum() True 你能更新你的非-encode、encode 和 regex 解决方案以排除 AZ。 (小问题,但你们似乎在这方面遥遥领先,我不想把 .lower(). 放在错误的地方并弄乱答案)我主要关心的是确保我的解决方案是正确的但我很高兴我在这里发布了问题,因为速度非常重要。这项检查将进行几百万次,并且看到了速度结果,这很重要! !!我认为我对 A.a'.lower().replace('.', '').isalnum() 的看法是错误的。这最好留给各位专家。 Nadia,您之前的详细帖子更具信息性和教育意义(即使它与问题有点偏离)。如果你能恢复它,请做。只是通读它可以帮助像我这样的新手。 如果您决定采用这种方法,另一个性能注意事项是您可能应该编译一次正则表达式,然后重新使用编译后的版本,而不是每次调用该函数时都编译它。编译正则表达式是一个非常耗时的过程。【参考方案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)地点:
-
ncalls
该函数被调用的次数
tottime
在给定函数中花费的总时间,不包括子函数的时间
每次调用
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
【讨论】:
hex_match = re.compile(r'^[A-F0-9]+$')
匹配 "F00BAA\n" ... 使用 \Z
而不是 $
$ 匹配 之前 \n:>>> re.match(r'^[A-F0-9]+$', 'F00BAA\n').group(0)'
<<< 'F00BAA'
。仅当您明确希望匹配失败时,如果末尾有换行符,则最好使用 \Z
阅读 OP 问题的第二行:“and no other character”——这需要\Z
【参考方案6】:
当您需要比较 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
如果可以避免,切勿使用正则表达式。
【讨论】:
【参考方案7】:另一种方法,因为在我的情况下,我还需要检查它是否包含某些单词(如本例中的“测试”),而不是单独的字符:
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
因此,允许的字符串(字符或单词)从输入字符串中删除。如果输入字符串只包含允许的字符串,它应该留下一个空字符串,因此应该传递if not input_string
。
【讨论】:
这会遍历每个允许的字符串的整个文本,使其花费 O(n*k) 时间。如果你正在处理大文本,你应该改变它,让它只循环一次它的字符,使它成为 O(n)【参考方案8】:自从版本 re 3,4 以来,它更容易了。使用fullmatch
函数。
import re
----
pattern = r'[^\.a-z0-9]'
result = re.fullmatch(pattern,string)
if result:
return True
else
return False
【讨论】:
以上是关于在 Python 中,如何检查字符串是不是只包含某些字符?的主要内容,如果未能解决你的问题,请参考以下文章