$Texto = StringReplace($Texto, "á", "a", 0, 1)
$Texto = StringReplace($Texto, "é", "e", 0, 1)
Si queremos cambiar 20 subcadenas, pues hay que procesar $texto 20 veces.
En este comentario y siguientes
http://www.emesn.com/autoitforum/viewto ... 845#p11818
comentaba la posibilidad de que cuando tengamos muchos reemplazos que hacer igual es mejor leer el archivo (o una larga cadena de texto) carácter a carácter y hacer el reemplazo de ese carácter si hace falta, así se lee el texto original sólo una vez, con lo que debería ser más rápido.
Pues he podido comprobar que dada la arquitectura interpretada de AutoIT, que lo hace un lenguaje facil de programar pero no muy eficiente, no podemos conseguir con código AutoIt superar la velocidad del compilado StringReplace, aunque tengamos que hacer muchas sustituciones.
He usado varias posibilidades que comento a continuación, puede seros especialmente interesante la que usa diccionarios (tabla hash), pues es mucho más rápida que usar vectores.
Así que he evaluado estas posibilidades:
a) Múltiples ejecuciones de StringReplace
b) Múltiples ejecuciones de StringRegExpReplace (parecido pero con expresiones regulares)
c) Sustitución carácter a carácter, varios métodos (subcadenas, matrices y una pequeña nota sobre archivos)
d) Sustitución carácter a carácter, con objeto COM Scripting.Dictionary.
En todos los casos uso una cadena de unos míseros 151kb, concretamente 153903 caracteres, correspondientes a los primeros doce capítulos del Quijote.
La sustitución es la del enlace anterior, que cambia algunos caracteres extendidos a la codificación básica ASCII7 (más o menos).
Lo programé en un ordenador rápido pero los tiempos los estoy sacando de un XP en un ordenador de 5 años. De todas maneras lo que importa es la comparación relativa, más que los tiempos absolutos.
El texto se lee de un archivo, pero este proceso no está contemplado en la toma de tiempos, sólo se cronometra el proceso de la sustitución.
a) _MultiTexto(): Este es el algoritmo original expresado en la entrada que enlazo, realizando un StringReplace por cada sustitución a hacer.
La comparación se hace caso sensitivo, lo que la hace muy rápida (StringReplace se vuelve bastante lenta cuando no es caso sensitivo).
Nuestro trozo de Quijote queda procesado en 0.095seg, o sea, se puede considerar instantáneo...
b) _MultiER(): Las expresiones regulares no están diseñadas para hacer sustituciones de textos fijos, sino de patrones que pueden ser muy complejos. Con las ER tenemos tanta flexibilidad que asusta, pero para cosas simples normalmente no hace falta complicarse la vida.
Tengo que poneros otro estudio relativo justamente a esto, pero adelanto que cuando se trata de manejar texto Unicode, es decir, más allá del simple ANSI básico, las ER en AutoIT tienen problemas. En este caso el proceso me cuesta 0.212seg, es decir, algo más del doble.
Hay que decir que aprovechando que tratamos con ER he agrupado sustituciones, pues esto:
$Texto = StringReplace($Texto, "à", "a", 0, 1)
$Texto = StringReplace($Texto, "á", "a", 0, 1)
$Texto = StringReplace($Texto, "â", "a", 0, 1)
$Texto = StringReplace($Texto, "ã", "a", 0, 1)
$Texto = StringReplace($Texto, "ä", "a", 0, 1)
$Texto = StringReplace($Texto, "å", "a", 0, 1)
$Texto = StringReplace($Texto, "æ", "a", 0, 1)
es equivalente a:
$Texto = StringRegExpReplace($Texto, "à|á|â|ã|ä|å|æ", "a")
así que los vectores con las sustituciones hay que modificarlos, tenéis el código completo al final con todos los añadidos.
Código: Seleccionar todo
Global $aOrig2 = StringSplit("Š,Ž,š|ß,ž,À|Á|Â|Ã|Ä|Å|Æ,Ç,È|É|Ê|Ë,Ì|Í|Î|Ï,Ð,Ñ,Ò|Ó|Ô|Õ|Ö|Ø|Œ,Ù|Ú|Û|Ü,Ÿ|¥|Ý,à|á|â|ã|ä|å|æ,ç,è|é|ê|ë,ì|í|î|ï,ñ,ð|ò|ó|ô|õ|ö|ø|œ,ù|ú|û|ü|µ,ý|ÿ", ",", 2)
Global $aDest2 = StringSplit("S,Z,s,z,A,C,E,I,D,N,O,U,Y,a,c,e,i,n,o,u,y", ",", 2)
c) _MultiCarArr(): Este método ha resultado sorprendentemente decepcionante. El código interpretado de AutoIt con su no muy eficiente tratamiento de subcadenas, vectores o archivos nos llevan al desastre...
Para empezar está el tema de cómo leer el texto carácter a carácter. Primero busqué la manera más eficiente de hacer esto, y después me centré en la sustitución.
El primer intento, y lo que uno probablemente piensa primero, es ir recorriendo hasta el tamaño del String e ir sacando los caracteres con StringMid($Texto, $posicion, 1).
La función era:
Código: Seleccionar todo
Func _MultiCarArr($Texto)
For $ci = 1 To StringLen($Texto)
$c = StringMid($Texto, $ci, 1)
Next
Return $Texto
EndFunc
A ver si podemos mejorarlo, StringMid parece lento usado así carácter a carácter, quizás sea más rápido pasando la cadena a un vector y leer el vector en vez de la cadena:
Código: Seleccionar todo
Func _MultiCarArr($Texto)
Local $aTexto = StringSplit($Texto, "", 2)
For $c In $aTexto
Next
Return $Texto
EndFunc
¿Y leyendo directamente desde el archivo? El bucle principal sería este:
Código: Seleccionar todo
$f = FileOpen($archivo,0)
Do
$c = FileRead($f, 1)
Until @error = -1
FileClose($f)
Así que me quedo con el que pasa el texto a vector con StringSplit.
Para la sustitución también probé varias posibilidades, que si For abortado con ExitLoop, que si While, que si Do...Until. Al final el más rápido era un Do...Until haciendo la sustitución fuera del bucle. Es el que tenéis en el programa adjuntado al final.
¿Y cuánto cuesta esto? Pues unos escalofriantes 25.73seg
Igual es que he metido la pata, si alguien mejora el código estaré encantado de leerlo.
Por cierto, hay otra manera de pasar el String a Array, es con StringToASCIIArray, que en vez de caracteres nos da codificaciones numéricas, y que resulta que parece ligeramente más rápido que StringSplit. El problema es que como trabajamos con caracteres Unicode luego hay que hacer las conversiones a carácter con ChrW, con lo que el proceso final es algo más lento (26.6seg frente a 25.73 de StringSplit)
d) _MultiCarDict(): El proceso que estamos realizando se puede modelizar muy bien con una tabla hash, en la que se asocian pares de objetos clave-valor. Las tablas hash están diseñadas de tal manera que dando una clave, te devuelve el valor de una manera muy rápida. He nombrado lo de "objetos" para que sepáis que se puede usar cualquier tipo de objeto, pero nosotros usaremos los caracteres, de tal manera que asociamos cada carácter a sustituir (la clave) con el carácter que le sustituye (el valor).
AutoIt no dispone de tablas hash de forma nativa, pero resulta que Windows ofrece una a través del objeto COM Scripting.Dictionary, así que usamos eso.
En el código adjunto al final están todas las pruebas juntas, os extraigo aquí sólo lo referente al diccionario para explicarlo:
Código: Seleccionar todo
Global $aOrig = StringSplit("Š,Œ,Ž,š,œ,ž,Ÿ,¥,µ,À,Á,Â,Ã,Ä,Å,Æ,Ç,È,É,Ê,Ë,Ì,Í,Î,Ï,Ð,Ñ,Ò,Ó,Ô,Õ,Ö,Ø,Ù,Ú,Û,Ü,Ý,ß,à,á,â,ã,ä,å,æ,ç,è,é,ê,ë,ì,í,î,ï,ð,ñ,ò,ó,ô,õ,ö,ø,ù,ú,û,ü,ý,ÿ", ",", 2)
Global $aDest = StringSplit("S,O,Z,s,o,z,Y,Y,u,A,A,A,A,A,A,A,C,E,E,E,E,I,I,I,I,D,N,O,O,O,O,O,O,U,U,U,U,Y,s,a,a,a,a,a,a,a,c,e,e,e,e,i,i,i,i,o,n,o,o,o,o,o,o,u,u,u,u,y,y", ",", 2)
$dict = ObjCreate("Scripting.Dictionary")
If Not IsObj($dict) Then
MsgBox(0, "Error", "El Diccionario no se ha podido crear")
Exit
EndIf
For $i = 0 To Ubound($aOrig)-1
$dict.Add($aOrig[$i], $aDest[$i])
Next
$entrada = FileRead("D:\Programacion\AutoIt\Cadenas\DonQuijote.txt")
$salida = _MultiCarDict($entrada)
ConsoleWrite(StringLeft($salida,100) & @CRLF) ;Sacamos el principio del texto para comprobar
$dict.RemoveAll()
$dict = 0
Func _MultiCarDict($Texto)
Local $res = "", $aTexto = StringSplit($Texto, "", 2)
For $c In $aTexto
If $dict.Exists($c) Then
$res &= $dict.item($c)
Else
$res &= $c
EndIf
Next
Return $res
EndFunc
Con $dict.item(Clave) obtenemos el valor de esa clave ¡pero sólo si la clave existe, claro!. El problema es que si no existe no da error, simplemente devuelve una cadena vacía Y CREA LA CLAVE CON VALOR VACÍO, esto último es un problema porque la próxima vez que usemos esta clave nos devolverá una sustitución incorrecta. Para evitar esto en la función de sustitución primero miramos si tenemos valor para esa clave, eso se hace con $dict.Exists(Clave). Si existe hacemos la búsqueda, si no usamos el carácter original.
Con este método obtenemos un tiempo de 1,7seg (¡el de vectores tardaba casi 26 segundos!). No llega a ser StringSplit pero gana de calle al vector.
Hay que tener en cuenta que estamos limitados por el hecho de que al usar caracteres extendidos hay un rango muy grande a tratar, pero si sólo fuera ASCII podríamos hacerlo aún más rápido (también el de vectores, simulando una especie de tabla hash a base de arrays y usando el código ASCII como índice en el vector).
Si sólo fuera ASCII podríamos insertar los caracteres base primero y luego modificar sólo los que hay que sustituir:
Código: Seleccionar todo
For $i = 1 To 255
$dict.Add(Chr($i), Chr($i))
Next
For $i = 0 To Ubound($aOrig)-1
$dict.item($aOrig[$i]) = $aDest[$i]
Next
Con esto la función de sustitución queda simplificada:
Código: Seleccionar todo
Func _MultiCarDict($Texto)
Local $res = "", $aTexto = StringSplit($Texto, "", 2)
For $c In $aTexto
$res &= $dict.item($c)
Next
Return $res
EndFunc
Y así queda el "estudio". Como conclusión, pues si no se nos ocurre algo mejor (¿función multisustituciones en una DLL?) a seguir usando múltiples StringSplit.
Queda como herramienta interesante el objeto Scripting.Dictionary de Windows, aquí no llega a salvarnos la vida pero es interesante tenerlo en cuenta porque puede ser muy útil.
Adjunto código completo. Para probarlo tenéis que cambiar el archivo de texto que se asigna a $entrada. Si probáis con cosas de más de 200kb yo comentaría el de matrices porque irá muy lento (el trozo que llama a _MultiCarArr()).
Si queréis hacer pruebas está comentado el método de carácter a carácter con StringMid, así como un par de formas más para el diccionario.
¡Qué aproveche!
Edit: Método mejorado, los valientes pueden pasar al siguiente comentario para la segunda parte.