Limpiar imágenes no usadas de una web usando Powershell

Tengo que reconocer que hay dos cosas que me cuestan especialmente. Son dos cosas que considero muy útiles en el día a día y que de vez en cuando vuelvo a darles una oportunidad para ver si consigo dominarlas de una vez. Se trata de las expresiones regulares y los lenguajes de scripting, como por ejemplo Powershell.

Una de las últimas veces que estuve trasteando con Powershell acabé haciendo un script para descargar emails. Esta vez quería automatizar una tarea bastante aburrida: eliminar los archivos de imágenes que no estaban siendo referenciados desde una página web.

Cuando tienes una página web que va evolucionando con el tiempo, es fácil que en la carpeta de imágenes acaben quedando archivos que ya no se usan y que lo único que hacen es ocupar espacio en el servidor. Eliminarlos manualmente es bastante aburrido (en mi caso eran más de 500 imágenes) así que me pareció una oportunidad estupenda para practicar un poco Powershell.

El script

Este script no está pensado para ser reutilizado por nadie, pero si eres de los que han llegado hasta aquí desde google y sólo quieren ver el código, no me voy a enrollar mucho más pero ten en cuenta algunas limitaciones muy serias de este script:

  • Sólo busca imágenes referenciadas estáticamente. Si tienes imágenes que se referencian desde contenido generado dinámicamente, el script no las va a detectar y las borrará de disco.
  • No tiene en cuenta imágenes referenciadas desde CSS, así que las que también las borraría.
  • La expresión regular es muy básica. A mi me servía porque todas las imágenes las tenía referenciadas igual, pero si tu html no es tan homogéneo, seguramente fuese mejor idea usar algo como HTML Agility Pack, en lugar de expresiones regulares para parsear html (que ya sabemos que no es una buena idea)
$imgFolder = './img'
$referenced = Select-String -Path .\*.html -Pattern '<img src=\"(?<url>[^\"]*)\"' -AllMatches `
    | % {$_.Matches} `
    | % {$_.Groups["url"]} `
    | % {$_.Value} `
    | % {[IO.Path]::GetFileName($_)}

dir $imgFolder | where { -not ($referenced -contains [io.path]::GetFileName($_)) } | rm

Cómo funciona

OJO: Si sabes algo de Powershell esto no tiene ningún interés, pero tengo ciertas esperanzas de que si lo escribo tal vez consiga memorizar de una vez la sintaxis de powershell.

Lo primero que hacemos es obtener todas las imágenes referenciadas en los documentos html. Para ello usamos el cmdlet select-string, que es similar al grep de linux. Podemos pasarle un conjunto de ficheros y una expresión regular, y nos devuelve una lista de objetos MatchInfo con las coincidencias encontradas.

A partir de ahí, vamos refinando el formato de los resultados hasta quedarnos con el nombre del fichero. Hay varias formas de hacer esto, pero en este caso estamos usando el cmdlet Foreach-Object (a través de su alias %). Dentro del foreach podemos referenciar al elemento actual con la variable $_ y así operar con él, acceder a sus propiedades, etc.

Un par de cosas que se me suelen olvidar: para partir una línea en varias, se usa un ` (backquote), y para invocar métodos estáticos de clases de .NET, [clase]::método (puede ser necesario incluir parte del espacio de nombres).

Una vez que tenemos las imágenes referenciadas, lo que hacemos es obtener los archivos existentes en la carpeta imágenes y, usando el cmdlet Where-Object (a través de su alias where), quedarnos sólo con aquellas imágenes que existen en disco y no son referenciadas en los documentos html. Enlazando eso con la entrada del comando rm, las borramos de disco.

Conclusiones

Aunque el script me ha sido de mucha utilidad y me ha permitido borrar más de 200 imágenes que no se usaban, tiene limitaciones muy serias, como ya expliqué antes. Sin embargo, la filosofía de este tipo de scripts no debería ser hacerlos reutilizables, sino poder escribirlos en un par de minutos para hacer una tarea concreta y luego desecharlos.

Dado lo poco cómodo que me siento con Powershell, seguramente habría tardado menos tiempo en programarlo en C#, pero poder ejecutarlo sin necesidad de abrir el Visual Studio y crear un proyecto de consola es lo bastante interesante como para que merezca la pena intentar dominar un lenguaje de scripting para realizar este tipo de tareas.

3 comentarios en “Limpiar imágenes no usadas de una web usando Powershell

  1. Pingback: Lo mejor de la semana sobre desarrollo web en español vol. 8 | ADWE

  2. De cabeza y sin poder probarlo, diría que después de «Matches», los valores dejan de ser colecciones, y los «%» son un poco desperdicio (entubar un «foreach-object» a otro «foreach-object» es un poco como anidar bucles, e iterar en 1 sólo elemento cuando sabes que es solo 1 elemento… queda un poco raro).

    … | % {$_.Matches} `
    | % {[IO.Path]::GetFileName($_.Groups[«url»].Value)}

    Luego creo recordar que dir es un alias para get-child-items y al recorrer directorios, esos child-items tienen filename como propiedad con lo que puedes comparar los filenames directamente sin tener que referenciar los métodos estáticos más de la cuenta.

    dir $imgFolder | where { -not ($referenced -contains $_.filename) } | rm

    Último refinamiento: como el Value del MatchObject es un string, y el filename tiene metodo toString, otra posibilidad sería comparar los filenames como strings y nos ahorramos completamente referenciar a ningún método estático (bueno, hacer comparaciones de strings cuando puedes hacer comparaciones con el tipo correcto no es exactamente una práctica recomendada… ¡maldición! creo que veo demasiadas strings en el curro :D ).

    Cosas útiles para trabajar con powershell:

    – difumina en tu cabeza la diferencia entre lenguaje de programación y de script. No son tantas.

    – el setup de console2 que describe Hanselman aquí; http://www.hanselman.com/blog/Console2ABetterWindowsCommandPrompt.aspx

    — en console2 tener una pestaña con «help «comando o tópico»»

    — tener otra pestaña con ««cosa» | get-members»

Comentarios cerrados.