¿Cómo verifico que una cadena solo contiene letras, números, guiones bajos y guiones?

86

Sé cómo hacer esto si repito todos los caracteres de la cadena, pero estoy buscando un método más elegante.

Ethan Post
fuente
5
¿Estás hablando de letras ascii, específicas de la configuración regional o unicode?
jfs

Respuestas:

122

Una expresión regular funcionará con muy poco código:

import re

...

if re.match("^[A-Za-z0-9_-]*$", my_little_string):
    # do something here
Thomas
fuente
25
Podría simplificar eso a: ^ [\ w \ d _-] * $
Prestaul
13
Esta solución coincidirá con cadenas de longitud cero. use + en lugar de * para que coincida con cadenas de 1 o más caracteres.
Jerub
10
@Prestaul: \wincluye \dy _, por lo tanto isvalid = re.match(r'[\w-]+$', astr)o isinvalid = re.search(r'[^\w-]', astr). La posible presencia de locale.setlocalecadenas Unicode o de cadenas requiere una consideración adicional.
JFS
1
Corrección: isvalid = re.match(r'[\w-]*$', astr)- las cadenas vacías son válidas.
jfs
¿Cómo puede permitir también un punto / punto (.) En esa expresión regular? Editar, así es como: ^ [a-zA-Z0-9 -_ \ s \.] + $
fredrik
24

[Editar] Hay otra solución que aún no se menciona, y parece superar a las demás dadas hasta ahora en la mayoría de los casos.

Use string.translate para reemplazar todos los caracteres válidos en la cadena, y vea si nos quedan algunos inválidos. Esto es bastante rápido, ya que usa la función C subyacente para hacer el trabajo, con muy poco código de bytes de Python involucrado.

Obviamente, el rendimiento no lo es todo: buscar las soluciones más legibles es probablemente el mejor enfoque cuando no se encuentra en una ruta de código crítica para el rendimiento, pero solo para ver cómo se comparan las soluciones, aquí hay una comparación de rendimiento de todos los métodos propuestos hasta ahora. check_trans es el que usa el método string.translate.

Código de prueba:

import string, re, timeit

pat = re.compile('[\w-]*$')
pat_inv = re.compile ('[^\w-]')
allowed_chars=string.ascii_letters + string.digits + '_-'
allowed_set = set(allowed_chars)
trans_table = string.maketrans('','')

def check_set_diff(s):
    return not set(s) - allowed_set

def check_set_all(s):
    return all(x in allowed_set for x in s)

def check_set_subset(s):
    return set(s).issubset(allowed_set)

def check_re_match(s):
    return pat.match(s)

def check_re_inverse(s): # Search for non-matching character.
    return not pat_inv.search(s)

def check_trans(s):
    return not s.translate(trans_table,allowed_chars)

test_long_almost_valid='a_very_long_string_that_is_mostly_valid_except_for_last_char'*99 + '!'
test_long_valid='a_very_long_string_that_is_completely_valid_' * 99
test_short_valid='short_valid_string'
test_short_invalid='/$%$%&'
test_long_invalid='/$%$%&' * 99
test_empty=''

def main():
    funcs = sorted(f for f in globals() if f.startswith('check_'))
    tests = sorted(f for f in globals() if f.startswith('test_'))
    for test in tests:
        print "Test %-15s (length = %d):" % (test, len(globals()[test]))
        for func in funcs:
            print "  %-20s : %.3f" % (func, 
                   timeit.Timer('%s(%s)' % (func, test), 'from __main__ import pat,allowed_set,%s' % ','.join(funcs+tests)).timeit(10000))
        print

if __name__=='__main__': main()

Los resultados en mi sistema son:

Test test_empty      (length = 0):
  check_re_inverse     : 0.042
  check_re_match       : 0.030
  check_set_all        : 0.027
  check_set_diff       : 0.029
  check_set_subset     : 0.029
  check_trans          : 0.014

Test test_long_almost_valid (length = 5941):
  check_re_inverse     : 2.690
  check_re_match       : 3.037
  check_set_all        : 18.860
  check_set_diff       : 2.905
  check_set_subset     : 2.903
  check_trans          : 0.182

Test test_long_invalid (length = 594):
  check_re_inverse     : 0.017
  check_re_match       : 0.015
  check_set_all        : 0.044
  check_set_diff       : 0.311
  check_set_subset     : 0.308
  check_trans          : 0.034

Test test_long_valid (length = 4356):
  check_re_inverse     : 1.890
  check_re_match       : 1.010
  check_set_all        : 14.411
  check_set_diff       : 2.101
  check_set_subset     : 2.333
  check_trans          : 0.140

Test test_short_invalid (length = 6):
  check_re_inverse     : 0.017
  check_re_match       : 0.019
  check_set_all        : 0.044
  check_set_diff       : 0.032
  check_set_subset     : 0.037
  check_trans          : 0.015

Test test_short_valid (length = 18):
  check_re_inverse     : 0.125
  check_re_match       : 0.066
  check_set_all        : 0.104
  check_set_diff       : 0.051
  check_set_subset     : 0.046
  check_trans          : 0.017

El enfoque de traducción parece mejor en la mayoría de los casos, dramáticamente con cadenas largas válidas, pero es superado por expresiones regulares en test_long_invalid (presumiblemente porque la expresión regular puede rescatar inmediatamente, pero la traducción siempre tiene que escanear toda la cadena). Los enfoques establecidos suelen ser los peores, superando las expresiones regulares solo para el caso de cadenas vacías.

El uso de all (x en permitido_set para x en s) funciona bien si se rescata temprano, pero puede ser malo si tiene que iterar a través de cada carácter. isSubSet y set difference son comparables y son consistentemente proporcionales a la longitud de la cadena independientemente de los datos.

Existe una diferencia similar entre los métodos de expresiones regulares que coinciden con todos los caracteres válidos y la búsqueda de caracteres no válidos. La coincidencia funciona un poco mejor cuando se busca una cadena larga pero completamente válida, pero peor para los caracteres no válidos cerca del final de la cadena.

Brian
fuente
1
Úselo en string.ascii_letterslugar de string.letterssi no usa el indicador re.LOCALE para las expresiones regulares (de lo contrario, podría obtener resultados falsos positivos check_trans(). string.maketrans()No funcionará para cadenas Unicode.)
jfs
Para Python 3 / Unicode / from __future__ import unicode_literals), use trans_table3 = dict((ord(char), '') for char in allowed_chars)y def check_trans(s): return not s.translate(trans_table3). Pero, en general, funciona peor que las versiones RE.
Hugo
14

Hay una variedad de formas de lograr este objetivo, algunas son más claras que otras. Para cada uno de mis ejemplos, 'Verdadero' significa que la cadena pasada es válida, 'Falso' significa que contiene caracteres no válidos.

En primer lugar, está el enfoque ingenuo:

import string
allowed = string.letters + string.digits + '_' + '-'

def check_naive(mystring):
    return all(c in allowed for c in mystring)

Luego está el uso de una expresión regular, puede hacer esto con re.match (). Tenga en cuenta que '-' debe estar al final de []; de lo contrario, se utilizará como delimitador de 'rango'. También tenga en cuenta el $ que significa 'final de cadena'. Otras respuestas indicadas en esta pregunta usan una clase de caracteres especial, '\ w', siempre prefiero usar un rango de clase de caracteres explícito usando [] porque es más fácil de entender sin tener que buscar una guía de referencia rápida, y más fácil de especial- caso.

import re
CHECK_RE = re.compile('[a-zA-Z0-9_-]+$')
def check_re(mystring):
    return CHECK_RE.match(mystring)

Otra solución señaló que puede hacer una coincidencia inversa con expresiones regulares, lo he incluido aquí ahora. Tenga en cuenta que [^ ...] invierte la clase de caracteres porque se usa ^:

CHECK_INV_RE = re.compile('[^a-zA-Z0-9_-]')
def check_inv_re(mystring):
   return not CHECK_INV_RE.search(mystring)

También puedes hacer algo complicado con el objeto 'set'. Eche un vistazo a este ejemplo, que elimina de la cadena original todos los caracteres que están permitidos, dejándonos con un conjunto que contiene a) nada, ob) los caracteres ofensivos de la cadena:

def check_set(mystring):
    return not set(mystring) - set(allowed)
Jerub
fuente
En su primera prueba de expresiones regulares, "[a-zA-Z0-9 _-] + $" no debería ser "[a-zA-Z0-9 _-] * $". La cadena vacía probablemente debería considerarse coincidente.
Brian
Úselo string.ascii_letterssi usa expresiones regulares '[a-zA-Z]'.
jfs
4

Como alternativa al uso de expresiones regulares, puede hacerlo en Conjuntos:

from sets import Set

allowed_chars = Set('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-')

if Set(my_little_sting).issubset(allowed_chars):
    # your action
    print True
Ber
fuente
3
 pat = re.compile ('[^\w-]')

 def onlyallowed(s):
    return not pat.search (s)
Javier
fuente
1

La expresión regular puede ser muy flexible.

import re;
re.fullmatch("^[\w-]+$", target_string) # fullmatch looks also workable for python 3.4

\w: Solamente [a-zA-Z0-9_]

Por lo tanto, debe agregar -char para justificar el guión char.

+: Coincide con una o más repeticiones del carácter anterior. Supongo que no acepta entradas en blanco. Pero si lo hace, cambie a *.

^: Coincide con el inicio de la cadena.

$: Coincide con el final de la cadena.

Necesita estos dos caracteres especiales ya que debe evitar el siguiente caso. Los caracteres no deseados como &aquí pueden aparecer entre el patrón coincidente.

&&&PATTERN&&PATTERN

Alston
fuente
0

Bueno, puedes pedir la ayuda de regex, el genial aquí :)

código:

import re

string = 'adsfg34wrtwe4r2_()' #your string that needs to be matched.
regex = r'^[\w\d_()]*$' # you can also add a space in regex if u want to allow it in the string  
if re.match(regex,string):
    print 'yes'
else: 
    print 'false'

Salida:

yes  

Espero que esto ayude :)

Sravan K Ghantasala
fuente
-1

Siempre puede usar una lista de comprensión y verificar los resultados con todos, sería un poco menos intensivo en recursos que usar una expresión regular: all([c in string.letters + string.digits + ["_", "-"] for c in mystring])

William Keller
fuente
Pruebe su código antes de publicarlo. Una solución basada en su respuesta rota que se ejecuta es: all (c en string.letters + string.digits + "_" para c en mystring)
Jerub
2
Eso requerirá muchos más recursos que una expresión regular. Está haciendo un escaneo lineal para cada personaje (mejor construir un conjunto con anticipación), y está construyendo innecesariamente una lista cuando la comprensión del generador sería más liviana.
Brian
-1

Aquí hay algo basado en el "enfoque ingenuo" de Jerub (¡ingenuo son sus palabras, no las mías!):

import string
ALLOWED = frozenset(string.ascii_letters + string.digits + '_' + '-')

def check(mystring):
    return all(c in ALLOWED for c in mystring)

Si ALLOWEDfuera una cadena, creo c in ALLOWEDque implicaría iterar sobre cada carácter de la cadena hasta que encuentre una coincidencia o llegue al final. Lo cual, para citar a Joel Spolsky, es una especie de algoritmo de Shlemiel the Painter .

Pero probar la existencia en un conjunto debería ser más eficiente, o al menos menos dependiente del número de caracteres permitidos. Ciertamente, este enfoque es un poco más rápido en mi máquina. Está claro y creo que funciona bastante bien en la mayoría de los casos (en mi máquina lenta puedo validar decenas de miles de cadenas cortas en una fracción de segundo). Me gusta.

REALMENTE en mi máquina, una expresión regular funciona varias veces más rápido, y es tan simple como esto (posiblemente más simple). Probablemente esa sea la mejor manera de avanzar.

MEGABYTE.
fuente
-4

¡use una expresión regular y vea si coincide!

([a-z][A-Z][0-9]\_\-)*
Kevin Conner
fuente
1
Todos estos personajes deben estar en una clase, o obtendrás falsos negativos. Además, olvidó incluir los marcadores de comienzo y final de cadena ... así, siempre coincidirá siempre que haya un carácter válido.
Thomas
1
Esto realmente coincidirá incluso si no hay caracteres válidos. Coincidencia de longitud cero. Además, no está en Python.
Jerub