Ya vamos acabando con nuestro proyecto. Parecía al principio
que era algo muy complicado y hoy, si no te aburres de leerme, vas a terminar
la penúltima de sus fases.
Pero no te preocupes, que algo más que hacer buscaremos.
Los atributos
Lo primero que vamos a construir es un analizador al que le
pasaremos una cadena con los atributos de una etiqueta y nos la convertirá en
un hash perteneciente a nuestra clase Attributes.
HTML es un lenguaje muy permisivo. Y los navegadores admiten
incluso documentos peor construidos de los que el lenguaje permite. Eso nos va a complicar un poco, sólo un
poco, las cosas. Sólo es cuestión de mirar qué hacen los navegadores cuando se
encuentran con algo raro y copiarnos de ellos.
Observemos la siguiente etiqueta
<table a=cadena b c=1 b= 'cadena' c =
"cadena1" >
|
El nombre de la etiqueta es “table”.
Y sus atributos vienen dados por:
a=cadena b c=1
b= 'cadena' c = "cadena1"
|
Donde se puede observar que las asignaciones de valores
pueden tomar varias formas. En primer lugar, las cadenas pueden ir encerradas
entre comillas simples o dobles. O incluso no ir encerradas entre comillas, en
cuyo caso acaban en cuanto se encuentra un espacio o el final de la etiqueta
(>).
Pero es que, además, puede que aparezca sólo el nombre del
atributo. Como la “b” del ejemplo. En estos casos, se supondrá que el valor
asignado es “1”.
Aparte de eso, y por muy mal que esté, vamos a tolerar que
aparezcan cadenas encerradas entre comillas dobles o simples donde no vengan a
cuento. Eso sí, las vamos a ignorar. Para nosostros, como si no existieran. Un
ejemplo:
a=cadena
'cadena fuera de lugar' b c=1 b= 'cadena' c = "cadena1"
|
O sea, lo que en lenguaje técnico se denomina "una porquería". Por no soltar aquí palabras malsonantes.
¿Que, a pesar de todo, queremos analizar esto con Ruby? ¡OK! Ya tenemos la
clase Tokenizer, de modo que sólo es cuestión de crear las expresiones
regulares.
Incluye tus ficheros
Vamos a crear un fichero llamado “htmlparser.rb” en la misma
carpeta en la que tenemos los otros que hemos ido creando hasta ahora.
Lo primero será incluir los archivos que contienen las clases
que vamos a necesitar:
require
File.dirname(__FILE__) + '/tokenizer.rb'
require
File.dirname(__FILE__) + '/attributes.rb'
require
File.dirname(__FILE__) + '/htmlthings.rb'
|
Ya tenemos disponibles los contenidos de “tokenizer.rb”,
“attributes.rb” y “htmlthings.rb”. Y tú me dirás: “¿Pero htmlthings.rb no hacía
ya un require de attributes.rb? ¿Qué pasa, que vamos a procesar attributes.rb dos veces?”
No en este caso. Porque require comprueba si el fichero ha
sido ya cargado antes y, de ser así, no hace nada. O sea, que si un fichero se
incluye más de una vez con “require” no pasa nada.
Habrá ocasiones en que sí nos interese cargar dos veces un
mismo fichero. Para eso, en lugar de “require”, se usa “load”.
Analizador de atributos
Vamos ahora con la clase que analiza los atributos. Se
llamará AttrParser y, para empezar, sólo indicaremos que es una subclase de
Tokenizer:
class AttrParser < Tokenizer
end
|
Momento de pensar un poco. ¿Qué expresiones regulares
necesitamos?
Comencemos por algo simple. Tenemos que descartar todos los
espacios, tabuladores, retornos de línea y similares. Ruby tiene una forma de
indicar este tipo de caracteres: [[:space:]] .
En las expresiones regulares, cuando uno se encuentra con dos
corchetes de apertura seguidos, [[, a continuación el carácter de dos puntos,
:, un nombre, otra vez los dos puntos , :, y dos corchetes de cierre, ]], eso
indica una categoría de caracteres. En el caso de “space”, como se indicó
antes, se trata de los separadores de palabras: espacio en blanco, tabulador,
retorno de carro, etc.
Para hacer más legible el código vamos a crear cadenas con
los principales elementos a reconocer y después las combinaremos para crear
expresiones más complejas. Recuerda que Tokenizer convertirá de motu propio
estas cadenas en expresiones regulares.
Así, para una secuencia de cero o más espacios consecutivos tendriamos:
Así, para una secuencia de cero o más espacios consecutivos tendriamos:
spaces = '[[:space:]]*'
|
Le toca ahora el turno a los nombres de atributos. Para
simplificar, vamos a considerar que estos nombres empiezan por un carácter que
no es ni un espacio ni un signo igual (=), ni comillas simples ni comillas
dobles. Eso, como expresión regular, sería:
[^[[:space:]]='"]
|
Observa que tenemos una serie de cosas entre corchetes y
recuerda que eso quería decir: “un carácter cualquiera de éstos”. Pero esta
vez lo primero que hay es un carácter ^. Cuando ocurre esto, se invierte el
significado: “un carácter que no sea niguno de éstos”. Ni [[:space:]], ni =, ni ',
ni ".
Ya hemos detectado el primer elemento del nombre. ¿Qué
pasa con el segundo, el tercer y todos los demás? ¿Hasta dónde y cuándo
seguirán siendo los caracteres parte del nombre?
Pues... hasta que encontremos un espacio o un signo
igual. O cuando se acaben los caracteres de la cadena. Cualquier otra cosa
seguirá siendo parte del nombre:
[^[[:space:]]=]*
|
Poniéndolo todo junto, un nombre de atributo vendría
dado por
[^[[:space:]]='"][^[[:space:]]=]*
|
Partes con nombre
Pero vamos a ir un paso más allá. Ruby nos permite hacer
cosas muy curiosas con las expresiones regulares y una de ellas es ponerle
nombres a sus partes. Supongamos que queremos
reconocer subcadenas formadas por una letra y un dígito. Tendríamos la
expresión regular:
[A-Za-z][0-9]
|
Ruby nos permite poner un nombre a una parte de la
expresión regular para más adelante recuperar su valor. Supongamos que después querremos
saber qué letra se encontró. Podríamos usar entonces
(?<letra>[A-Za-z])[0-9]
|
Fíjate: hemos puesto la parte de la expresión regular
que queremos reconocer entre paréntesis y, junto al paréntesis de apertura,
hemos puesto “?<letra>” para ponerle un nombre (“letra”). Con irb podemos
ver cómo funciona:
irb(main):043:0> coincidencia =
/(?<letra>[A-Za-z])[0-9]/.match('1232!Qe3 ')
=> #<MatchData "e3"
letra:"e">
irb(main):044:0> coincidencia[:letra]
=> "e"
irb(main):045:0>
|
Observa que el objeto de clase MatchData que representa
la coincidencia guarda una referencia a la “letra” y que ésta se puede extraer
aplicándole el operador [] con un argumento Symbol.
Volviendo al nombre de los atributos, y representándolo
ya como cadena, quedaría:
name
= '(?<attr>[^[[:space:]]=\'"][^[[:space:]]=]*)'
|
Lo único raro, aparte de que hemos puesto el nombre
“attr” al nombre del atributo, es ese \' que sirve para meter una comilla
simple dentro de una cadena encerrada entre comillas simples. Otro uso de las
“secuencias de escape”: quitarle el significado especial a los caracteres.
Sigamos. Entre el nombre del atributo y su
correspondiente valor pueden ir unos espacios opcionales, el signo igual y
otros espacios opcionales. Recuerda que para lo de los espacios habíamos creado
una variable llamada “spaces”, lo que nos deja:
equal
= spaces + '=' + spaces
|
Y después de eso el valor. Que puede ser una cadena
encerrada entre comillas simples. A lo que va entre las comillas le pondremos
como nombre “val”:
'(?<val>[^']*)'
|
La parte con nombre “val” exluye, como ves, las comillas. Será la que contenga el valor
del atributo.
A la hora de crear una cadena con este contenido, y para
evitar problemas, lo encerraremos entre comillas dobles:
string1
= "'(?<val>[^']*)'"
|
Algo similar podemos hacer con las cadenas entre
comillas dobles:
"(?<val>[^"]*)"
|
… y tendríamos, encerrándola entre comillas simples para formar una cadena:
string2 = '"(?<val>[^"]*)"'
|
Finalmente, definiremos las cadenas no encerradas entre
comillas de forma similar a los nombres de atributos:
literal
= '(?<val>[^[[:space:]]=\'"][^[[:space:]]=]*)'
|
Con los componentes que hemos definido, es posible crear
las reglas que reconocen los atributos y que responderían a las estructuras:
nombre
nombre = 'cadena entre comillas simples'
nombre = "cadena entre comillas dobles"
nombre = cadenasincomillas
|
… y que, utilizando los métodos heredados de la clase
Tokenizer, quedaría
addRule(name,
:attribute)
addRule(name+equal+string1,
:attribute)
addRule(name+equal+string2,
:attribute)
addRule(name+equal+literal,
:attribute)
|
También tenemos que decirle que debe ignorar los
espacios sobrantes y las cadenas que aparezcan fuera de lugar:
addRule(spaces,
:ignore)
addRule(string1,
:ignore)
addRule(string2, :ignore)
|
Observa que sólo hemos definido dos tipos de elementos:
- :attribute para las asignaciones de valores a
atributos
- :ignore para cosas a ignorar
Qué hacer con cada cosa
Ha llegado el turno de decirle a nuestra clase qué tiene
que hacer cuando encuentre un atributo y cuando encuentre algo a ignorar. Para
eso crearemos dos métodos privados con los mismos nombres que les pusimos a los
tipos de elemento: “attribute” e “ignore”:
# Atributos
def attribute(x)
val =
x.names.include?("val") ? x[:val] : 1
return
{x[:attr]=>val}
end
# Cosas a ignorar
def ignore(x)
return :ignore
end
|
Para las cosas a ignorar, simplemente retornamos
:ignore. Para los atributos hacemos un poco más:
Recuerda que estaríamos hablando de cosas que han coincidido con expresiones regulares del tipo:
(?<attr>[^[[:space:]]='"][^[[:space:]]=]*)[[:space:]]=[[:space:]]'(?<val>[^']*)' |
o
(?<attr>[^[[:space:]]='"][^[[:space:]]=]*) |
Para empezar, comprobamos si el MatchData que recibimos
tiene una parte a la que le hayamos puesto el nombre “val”. De existir, ése
sería el valor asignado al atributo (eso es lo que quiere decier x[:val] ). En
caso contrario tomamos “1” como valor por defecto.
Hecho esto, retornamos un hash de un único elemento del
tipo { nombre_del_atributo => valor }.
Ya sólo quedaría automatizar el análisis de todos los
atributos que vengan definidos en una misma cadena:
def
parseAttr(str)
ret = Attributes.new
getTokens(nil,str).delete_if{|thing|
thing==:ignore}.each do |hash|
ret.copyHash(hash)
end
return ret
end
|
Para empezar, se crea un objeto de tipo Attributes (recuerda,
aquella subclase de Hash que no distingue si sus índices están en mayúsculas o
minúsculas).
Después se extraen todos los tokens de la cadena. El
resultado es un Array con los elementos obtenidos, que podrán ser de tipo :ignore o bien hashes con atributos. Al resultado se le aplica el
método “delete_if”.
delete_if elimina de un Array aquellos elementos que cumplan
una determinada condición, la cual se puede especificar mediante un bloque de
código que, en nuestro caso es
{|thing| thing==:ignore}
|
O sera, se eliminan los elementos que sean igual a :ignore.
Descartados los elementos a ignorar, nos quedará una serie de hashes
{nombre_de_atributo => valor} que iremos añadiendo a la variable “ret”.
Terminado el proceso, ret contendrá los atributos en forma de
hash de clase Attributes.
Ya tenemos la clase AttrParser.
Y así nos quedó
# Analizador de atributos de un elemento
class
AttrParser < Tokenizer
def initialize(html = '')
super(html) #
Crear las componentes básicas del Tokenizer
#
Expresiones regulares a reconocer
spaces
= '[[:space:]]*'
name =
'(?<attr>[^[[:space:]]=\'"][^[[:space:]]=]*)'
equal = spaces + '=' +
spaces
string1 =
"'(?<val>[^']*)'"
string2 =
'"(?<val>[^"]*)"'
literal =
'(?<val>[^[[:space:]]=\'"][^[[:space:]]=]*)'
# Reglas para
analizar los atributos
addRule(name,
:attribute)
addRule(name+equal+string1,
:attribute)
addRule(name+equal+string2,
:attribute)
addRule(name+equal+literal,
:attribute)
# Reglas para
cosas a ignorar
addRule(spaces, :ignore)
addRule(string1,
:ignore)
addRule(string2,
:ignore)
end
#
Analizar una cadena con definición de atributos
def parseAttr(str)
ret = Attributes.new
getTokens(nil,str).delete_if{|thing|
thing==:ignore}.each do |hash|
ret.copyHash(hash)
end
return ret
end
private
# Atributos
def attribute(x)
val =
x.names.include?("val") ? x[:val] : 1
return
{x[:attr]=>val}
end
# Cosas a ignorar
def ignore(x)
return :ignore
end
end #class AttrParser
|
No hay comentarios:
Publicar un comentario