Python2.6: Generar strings aleatorios

El generar strings aleatorios es algo muy importante en implementaciones de todo tipo de sistemas seguros, y desde siempre me ha sorprendido que Python no disponga de un generador nativo de cadenas aleatorias cuando presumen de que viene con "pilas incluidas", refiriéndose a su extensa librería estándar.

Por otro lado, también es sorprendente lo poco que se ha tratado este tema, o al menos, lo difícil que es, relativamente, encontrar lugares en internet donde se trate este tema.

Dada la necesidad de encontrar el algoritmo óptimo, para implementar claves aleatorias en un servidor, mi primera implementación, basada en el código que encontré en un comentario del blog de Stas Ostapenko.

def simpleSample(chars, length):
return "".join( random.sample( chars*length, length ))

Pero me entró la duda de si existiría una forma más eficiente, de modo que hice la prueba con el siguiente código.

# Random string generation time
from time import time
import string
import random

def classic(chars, length):
# Classic
tr=[]
l = len(chars)-1
for i in xrange(length):
tr.append( chars[ random.randint(0,l) ] )
return "".join(tr)

def simpleSample(chars, length):
# derived from
# http://ostas.blogspot.com/2006/12/python-generate-random-strings.html#c6033016737183722956
return "".join( random.sample( chars*length, length ))

def choice(chars, length):
# http://ostas.blogspot.com/2006/12/python-generate-random-strings.html#c9062709590606913845
return "".join([random.choice(chars) for x in xrange(length)])

if __name__=="__main__":
chars = string.letters + string.digits
times = 100
print ("Random string generators comparison.\n" +
"Dictionary length: %s\n" +
"Times to test every algorithm: %s\n") % (len(chars), times)

for size in (8,16,32,64,128,256,512,1024):
print "String size: %s" % size
winnertime = None
winner = None
for i in (classic, simpleSample, choice):
tf = 0
tmin = None
tmax = None
for j in xrange(times):
t0 = time()
text = i(chars, size)
t1 = time()
t = t1-t0
tf += t/times
if not tmin or t < tmin:
tmin = t
if not tmax or t > tmax:
tmax = t
#print "Algorithm: %s\nTime: %s\nGenerated: %s\n" % ( str(i), t1-t0, text )
print " %s\n (%fs, min %fs, max %fs)" % ( repr(i), tf, tmin, tmax )
if not winnertime or tf < winnertime:
winnertime = tf
winner = i
print " Winner: %s (%f seconds)" % (winner, winnertime)

Los resultados serían, con mi AMD Sempron 1.6Ghz:

Random string generators comparison.
Dictionary length: 62
Times to test every algorithm: 100

String size: 8
<function classic at 0xb7683614> (0.000224s, min 0.000037s, max 0.018489s)
<function simpleSample at 0xb7683ae4> (0.000025s, min 0.000023s, max 0.000069s)
<function choice at 0xb7683b1c> (0.000149s, min 0.000018s, max 0.013031s)
Winner: <function simpleSample at 0xb7683ae4> (0.000025 seconds)
String size: 16
<function classic at 0xb7683614> (0.000187s, min 0.000070s, max 0.011462s)
<function simpleSample at 0xb7683ae4> (0.000035s, min 0.000033s, max 0.000070s)
<function choice at 0xb7683b1c> (0.000169s, min 0.000032s, max 0.013551s)
Winner: <function simpleSample at 0xb7683ae4> (0.000035 seconds)
String size: 32
<function classic at 0xb7683614> (0.000151s, min 0.000135s, max 0.001198s)
<function simpleSample at 0xb7683ae4> (0.000317s, min 0.000054s, max 0.026029s)
<function choice at 0xb7683b1c> (0.000064s, min 0.000062s, max 0.000098s)
Winner: <function choice at 0xb7683b1c> (0.000064 seconds)
String size: 64
<function classic at 0xb7683614> (0.000698s, min 0.000267s, max 0.017400s)
<function simpleSample at 0xb7683ae4> (0.000224s, min 0.000095s, max 0.008380s)
<function choice at 0xb7683b1c> (0.000268s, min 0.000116s, max 0.013866s)
Winner: <function simpleSample at 0xb7683ae4> (0.000224 seconds)
String size: 128
<function classic at 0xb7683614> (0.001032s, min 0.000529s, max 0.021555s)
<function simpleSample at 0xb7683ae4> (0.000198s, min 0.000182s, max 0.001264s)
<function choice at 0xb7683b1c> (0.000534s, min 0.000229s, max 0.013713s)
Winner: <function simpleSample at 0xb7683ae4> (0.000198 seconds)
String size: 256
<function classic at 0xb7683614> (0.001496s, min 0.001052s, max 0.014621s)
<function simpleSample at 0xb7683ae4> (0.000634s, min 0.000344s, max 0.017386s)
<function choice at 0xb7683b1c> (0.000928s, min 0.000451s, max 0.013740s)
Winner: <function simpleSample at 0xb7683ae4> (0.000634 seconds)
String size: 512
<function classic at 0xb7683614> (0.003289s, min 0.002106s, max 0.023516s)
<function simpleSample at 0xb7683ae4> (0.001210s, min 0.000693s, max 0.014664s)
<function choice at 0xb7683b1c> (0.001481s, min 0.000901s, max 0.011516s)
Winner: <function simpleSample at 0xb7683ae4> (0.001210 seconds)
String size: 1024
<function classic at 0xb7683614> (0.005419s, min 0.004200s, max 0.023496s)
<function simpleSample at 0xb7683ae4> (0.002135s, min 0.001389s, max 0.014853s)
<function choice at 0xb7683b1c> (0.002663s, min 0.001804s, max 0.013014s)
Winner: <function simpleSample at 0xb7683ae4> (0.002135 seconds)

Por otro lado, en una entrada de stakoverflow, mikelikespie aporta el código siguiente.

def rand1(leng):
nbits = leng * 6 + 1
bits = random.getrandbits(nbits)
uc = u"%0x" % bits
newlen = (len(uc) / 2) * 2 # we have to make the string an even length
ba = bytearray.fromhex(uc[:newlen])
return base64.urlsafe_b64encode(str(ba))[:leng]

Este último algoritmo desbanca sin dificultad a simpleSample, salvo con cadenas de 8 bytes (los cálculos iniciales no compensa en ese caso), en las mismas condiciones que los anteriores, pero que no permite controlar, exactamente, que diccionario de caracteres queremos para la cadena aleatoria generada, ya que el base64 de Python utiliza caracteres de la A a la Z (mayúsculas y minúsculas), dígitos del 0 al 9, / y + (o - y _ si especificamos que sea seguro para urls), y el carácter = como relleno.

No se con lo que me quedaré al final, pero supongo que la enorme diferencia de rendimiento del último podría compensar la falta de flexibilidad en determinados casos.

0 comentarios:

Publicar un comentario