Múltiples reemplazos de texto

Tus preguntas. Algoritmos o Grupos de Comandos formando Programas Escripts.
Responder
Avatar de Usuario
Ximorro
Profesional del Autoit
Mensajes: 1500
Registrado: 10 Jul 2009, 12:35
Ubicación: Castellón, España

Múltiples reemplazos de texto

Mensaje por Ximorro »

En AutoIt existe la función StringReplace para reemplazar subcadenas dentro de cadenas, pero no tenemos una función nativa que realice simultáneamente múltiples reemplazos en una cadena de texto. Así por ejemplo si queremos cambiar en una cadena las "á" por "a" y las "é" por "e" tenemos que hacerlo en dos pasos:
$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)
Nótese que ese cambio sólo se puede hacer cuando el carácter destino es el mismo, en este caso "a" en todos. No tenemos una sustitución múltiple completa.

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... :smt010

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
¡¡Sólo eso ya cuesta 0.434seg!! O sea, sólo leerlo cuesta casi 5 veces más que hacer las sustituciones con StringReplace

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
En este caso resulta que el bucle es realmente bastante más rápido, con textos pequeños no se nota mucho la diferencia entre los dos métodos, pero cuando va creciendo mejora. Por ejemplo con un texto de más de dos megas el método StringMid cuesta 6.267seg, con StringSplit sólo 3.57seg, la lectura del vector es rápida (aunque tampoco para tirar cohetes) lo que se paga es el preproceso, ¡ese StringSplit cuesta 2.4seg de los 3.57!

¿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)
¡Pues son 11.2seg! Ir letrita a letrita se paga...

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 :smt010
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
En el primer bucle For con $dict.Add(Clave, Valor) vamos creando las asociaciones entre pares de caracteres en Orig y Dest, es decir, "Š" con "S", "Œ" con "O", etc... Nótese que los caracteres que no hay que sustituir no están aquí.
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
¡Ojo que no se puede hacer Dic.ADD de una clave que ya está agregada, eso da error! (que no notas si no compruebas). Para modificar el valor de una clave ya añadida se hacer Dic.item(Clave) = Valor
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
Pero como digo esto sólo sería para sustituciones en ASCII7 o si acaso en ASCII8 siempre que el programa se use en la misma página de códigos.

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!
MultiReemplazos.au3
(3.73 KiB) Descargado 212 veces
Edit: Método mejorado, los valientes pueden pasar al siguiente comentario para la segunda parte. :smt004
"¿Y no será que en este mundo hay cada vez más gente y menos personas?". Mafalda (Quino)
Avatar de Usuario
Ximorro
Profesional del Autoit
Mensajes: 1500
Registrado: 10 Jul 2009, 12:35
Ubicación: Castellón, España

Re: Múltiples reemplazos de texto

Mensaje por Ximorro »

Os ha asustado la entrada ¿eh?
Lo del diccionario me tocará sacarlo a otra, si no creo que la gente no lo va a ver.

Bueno, he seguido mejorando la cosa. Resulta que tenemos otro problema en AutoIT al manejar las cadenas. Al hacer cosas como:
$cadena &= $letra
"simplemente" añadimos un carácter a una cadena, pero eso implica copiar toda la cadena a otra zona de memoria dándole un carácter más de espacio, y entonces poner el carácter. Eso que parece una tontería es un tiempo apreciable y cuando la cadena va creciendo más y más se nota mucho.

StringReplace puede acceder a la representación interna de la cadena y cambiarla directamente, como AutoIT está hecho en C seguramente serán cadenas tipo C, que son vectores, muy rápidos de manejar.

He intentado hacer algo parecido en AutoIT, volviendo a la idea de usar las codificaciones de los caracteres en vez de los caracteres. He usado AscW con la esperanza de que tratará bien todo el rango Unicode, entiendo que es para eso...
Ahora los códigos se simplifican porque cuando el carácter no hay que cambiarlo se hace ¡nada!, pues modifico directamente la matriz que representa la cadena.
El añadido es que ahora para recomponerla hay que usar al final StringFromASCIIArray(), pero queda compensado con el tiempo reducido en la sustitución.

De esto se benefician los métodos con array y con diccionario, los nuevos tiempos son estos:
Leyendo cada carácter - arrays: 22.838seg (antes 25.73)
Leyendo cada carácter - diccionario: 1.253seg (antes 1.7)

¡Así que algo se gana!

Por cierto, cuando sea posible hay que evitar accesos a matrices, también son bastante lentas :-(
Por ejemplo esta función:

Código: Seleccionar todo

Func _MultiCarArr($Texto)
	Local $aTexto = StringToASCIIArray($Texto), $ubO = Ubound($aOrig)
	For $ci = 0 To UBound($aTexto)-1
		$i = 0
		$c = $aTexto[$ci]
		Do
			$i += 1
		Until $i = $ubO Or $aOrigC[$i] = $c
		If $i < $ubO Then $aTexto[$ci] = $aDestC[$i]
	Next
	Return StringFromASCIIArray($aTexto)
EndFunc
Si la ponemos así, mirad la pequeña diferencia:

Código: Seleccionar todo

Func _MultiCarArr($Texto)
	Local $aTexto = StringToASCIIArray($Texto), $ubO = Ubound($aOrig)
	For $ci = 0 To UBound($aTexto)-1
		$i = 0
		Do
			$i += 1
		Until $i = $ubO Or $aOrigC[$i] = $aTexto[$ci]
		If $i < $ubO Then $aTexto[$ci] = $aDestC[$i]
	Next
	Return StringFromASCIIArray($aTexto)
EndFunc
¡pasa de 22.8segs a 26 segundos! Así que cuando accedáis a vectores en bucles que se ejecutan mucho, si podéis precalcular el valor mejor.


Este es el nuevo código, como veis las funciones quedan bastante más simples, no pongo el de expresiones regulares que queda igual.
MultiReemplazosW.au3
(2.48 KiB) Descargado 151 veces
"¿Y no será que en este mundo hay cada vez más gente y menos personas?". Mafalda (Quino)
jamaro
Hacker del Foro
Mensajes: 253
Registrado: 03 Nov 2010, 23:04

Re: Múltiples reemplazos de texto

Mensaje por jamaro »

Estupendo análisis.

Tendremos que leer y probar detenidamente los métodos más satisfactorios :smt023
Avatar de Usuario
Ximorro
Profesional del Autoit
Mensajes: 1500
Registrado: 10 Jul 2009, 12:35
Ubicación: Castellón, España

Re: Múltiples reemplazos de texto

Mensaje por Ximorro »

¡Me ha leído alguien! ¡Me ha leído alguien! :smt043

Gracias.
Está claro que lo que es por la multisustitución no se ha avanzado mucho: lo más eficiente es hacer múltiples StringReplace.
(Bueno, se ha avanzado en que si no lo probamos no lo sabemos, porque a priori no era tan evidente)

Pero lo interesante son las cosas que he aprendido en el camino, y que espero que a alguien les sea útil:
.- Evitar accesos masivos a vectores por ejemplo en bucles si se puede tomar el valor fuera del bucle.
.- Manipulaciones carácter a carácter sobre vector de valores ANSI/Unicode en vez de sobre el string directamente. (esto no es tan importante pero puede ser útil)
.- Ejemplo de uso de Scripting.Dictionary (esto creo que puede ser muy útil)
"¿Y no será que en este mundo hay cada vez más gente y menos personas?". Mafalda (Quino)
Avatar de Usuario
arkcrew
Profesional del Autoit
Mensajes: 506
Registrado: 28 Sep 2009, 19:17
Ubicación: Granada, España
Contactar:

Re: Múltiples reemplazos de texto

Mensaje por arkcrew »

Ximorro,

Felicidades estupendo análisis, yo siempre uso los múltiples replaces y son bastante lentos, pero es cuestión de seguir mirando, estupendo analisis una vez mas

Un saludo!
Avatar de Usuario
Chefito
Profesional del Autoit
Mensajes: 2035
Registrado: 21 Feb 2008, 18:42
Ubicación: Albacete/Cuenca (España)

Re: Múltiples reemplazos de texto

Mensaje por Chefito »

Que sí te leeemooooss. No pienses que no :smt002 . Lo que pasa que quería probar tu script antes de contestar.

Muy buen análisis de los casos y muy bien explicados.
He probado tu script con la novela de Stephenie Meyer, Luna Nueva. Aquí tienes los resultados de mi protatil:
Leyendo el texto muchas veces (StringReplace): 0.768
Stephenie Meyer
Luna Nueva
- 1 -
Stephenie Meyer Luna Nueva
Para mi padre, Stephen Morgan.
Nadi
Leyendo el texto muchas veces (StringRegExpReplace): 1.309
Stephenie Meyer
Luna Nueva
- 1 -
Stephenie Meyer Luna Nueva
Para mi padre, Stephen Morgan.
Nadi
Leyendo cada carácter - diccionario: 6.103
Stephenie Meyer
Luna Nueva
- 1 -
Stephenie Meyer Luna Nueva
Para mi padre, Stephen Morgan.
Nadi
Leyendo cada carácter - arrays: 128.238
Stephenie Meyer
Luna Nueva
- 1 -
Stephenie Meyer Luna Nueva
Para mi padre, Stephen Morgan.
Nadi
+>23:49:59 AutoIT3.exe ended.rc:0
>Exit code: 0 Time: 137.913
He estado pensando y me pregunto, se comportarán igual stringinstr y StringRegExp que stringreplace y StringRegExpReplace??? Puede que no. Piensa que en el post anterior que hablamos de velocidades, utilizamos las primeras, las de búsqueda, no las de sustitución. Habría que verlo. Es muy facil de comprobar, simplemente variando en tu script estas funciones por las de búsqueda.

Saludos.
Cita vista en algún lugar de la red: En este mundo hay 10 tipos de personas, los que saben binario y los que no ;).
Avatar de Usuario
Ximorro
Profesional del Autoit
Mensajes: 1500
Registrado: 10 Jul 2009, 12:35
Ubicación: Castellón, España

Re: Múltiples reemplazos de texto

Mensaje por Ximorro »

No, si yo pensaba que si no me leíais era culpa mía, por hacer posts tan largos que dan miedo... :smt103
Así que gracias por la paciencia. :smt003

La segunda versión que hice usando codificaciones en vez de caracteres es más rápida, podrías probar esa. Ahí no puse el de ER, habría que reinsertarlo.
Para sacar los tiempos también se puede quitar lo de poner el principio del texto, así queda más compacto (encima en tu texto ni sirve para comprobar porque no ha pillado ningún carácter especial ;-) )
Jope, te has esperado los 128seg del lento, qué moral. Con la segunda versión esperarás menos, je, je.

Creo que la búsqueda en ER tendrá los mismos problemas que la sustitución, haré también alguna prueba y a ver cómo pienso lo de la entrada esa para que no se haga tan larga...
"¿Y no será que en este mundo hay cada vez más gente y menos personas?". Mafalda (Quino)
Responder