Bueno. Ya tenemos nuestra clase para
analizar código HTML. Pero... ¿para qué sirve?
Pues entre otras cosas, para poder
procesar archivos HTML de forma sencilla.
Pongamos un ejemplo: es frecuente que
los documentos HTML hagan referencia a datos almacenados en otros
ficheros. Como una etiqueta “IMG” con su campo “SRC”. O como
un “IFRAME”. O como una especificación de hoja de estilos
mediante un “LINK” con su correspondiente “HREF”. Por
ejemplo:
<html>
<head><title>Prueba</title></head>
<body>
<h1>Colores</h1>
<table border="1">
<tr><td>Rojo</td><td><img
src="rojo.png"></td></tr>
<tr><td>Azul</td><td><img
src="azul.png"></td></tr>
<tr><td>Verde</td><td><img
src="verde.png"></td></tr>
</table>
</body>
</html> |
Para mostrar las imágenes “rojo.png”,
“azul.png” y “verde.png”, nuestro navegador necesita procesar
otros ficheros. Existe, sin embargo, un esquema, un tipo, de URI que
permite indicar el contenido de los datos externos directamente
dentro del documento HTML. Se trata de las URI “data”, de las que
en su día traté en relación con cierta vulnerabilidad en
y en
Resumiendo, para insertar una imagen en
formato JPEG, cuyo tipo MIME asociado es “image/jpeg”, se podría
usar una URIs como:
data:image/jpeg;base64, /9j/4AAQSkZJRgABAQAAAQABAAD... |
… donde lo que sigue a “base64,”
es la representación en Base64 del contenido de la imagen. De este
modo, podríamos escribir en un documento HTML algo del tipo:
<img src='data:image/jpeg;base64,
/9j/4AAQSkZJRgABAQAAAQABAAD...'> |
Pues, venga. Vamos a hacer un programa
que nos convierta todas las referencias a ficheros externos en URIs
de tipo “data”.
¿Qué necesito?
Para analizar los documentos HTML ya
tengo mis clases. Pero aún no tengo nada para convertir ficheros en
su representación en Base64.
No te preocupes. Ruby trae eso “de
serie”. Y para utilizarlo, sólo hay que poner una cosita al
principio de nuestro programa
El fichero “base64.rb”, que Ruby
sabrá dónde lo guarda, no contiene una clase, sino un “MÓDULO”.
Un módulo es un conjunto de métodos y otras definiciones. Algo así
como una librería.
Y uno de los métodos definidos en el
módulo “Base64” hace exactamente lo que queremos: convertir una
cadena a Base64. Se trata de Base64.strict_encode64:
irb(main):001:0> require 'base64.rb'
=> true
irb(main):002:0> a = Base64.strict_encode64('Hola. ¿Cómo
estás?')
=> "SG9sYS4gwr9Dw7NtbyBlc3TDoXM/"
irb(main):003:0>
|
Fíjate en que para llamar a los
métodos de un módulo hay que ponerles antes el nombre del módulo y
un punto: Base64.strict_encode64
Creando nuestro propio módulo
Para que nos sirva de ejemplo, vamos a
crearnos un módulo muy sencillito. El fichero que lo contiene se
llamará “mime.rb” y contendrá métodos para determinar el tipo
MIME de las cosas. Su contenido será:
module MimeCheck
def extensionToMime(uri)
extension = uri.split('.').last
return case extension.downcase
when 'htm' then 'text/html'
when 'html' then 'text/html'
when 'php' then 'text/html'
when 'chm' then 'text/html'
when 'asp' then 'text/html'
when 'aspx' then 'text/html'
when 'jpg' then 'image/jpg'
when 'jpeg' then 'image/jpg'
when 'png' then 'image/png'
when 'gif' then 'image/gif'
when 'pdf' then 'application/pdf'
when 'js' then 'application/javascript'
when 'swf' then 'application/shockwave-flash'
when 'css' then 'text/css'
else 'unknown/unknown'
end
end
end # module MimeCheck |
Como puede observarse, la definición
del módulo comienza con la palabra “module”, seguida del nombre
del módulo y termina, como casi siempre, con “end”
Nuestro módulo sólo tiene un método,
“extensionToMime” que trata de adivinar el tipo MIME de un
archivo a partir de su extensión, que se determina a partir de la
ruta en la línea:
extension = uri.split('.').last |
Donde “uri” es una cadena con la
ruta. El método “split” divide esta cadena en trozos y crea un
array con ellos. El parámetro pasado a “split”, '.', le indica
que los trozos que queremos obtener están separados en la cadena
original por el carácter punto (.).
De todos estos trozos, nos quedamos con
el último con “last”. Y ya se sabe que lo que sigue al último
punto en el nombre de un archivo es su extensión...
Eso es todo. Ahora, cuando deseemos
usar la función “extensionToMime”, aparte de hacer un require
del fichero “mime.rb”, tendremos que escribir
“MimeCheck.extensionToMime”.
¿Te parece demasiado? Si escribir
mucho te gusta poco o nada, te comprendo. Y parece que también te
comprendió en su día el creador de Ruby, porque hay una instrucción
muy apañada: “include”. Puedes poner en tu programa
… y a partir de ahí, usar las
funciones del módulo MimeCheck directamente, como en:
mime = extensionToMime(file) |
¡Hala! A escribir
Vamos ya con nuestro programa. El
fichero que lo contiene se llamará “poc.rb”. Y comenzará con:
require "base64.rb"
require File.dirname(__FILE__) + '/htmlparser.rb'
require File.dirname(__FILE__) + '/mime.rb'
include MimeCheck
|
Hora de escribir el método que
analizará y procesará los documentos HTML. Lo primero es decidir
qué etiquetas y atributos vamos a modificar. Por ejemplo, los “HREF”
de los “LINK”. O los “SRC” de las “IMG”. O los “SRC”
de los “SCRIPT”. O...
Para que sea fácil modificarlo, vamos
a crear una variable que apunte a un array. Los elementos de este
array tendrán dos elementos: Un nombre de etiqueta y un nombre de
atributo a procesar para esa etiqueta:
# Cada par [tag, attr] indica que se desea convertir a data:
# el atributo "attr" de las etiquetas "tag"
$targets = [ ['LINK', 'HREF'], ['SCRIPT','SRC'], ['IMG','SRC'],
['IFRAME','SRC'], ['OBJECT','DATA'] ]
|
¡Quieto Parao! ¿Qué ese dólar?
Sí. Hay un dólar al inicio del nombre
de la variable “$targets”. Ese dólar quiere decir que la
variable es global. Que está accesible para los métodos de nuestro
programa. Por lo demás, podemos utilizarla como ya venimos haciendo
con otros tipos de variables: se le asigna valor y se utiliza después
Procesar el fichero HTML
El método que procesa el fichero HTML
es “dataHTML”:
def dataHTML(file, dirbase = '')
if not File.readable?(file)
return ''
end
# Si no se proporciona directorio base, tomar el del fichero a
procesar
base = (dirbase == '' ? File.dirname(File.absolute_path(file))
: dirbase)
# Valor a retornar
res = ''
# Convertir el fichero en representación de HTML
# Y modificar las etiquetas y atributos indicados en $targets
HTMLParser.new(File.read(file)).parseHTML.each
do |element|
if element.class.name == 'HTMLTag'
$targets.select{|t| element.name == t[0] and
element.attributes.has_key?(t[1])}.collect{|s| s[1]}.each do
|attribute|
# Determinar fichero a convertir a data:
attrfile = element.attributes[attribute]
if attrfile != ''
if attrfile[0] != '/' and
not(attrfile.match(/^[A-Za-z]:\\/))
attrfile = base + '/' + attrfile
end
# Convertir el fichero en data:
element.attributes[attribute] = data(attrfile, base)
end
end
end
# Añadir a la salida la reperesentación del elemento
res += element.to_s
end
return res
end
|
Al principio, realizamos alguna
comprobación sobre el fichero que nos pasan como argumento. Con
“File.readable?” determinamos si es posible leer el fichero. Con
“File.absolute_path” convertimos una ruta de archivo relativa en
una ruta absoluta. Poco más.
El meollo está en
HTMLParser.new(File.read(file)).parseHTML.each
do |element|
if element.class.name == 'HTMLTag'
$targets.select{|t| element.name == t[0] and
element.attributes.has_key?(t[1])}.collect{|s| s[1]}.each do
|attribute|
|
La primera línea abre el fichero para
lectura (r) en modo binario (b) y lee todo su contenido
(File.read(file)) y se lo pasa a un objeto de tipo
HTMLParser para que lo analice con “parseHTML”. Eso nos retornará
un array de los elementos que componen el documento.
De ellos, sólo nos interesan los que
sean de la clase HTMLTag. Las etiquetas. Los demás no los vamos a
tocar.
Y en la tercera línea...¡Vaya, qué
larga! ¡Posiblemente te aparezca partida en dos...! Para cada etiqueta de nuestro documento, cogemos el array
“$targets” y seleccionamos de él aquellas parejas cuyo primer
elemento (el nombre de etiqueta) sea igual al nombre de nuestra
etiqueta y cuyo segundo elemento (el nombre del atributo) esté entre
las claves del hash de atributos de nuestro elemento. Y con “collect”
nos quedamos con el nombre del atributo (el segundo elemento de la
parejita).
De ese modo, tenemos una lista de los
atributos de nuestra etiqueta que tenemos que procesar. Le ponemos el
“each” de turno y el bloque de código de qué hay que hacer con
ellos:
# Determinar fichero a convertir a data:
attrfile = element.attributes[attribute]
if attrfile != ''
if attrfile[0] != '/' and not(attrfile.match(/^[A-Za-z]:\\/))
attrfile = base + '/' + attrfile
end
# Convertir el fichero en data:
element.attributes[attribute] = data(attrfile, base)
end
|
El contenido de dicho atributo debe ser
una ruta de fichero. Si no es una cadena vacía, comprobamos si la
ruta es relativa y, de serlo, la pasamos a absoluta poniéndole
delante el directorio base. Observa que en la expresión regular:
…, que trata de bucar cosas como
“c:\” o “D:\”, hay dos cosas reseñables:
Que al principio hay un carácter
“^”. Cuando nos encontramos con este carácter fuera de una
categoría (fuera de corchetes) su significado es distinto del que
hemos visto en otras ocasiones: En este caso significa “al
principio de la cadena o al principio de una línea”.
Que para indicar una barra
invertida, “\”, ponemos dos “\\”. Recuerda que la barra
invertida le quita los significados especiales a los caracteres que
lo tienen. Es como la Kryptonita para Superman. Así que, para que
una barra invertida pierda su poder de neutralizar lo que sigue, le
ponemos delante... otra barra invertida.
Y, directamente, convertimos el
contenido del atributo en una URI de tipo “data” llamando a la
función “data”:
def data(file, dirbase)
# Determinar el tipo del fichero
mime = extensionToMime(file)
x = 'data:' + mime + ';base64,'
if mime == 'text/html'
return x + Base64.strict_encode64(dataHTML(file))
else
return x + (File.readable?(file) ?
Base64.strict_encode64(open(file,"rb").read) : '')
end
end
|
Relativamente sencilla. Primero se
determina el tipo MIME del fichero. Si el documento es de tipo HTML,
llama de forma recursiva a dataHTML y convierte el resultado obtenido
en Base64 y se crea con lo que obtenemos una URI de tipo data.
En caso contrario, se codifica en
Base64 el contenido y se crea la URI data.
Pues ya está casi todo.
Haciéndolo andar
Bueno, ya sólo falta que el programa
sepa qué fichero contiene el documento HTML que hay que convertir y
dónde tiene que guardar los resultados. Para ello se utilizarán
parámetros de línea de comandos e invocaremos nuestro programa así:
ruby poc.rb fichero_de_entrada fichero_de_salida |
El programa puede acceder a los
parámetros de línea de comandos mediante una variable de tipo Array
predefinida llamada “ARGV”. Así,
.. será el primer parámetro,
… será el segundo. Y así
sucesivamente.
Lo primero es comprobar si nos han
pasado dos parámetros:
if ARGV.length != 2
print "Uso: #{$0} fichero_entrada fichero_salida\n\n"
else
|
Observa esa cadena, que es rara de
coj@#!!&% Y es que dentro de una cadena encerrada entre comillas
dobles podemos insertar el resultado de evaluar una expresión. Para
eso se pone una almohadilla “#” seguida de la expresión a
evaluar encerrada entre llaves (“{“ y “}”).
En este caso, la expresión a evaluar
es “$0”. Ésta es una variable global predefinida que nos
proporciona el nombre del fichero que contiene el programa.
En definitiva, si no le pasamos dos
argumentos, nuestra fiera se queja:
$ ruby poc.rb
Uso: poc.rb fichero_entrada fichero_salida
$
|
Ya sólo queda, identificar los
ficheros que tenemos que utilizar:
# Determinar ficheros a utilizar
input_file = ARGV[0]
if not File.readable?(input_file)
raise "No se puede leer #{input_file}\n"
end
output_file = ARGV[1]
|
… y procesar el fichero de entrada y
guardar el resultado en el fichero de salida:
output = open(output_file, "wb")
output.write(dataHTML(input_file))
output.close
|
El fichero de salida se abre con open
para escritura (w) en modo binario (b). Y se escribe su contenido (el
resultado de procesar el fichero de entrada) con “write”.
Finalmente, se cierra el fichero con “close”.
Y ya está.
El fichero poc.rb quedará...
require "base64.rb"
require File.dirname(__FILE__) + '/htmlparser.rb'
require File.dirname(__FILE__) + '/mime.rb'
include MimeCheck
# Cada par [tag, attr] indica que se desea convertir a data:
# el atributo "attr" de las etiquetas "tag"
$targets = [ ['LINK', 'HREF'], ['SCRIPT','SRC'], ['IMG','SRC'],
['IFRAME','SRC'], ['OBJECT','DATA'] ]
# Convierte elementos externos de un fichero HTML en elementos
data:
# Parámetros
# file: fichero con código HTML
# dirbase: directorio a utilizar como base para rutas
relativas
def dataHTML(file, dirbase = '')
if not File.readable?(file)
return ''
end
# Si no se proporciona directorio base, tomar el del fichero a
procesar
base = (dirbase == '' ? File.dirname(File.absolute_path(file))
: dirbase)
# Valor a retornar
res = ''
# Convertir el fichero en representación de HTML
# Y modificar las etiquetas y atributos indicados en $targets
HTMLParser.new(open(file,"rb").read).parseHTML.each
do |element|
if element.class.name == 'HTMLTag'
$targets.select{|t| element.name == t[0] and
element.attributes.has_key?(t[1])}.collect{|s| s[1]}.each do
|attribute|
# Determinar fichero a convertir a data:
attrfile = element.attributes[attribute]
if attrfile != ''
if attrfile[0] != '/' and
not(attrfile.match(/^[A-Za-z]:\\/))
attrfile = base + '/' + attrfile
end
# Convertir el fichero en data:
element.attributes[attribute] = data(attrfile, base)
end
end
end
# Añadir a la salida la reperesentación del elemento
res += element.to_s
end
return res
end
# Convierte un fichero en data:
# Parámetros igual que en dataHTML
def data(file, dirbase)
# Determinar el tipo del fichero
mime = extensionToMime(file)
x = 'data:' + mime + ';base64,'
if mime == 'text/html'
return x + Base64.strict_encode64(dataHTML(file))
else
return x + (File.readable?(file) ?
Base64.strict_encode64(open(file,"rb").read) : '')
end
end
# Comprobar que se han pasado dos parámetros por línea de
comandos
if ARGV.length != 2
print "Uso: #{$0} fichero_entrada fichero_salida\n\n"
else
# Determinar ficheros a utilizar
input_file = ARGV[0]
if not File.readable?(input_file)
raise "No se puede leer #{input_file}\n"
end
output_file = ARGV[1]
# Realizar los cambios y guardar los resultados
output = open(output_file, "wb")
output.write(dataHTML(input_file))
output.close
end |
Y a usarlo
¿Recuerdas el fichero HTML que pusimos
de ejemplo al principio? No es que sea demasiado impresionante,
pero, en conjunto, quedaría algo así:
El fichero se llama "1.html" y está en el directorio "pagina". Vamos a convertirlo con nuestro
programita:
$ ruby poc.rb pagina/1.htm salida.htm
|
En unos instantes habrá acabado y
podremos abrir el fichero de salida
Sí. Tiene la misma pinta que el original. Pero si lo miramos por dentro, comprobaremos que las imágenes están
puestas como URIs de tipo data.
¡Tachaaaaaaaaán!