Ahora que sabemos cómo representar los
elementos de un documento HTML, es momento de ir a por lo realmente
interesante: cómo analizar el documento e ir extrayendo elemento a
elemento, separando unas cosas de otras, decidiendo dónde empieza y
dónde acaba cada cosa.
Afortunadamente, Ruby es, además de un
lenguaje de programación moderno, una mezcla de características de
otros lenguajes. De aquellas características que más le gustaban a
su creador.
Y entre ellas estaban las expresiones
regulares.
Expresiones regulares
Hay veces que necesitas saber si una cadena sigue o no un determinado patrón. Que si comienza así, que si termina de aquel modo, que si contiene tal cosa... Pues bien, para representar los patrones se utilizan las expresiones regulares.
Mira este ejemplo:
irb(main):001:0> /a.*b/.match('123a11b456')
=> #<MatchData "a11b"> |
Vamos por partes. Para empezar, eso de
/a.*b/ es la expresión regular. Las expresiones regulares se
encierran entre barras “/” pero, como ocurre con las almejas, lo
que nos interesa es lo de dentro. Y ahí pone “a.*b”.
¿Qué significa eso? Bueno, la letra
“a” inicial no tiene demasiado misterio. Quiere decir que la
expresión regular, al compararse con la cadena, trata primero de
encontrar una letra “a”.
Lo de “.*” merece mención aparte.
El punto “.” es un comodín que significa “cualquier carácter”.
Pero no queda ahí la cosa: al punto le sigue un asterisco, *, que, en la
jerga de las expresiones regulares, quiere decir “repite esto
último tantas veces como haga falta. Cero o más veces”.
En definitiva, “.*” podría
traducirse como “repite cualquier carácter cero o más veces”.
O, en otras palabras “aquí me vale cualquier cosa, o incluso
nada”.
Y despúes de “.*” va la “b”.
Resumiendo: /a.*b/ significa que
primero va la “a”, después cualquier cosa de la longitud que sea
y, finalmente, una “b”.
En la cadena que se le pasó como
argumento a “match” en el ejemplo anterior, “123a11b456”, se
puede encontrar ese patrón. Exactamente, a partir del cuarto
carácter tenemos “a11b”. El método “match” lo encuentra y
retorna un objeto de tipo MatchData con la información acerca de qué
coincidencia ha encontrado y dónde la ha hecho.
Sobre expresiones regulares, Ruby y cómo elegir un buen burro
A veces hay más de un posible acierto.
Por ejemplo, imaginemos la misma
expresión regular de antes y la cadena “123a11b456b”. Ahora, la
expresión /a.*b/ tiene dos posibles soluciones: “a11b” y
“a11b456b”. ¿Cuál elige Ruby?
Bueno, la cosa depende (ya se verá más
adelante), pero siguiendo la famosa regla empírica de “mejor el
burro grande, ande o no ande”, lo normal es que se elija la
coincidencia más larga. Y, como no quiero que te creas nada sin
probarlo, vete al irb e inténtalo:
irb(main):002:0> /a.*b/.match('123a11b456b')
=> #<MatchData "a11b456b"> |
Observa que el valor devuelto no es una
cadena. Es un objeto de tipo MatchData. Si deseas saber qué texto
fue el encontrado y obtenerlo como cadena, puedes utilizar el método
“to_s”
irb(main):004:0> /a.*b/.match('123a11b456b').to_s
=> "a11b456b" |
Personalmente, creo que no es momento
aún de entrar en más profundidades acerca de cómo funcionan las
expresiones regulares, pero si tú tienes otra opinión, te dejo un
rato para ir a leer el siguiente documento:
http://www.ruby-doc.org/core/Regexp.html |
Eso sí, vuelve cuando termines. Aquí me quedo esperando.
El tokenizador
Bueno, pues aquí estamos de nuevo. ¿A
qué vino todo ese rollo de las expresiones regulares?
Cuando queremos analizar un documento,
lo primero es ir extrayendo los elementos básicos que lo componen.
Que si una palabra, que si un número,... Esto se llama análisis
lexicográfico y, por hoy, a estos elementos básicos les vamos a
llamar “tokens”.
Después hay que determinar cómo se
relacionan entre sí los elementos para componer estructuras más
complejas. Eso es el análisis sintáctico.
Y, finalmente, hay que ver qué
significan las estructuras que hemos encontrado. El análisis
semántico, vamos.
Por ahora, nos centraremos en el
análisis lexicográfico. Y un poquito del sintáctico. A este nivel,
se trata de hacer cosas como tomar una expresión como:
contador = contador + 5 |
Y extraer de ella sus tokens e
identificarlos. En el caso anterior, si se pueden ignorar los
espacios, se tendrían cinco tokens:
Primero hay un nombre de variable, “contador” |
Después, un operador de asignación “=” |
Después, un nombre de variable, “contador” |
Después, un operador aritmético, “+” |
Después, un número “5” |
Como puedes comprobar, es importante
saber tanto la cadena que representa el token como de qué tipo de
token se trata.
Pues bien, para esto, las expresiones
regulares son muy, muy, muy, muy útiles. Para demostrarlo, vamos a
crear una clase que extraiga los tokens que forman una cadena.
Los objetos de nuestra clase, a la que
llamaremos Tokenizer (así soy de original), necesitarán dos cosas:
- una lista de expresiones regulares, cada una de las cuales tendrá asociado un tipo de token. Por ejemplo, podremos definir una expresión regular consistente en una sucesión de digitos y decir que eso se corresponde con un número entero.
- Una cadena de texto a analizar
Y, una vez les hayamos dado estos
datos, podremos decirle “saca un token y dime qué encuentras y de
qué tipo es”.
Como la mejor forma de demostrar el
movimiento es andando, veamos cómo nos gustaría poder utilizar la
clase. Utilizando irb podríamos empezar por cargar el fichero en que
se define la clase y a crear un objeto Tokenizer
irb(main):001:0> require './tokenizer.rb'
=> true irb(main):002:0> t = Tokenizer.new('ab28dgadf83a3') => #<Tokenizer:0x8cd7a14 @rules=[], @str="ab28dgadf83a3"> |
Al crearlo, le hemos dicho que queremos analizar la cadena 'ab28dgadf83a3'. Ahora le vamos a añadir dos reglas para reconocer los tokens de esta cadena. Supongamos que queremos reconocer dos tipos de cosas: secuencias de letras y secuencias de números.
Tendremos, pues, dos expresiones
regulares. La primera de ellas, que nos permitirá encontrar los
números, vendrá dada por un hash:
{ :regexp => /[0-9]+/ , :type => :number } |
La expresión regular es /[0-9]+/ .
Cuando en una expresión se encuentra un corchete de apertura y otro
de cierre, “[“ y “]”, eso representa un carácter cualquiera
de los que haya dentro. Así “[abc]” representa “a” o “b”
o “c”. Uno de ellos.
Pero imagina ahora que quieres indicar
que quieres una letra minúscula cualquiera. Podrías poner
[abcdefghijklmnopqrstuvwxyz] |
¿Qué te parece? ¿No es demasiado
largo? Aparte de que uno puede equivocarse cuando escribe tanto. Por
eso hay una forma alternativa de escribirlo:
[a-z] |
Con “a-z” se indica “cualquier
carácter desde la “a” hasta la “z”.
De igual forma,
[0-9] |
… representa cuanquier carácter del
“0” al “9”. Cualquier dígito.
En nuestra expresión regular, después
va un signo más, “+”. Este carácter quiere decir “repite lo
anterior una o más veces”. O sea, y en definitiva, que “[0-9]+”
quiere decir “uno o más dígitos”. Y a eso le asignamos el tipo
:number.
De igual forma, a “[a-z]+”, una
secuencia de letras de longitud mayor que cero, le asignamos el tipo
:letter con
{ :regexp => /[a-z]+/ , :type => :letter } |
Podemos usar un método llamado
addRules para añadir ambas reglas de una sola vez:
irb(main):003:0> t.addRules([{:regexp=>/[0-9]+/,
:type=>:number},{:regexp=>/[a-z]+/, :type=>:letter}])
=> [{:regexp=>/[0-9]+/, :type=>:number}, {:regexp=>/[a-z]+/, :type=>:letter}] |
Y ya sólo quedaría decirle al objeto
que nos proporcione los tokens que hay en la cadena y hacer algo con
ellos. Habrá un método, getTokens, que lo haga:
irb(main):004:0> t.getTokens.each {|x| print x.inspect ;
print "\n"}
{:type=>:letter, :string=>"ab"} {:type=>:number, :string=>"28"} {:type=>:letter, :string=>"dgadf"} {:type=>:number, :string=>"83"} {:type=>:letter, :string=>"a"} {:type=>:number, :string=>"3"} => [{:type=>:letter, :string=>"ab"}, {:type=>:number, :string=>"28"}, {:type=>:letter, :string=>"dgadf"}, {:type=>:number, :string=>"83"}, {:type=>:letter, :string=>"a"}, {:type=>:number, :string=>"3"}] |
Observa que el bloque de código contiene dos instrucciones y que, como están en la misma línea, hay que buscar una forma de separarlas. En Ruby, para eso, podemos usar un punto y coma “;”.
getTokens nos extrae todos los tokens de una sola vez. También existirá un método, getToken (en singular) que obtiene sólo un token.
A ambos, getTokens y getToken, le vamos a añadir una característica más. Puede que haya ocasiones y situaciones en que no todos los tipos de tokens sean sintáctica o semánticamente correctos. En que no queramos obtener un token de un tipo inadecuado. Para esos casos, los dos métodos admitirán un parámetro que les indique qué tipos de token queremos reconocer.
Esto nos permitirá tambén utilizar un mismo objeto Tokenizer para distintos análisis. Algunas veces puede venir bien tener este grado de flexibilidad. Y, como veremos, es algo que no cuesta demasiado.
La creación del objeto
Comencemos con los métodos que preparan el objeto para hacer su trabajo. Ahí van los primeros:# Object creation. Initial text may be provided / Creación
del objeto. Se puede dar texto inicial
def initialize(str='') @rules = [] loadStr(str) end # Load initial text / Cargar el texto inicial def loadStr(str='') @str = str end |
Poco nuevo hay aquí. El objeto tendrá dos variables de instancia, @rules y @str, que contendrán las reglas (lo de las expresiones regulares y sus correspondientes tipos) y la cadena a analizar, respectivamente. Pasemos ahora a la carga de los patrones a buscar:
# Add a rule for token recognition / Añadir regla para reconocimiento de tokien # Parameters / Parámetros: # regexp: String or RegExp with the regular expression. If it doesn't start with \A, \A will be #appended at the start # Cadena o Expresión Regular con la expresión regular. Si no empieza por \A, se le añade al #principio # type: Symbol recommended. A type for the regexp. Several regexp may share a common type # Recomendable que sea Symbol. El tipo asociado a regexp. Varias regexp pueden compartir tipo # opts: Options when applying the regexp # Opciones para aplicar regexp def addRule(regexp, type, opts = Regexp::MULTILINE + Regexp::IGNORECASE) str = case regexp.class.name when 'Regexp' then regexp.source when 'String' then regexp else raise 'Unsupported type for regexp' end if str[0..1] != '\A' str = '\A' + str end myregexp = Regexp.new(str, opts) @rules.push({:regexp=>myregexp, :type=>type}) end |
El método sirve para que le digamos al objeto “cuando encuentres algo que siga este patrón, es que tienes un token de tal tipo. Y recibe tres parámetros.
Para no empezar por el principio, el segundo de los parámetros indica qué tipo de token estamos definiendo. A Ruby no le preocupa demasiado qué Yo recomendaría usar aquí valores de tipo Symbol, salvo que se tenga una razón fundada para hacer otra cosa. Ya veremos por qué.
El primero es una expresión regular o bien una cadena que contiene el texto correspondiente a una expresión regular. Igual me da que pongas una cadena como 'a.*b' que una expresión regular como /a.*b/ . En ambos casos, eso va a indicar el patrón que hay que encontrar.
Y el tercero son unas opciones a utilizar cuando se utilice la expresión regular. Ruby tiene unos cuantos. Por defecto, le diremos que las expresiones regulares pueden ocupar más de una línea y que no nos preocupa si las cosas están en mayúsculas o la minúsculas.
El cuerpo del método comienza con una estructura rara:
str = case regexp.class.name
when 'Regexp' then regexp.source when 'String' then regexp else raise 'Unsupported type for regexp' end |
Aquí “str” es una variable local al método. Las variables locales se crean al asignarles el valor. Y esta vez el valor que se le asigna es el resultado de evaluar
case regexp.class.name
when 'Regexp' then regexp.source when 'String' then regexp else raise 'Unsupported type for regexp' end |
Los bloques “case” como éste, toman una expresión (en este caso “regexp.class.name”, una cadena con el nombre de la clase a que pertenece el parámetro “regexp”) y la evalúan. Después miran, una a una, las reglas “when opcion then valor” que siguen. En el caso anterior tenemos
when 'Regexp' then regexp.source
when 'String' then regexp |
.. lo que quiere decir: si es “Regexp”, entonces el valor a devolver es “regexp.source”. El método “source” de la clase “Regexp” convierte la expresión regular en una cadena. Por otro lado, si ya es “String”, entonces retorna directamente “regexp”.
En definitiva, ¿recuerdas que regexp podía ser una expresión regular o una cadena? Pues estamos asegurándonos de que a partir de ahora vamos a trabajar con una cadena.
Lo de
else raise 'Unsupported type for regexp' |
… quiere decir que si no se cumple ninguna de las condiciones de las reglas “when … then ...” (eso es el “else”), se genere una excepción de esas que suelen hacer cascar los programas, con el mensaje de error “Unsupported type for regexp” (“Tipo de datos no soportado para regexp”). La instrucción “raise” se encarga de provocar el error.
Quizá no te parezca buena idea esa de hacer reventar tú mism@ tus programas. Que no queda bien eso de que tus aplicaciones terminen de forma rara y con mensajes de error. Pues te aseguro que te equivocas: me sé de una compañía de software cuyos productos no paran de hacerlo y se venden como rosquillas.
Aparte de eso, ocasión habrá de ver como lidiar con las excepciones y utilizarlas en beneficio propio.
Dejando atrás estas disquisiciones y volviendo al grano, como es de esperar, un “end” marca el final de la expresión.
A continuación aparece:
if str[0..1] != '\A'
str = '\A' + str
end |
La instrucción “if” permite ejecutar unas instrucciones si y solo si se cumple una condición. En este caso, se comprueba si “str[0..1] != '\A'”.
Recuerda que “str” era una cadena. Con str[0..1] se obtienen los caracteres de str que ocupan las posiciones a partir de la 0 (el primer carácter) hasta la 1 (el segundo). En definitiva, estamos hablando de los dos primeros caracteres de str. Después va “!=” que significa “es distinto de”.
Tenemos encontces que si los dos primeros caracteres de str no son “\A”, se le pone “\A” al principio. De ese modo nos aseguramos que str va a comenzar por “\A” ¿Y a qué viene este afán?
Bueno, queremos ir obteniendo los tokens en orden, empezando por el principio, y, como vimos, el método “match” de las expresiones regulares busca en cualquier posición de la cadena... a no ser que le digamos lo contrario. Y “\A” es un código especial de las expresiones regulares que indica, precisamente, el inicio del texto.
Seguimos con
myregexp = Regexp.new(str, opts) |
… que crea una expresión regular a partir de la cadena str y con las opciones que le pasamos al método (recuerda, el tercer parámetro) para después asignársela a la variable local myregexp.
Y acabamos con
@rules.push({:regexp=>myregexp, :type=>type}) |
… que usa el método “push” para añadir un elemento al final del array @rules . Dicho elemento es un hash cuyos índices :regexp y :type apuntan a la expresión regular y su correspondiente tipo, respectivamente.
Ya tenemos como añadir las reglas de una en una. Si queremos poder hacerlo en modo batch (aquí lo llamamos “a puñaos”), podemos definir otro método con unas cuantas líneas de código más:
# Utility function to add several rules at once from an array
of hashes with indexes :regexp and :type
# Función de ayuda para añadir varias reglas de una vez a partrir de un array de hashes con índices :regexp y :type # Parameters / Parámetros: # array: the array / el array # opts: options for regexp / opciones para aplicar las regexp def addRules(array, opts = Regexp::MULTILINE + Regexp::IGNORECASE) array.each do |r| addRule(r[:regexp], r[:type], opts) end end |
Usando el objeto
Con lo anterior tenemos suficiente para crear y configurar el objeto. Pero, claro, si no añadimos algunos métodos más, no nos va a servir para nada. Comencemos por determinar si queda o no texto por procesar:# True if there is nothing more to read / True si no queda
nada por leer def endOfString? return @str == '' end |
Fíjate que le hemos puesto un interrogante al final del nombre del método. Y recuerda que eso era habitual (que no obligatorio) en los métodos que retornaban true o false.
Una sóla línea de instrucciones: “ return @str == '' ”.
Con “@str == '' “ se comprueba si @str es igual a la cadena vacía. El valor resultante de evaluar esta condición es retornado por el método. De hecho, aquí no sería obligatorio poner el “return”, puesto que los métodos retornan, si no se le dice otra cosa, el resultado de evaluar la última instrucción ejecutada. De modo que habría bastado con:
# True if there is nothing more to read / True si no queda
nada por leer def endOfString? @str == '' end |
Y ahora al meollo de la cuestión: cómo reconocer el primer elemento de la cadena:
def getToken(types=nil)
if endOfString? return nil end # Select applicable rules myrules = @rules.select{|r| types == nil or types.include?(r[:type])} # Get longest match match, type = myrules.collect{|r| [r[:regexp].match(@str), r[:type]]}.max{|x,y| x[0].to_s.length <=> y[0].to_s.length} string = match.to_s # If found, process output. Else return nil if string != '' # Removed matched text from the input @str = match.post_match # Subclasses may define methods to process each type if private_methods.include?(type) return send(type.to_s, match[0]) else return {:type=>type, :string=>string} end else return nil end end |
Lo primero que hacemos es comprobar si nos queda algo que analizar. En caso contrario, el método retorna “nil” (recuerda: nil significa nada). Lógico: si no hay cadena a procesar, no hay tokens.
Después seleccionamos qué reglas vamos a comprobar. Porque, recuerda, nuestra clase es capaz de comprobar sólo una o varias reglas si se lo decimos. Y para decírselo tenemos el parámetro opcional “types” al que podemos asignar un array de tipos:
myrules = @rules.select{|r| types == nil or types.include?(r[:type])} |
Observa que se aplica a @rules, un array, el método “select” que permite seleccionar aquellos elementos que cumplen una condición. La condición viene dada por el bloque
{|r| types == nil or types.include?(r[:type])} |
O lo que es lo mismo, si no se ha pasado ningún array de tipos permitidos (types == nil), se considera que todas las reglas son aplicables. Y si se ha pasado una lista de tipos, se seleccionan sólo aquellas reglas cuyo tipo asociado figure en esa lista.
Observa el uso del operador “or”. Una expresión del tipo “a or b” retorna valor true si a es cierto o, caso de que a sea falso, si b es cierto. Y en otro caso retorna false.
Ahora myrules contiene las reglas aplicables. Vamos a la instrucción más extraña de todas:
match, type = myrules.collect{|r| [r[:regexp].match(@str), r[:type]]}.max{|x,y| x[0].to_s.length <=> y[0].to_s.length} |
Rara por varias cosas. En primer lugar porque es una asignación, pero en el lado de la izquierda hay dos variables separadas por comas en lugar de sólo una. Ruby permite asignar los elementos de un array a varias variables a la vez.
irb(main):012:0> v1, v2, v3 = [1, 3, 5]
=> [1, 3, 5] irb(main):013:0> v1 => 1 irb(main):014:0> v2 => 3 irb(main):015:0> v3 => 5 |
Luego, deducirás, lo que hay a la derecha del operador de asignación debe retornar un array de al menos dos elementos. ¡Bien! ¡Se ve que te vas haciendo con la filosofía de Ruby!
Y... ¿qué contiene ese array?
Vamos por partes. En primer lugar, aplicamos el método “collect”. Para cada elemento de myrules, se compara su expresión regular con @str, nuestra cadena de texto a analizar. Después se crea un array de dos elementos: la coincidencia encontrada (que bien podría ser de longitud cero si no se ha encontrado nada) y el tipo de token asociado a la regla aplicada.
Y con todos ellos se crea un nuevo array. Eso significa lo de:
myrules.collect{|r| [ r[:regexp].match(@str), r[:type] ] } |
Lo ideal sería que, como mucho, existiera una única coincidencia de longitud mayor que cero. Pero... ¿y si hay más de una? En ese caso, nos quedaremos con la que se corresponda a una cadena más larga (otra vez la regla de oro del burro grande). Para eso utilizamos el método “max”, que permite obtener el máximo valor de un array.
Comparando lo incomparable
Si el array contuviera números, está claro cuál sería su máximo: el mayor de todos ellos. Si contuviera cadenas, es de esperar que se eligiera la que álfabéticamente queda en último lugar. Pero estamos procesando un array de arrays... ¿cómo se comparan unos arrays formados por dos elementos: un MatchData y, previsiblemente, un Symbol?Bueno, Ruby quizá no lo sepa, pero sabe que nosotros sí. Que tenemos un criterio en mente. Por eso se le puede pasar a “max” un bloque de código que sirva para evaluar si un elemento es mayor o menor que otro. El valor retornado por el bloque debe usar la misma convención que el operador “<=>”: positivo si el primer valor a comparar se considera mayor que el segundo, negativo si menor y cero si son iguales.
El criterio en este caso es la longitud de la coincidencia encontrada. O sea:
{|x,y| x[0].to_s.length <=> y[0].to_s.length} |
Donde x e y son arrays de dos elementos. El primer elemento es una coincidencia MatchData y, el segundo, el tipo asociado.
De modo que x[0] e y[0] son las coincidencias. Comparamos sus longitudes... y la mayor de ellas gana y es la seleccionada.
Y ahora que lo tenemos, vamos a ver qué hacemos con ello. Lo primero es comprobar si la cadena que se ha encontrado es vacía o no. Si no es vacía la procesaremos y si sí lo es retornaremos un valor nil, puesto que no hemos encontrado nada.
Para realizar este tipo de cosas son útiles las sentencias “if … else”, cuyo formato es:
if condición acciones a realizar si la condición es cierta else acciones a realizar si la condición es falsa end |
Volviendo a nuestro método tenemos:
string = match.to_s
if string != ''
# Removed matched text from the input
@str = match.post_match # Subclasses may define methods to process each type if private_methods.include?(type) return send(type.to_s, match) else return {:type=>type, :string=>string} end else return nil end |
De haber encontrado algo, lo primero es quitarlo de @str, de modo que no se lo vuelva a reconocer la próxima vez que se llame a getToken. Para ello usamos el método “post_match” que devuelve el texto de la cadena que va detrás de la coincidencia.
La pregunta del millón
Y después hacemos una pregunta curiosa: ¿Incluye private_methods, la lista de los métodos privados de nuestra clase, uno cuyo nombre sea igual al valor representado por el tipo de la regla seleccionada? O sea, y por poner un ejemplo, si la regla tuviera asignado el tipo :entero... ¿Tiene la clase un método privado llamado “entero”?Pero ¿acaso no sabemos nosotros, que estamos haciendo la clase, si tenemos o no algún método privado y cómo se llama? ¿a qué viene esta pregunta?
Bueno, de hecho esta clase no va a tener ningún método privado. Así que la cosa estaría clara. Pero recuerda lo de la herencia y las subclases. Quién sabe qué van a tener las subclases que podamos crear a partir de ahora.
Si el método privado existe, ejecutamos otra instrucción llamativa:
send(type.to_s, match[0]) |
El método send es una forma de invocar los métodos aplicables a un objeto. Es lo mismo poner
3 + 1 |
… que
3.+(1) |
… que
3.send('+',1) |
… que
3.send(:+,1) |
Fíjate: al usar “send”, en los parámetros primero se pone el identificador del método a invocar, como cadena o como Symbol, y después, si es necesario, los parámetros que hay que pasarle.
Nosotros usamos “send” para que futuras subclases puedan definir qué hacen cada vez que encuentran algo. Y, por si no lo hacen, prevemos que por defecto vamos a retornar un hash de dos elementos, de índices :type y :string, que contienen el tipo de token encontrado y la cadena reconocida.
Pues ya hemos casi acabado. Para simplificar las cosas podemos definir un método que saque todos los tokens de una cadena, uno trás otra, y cree un array con ellos:
def getTokens(types=nil, str=nil)
if str != nil loadStr(str) end return_value = [] while (r = getToken(types)) != nil return_value.push(r) end return return_value end |
Lo único que llama aquí la atención es eso del while:
while (r = getToken(types)) != nil
return_value.push(r) end |
La construcción while realiza una comprobación y, si es falsa, no hace nada. Si es cierta, ejecuta un conjunto de instrucciones y, cuando las termina, vuelve a empezar con la comprobación.
La condición consiste en comparar dos valores y ver si son distintos. El primer valor es el resultado de evaluar
(r = getToken(types)) |
Una asignación. Asignamos a “r” el resultado de obtener un token. Recuerda que una asignación se evalúa al valor asignado. En nuestro caso, el token.
Si eso es distinto de nil es que algún token encontramos. Así que cogemos ese token y lo añadimos al final del array return_value:
return_value.push(r) |
Para, cuando todo acabe, retornar este array:
return return_value |
Ya está todo listo...
La clase completa
Y esto es lo que tenemos:# Class to parse a text and extract tokens from it
class Tokenizer # Object creation. The initial text may be provided / Creación del objeto. Se le puede dar el texto inicial def initialize(str='') @rules = [] loadStr(str) end # Load initial text / Cargar el texto inicial def loadStr(str='') @str = str end # Add a rule for token recognition / Añadir regla para reconocimiento de tokien # Parameters / Parámetros: # regexp: String or RegExp with the regular expression. If it doesn't start with \A, \A will be appended at the start # Cadena o Expresión Regular con la expresión regular. Si no empieza por \A, se le añade al principio # type: Symbol recommended. A type for the regexp. Several regexp may share a common type # Recomendable que sea Symbol. El tipo asociado a regexp. Varias regexp pueden compartir tipo # opts: Options when applying the regexp # Opciones para aplicar regexp def addRule(regexp, type, opts = Regexp::MULTILINE + Regexp::IGNORECASE) str = case regexp.class.name when 'Regexp' then regexp.source when 'String' then regexp else raise 'Unsupported type for regexp' end if str[0..1] != '\A' str = '\A' + str end myregexp = Regexp.new(str, opts) @rules.push({:regexp=>myregexp, :type=>type}) end # Utility function to add several rules at once from an array of hashes with indexes :regexp and :type # Función de ayuda para añadir varias reglas de una vez a partrir de un array de hashes con índices :regexp y :type # Parameters / Parámetros: # array: the array / el array # opts: options for regexp / opciones para aplicar las regexp def addRules(array, opts = Regexp::MULTILINE + Regexp::IGNORECASE) array.each do |r| addRule(r[:regexp], r[:type], opts) end end # True if there is nothing more to read def endOfString? return @str == '' end # Get the next token. An array with the allowed types may be provided # Returns nil if no match was found def getToken(types=nil) if endOfString? return nil end # Select applicable rules myrules = @rules.select{|r| types == nil or types.include?(r[:type])} # Get longest match match, type = myrules.collect{|r| [r[:regexp].match(@str), r[:type]]}.max{|x,y| x[0].to_s.length <=> y[0].to_s.length} string = match.to_s # If found, process output. Else return nil if string != '' # Removed matched text from the input @str = match.post_match # Subclasses may define methods to process each type if private_methods.include?(type) return send(type.to_s, match) else return {:type=>type, :string=>string} end else return nil end end # Get tokens from a String. A list of applicable types and a string may be provided def getTokens(types=nil, str=nil) if str != nil loadStr(str) end return_value = [] while (r = getToken(types)) != nil return_value.push(r) end return return_value end end #class Tokenizer |
Guárdalo en un fichero llamado “tokenizer.rb” y... experimenta un poco.
No hay comentarios:
Publicar un comentario