…esto no es un subtítulo…
2009-10-03
Seguimos con *nix en casos prácticos. Esta vez vamos a preparar un pequeño programita para filtrar el contenido de una larga lista. El caso está basado en uno real en el que un compañero tuvo que filtrar una larguísima lista de ficheros con resultados de un experimento. Se trata del típico problema con el que se encuentra alguien que tiene que manejar una gran cantidad de datos y seleccionar una parte.
Tenemos un fichero de texto simple que contiene muchísimas líneas.
Cada línea es un elemento de una lista. Cada línea o elemento de la
lista tiene dos palabras separadas por un guión: un nombre y un
número. Puede haber muchas líneas con el mismo nombre, pero el número
no se repite. La lista tendría que estar ordenada con el segundo campo,
el número, pero nos ha llegado desordenada. Un fragmento típico de la
lista tiene el siguiente aspecto:
QUUX-000064
WOMBAT-000053
FOOBAR-000022
Hay que filtrar la lista. Hay varios criterios que dependen del nombre que tenga cada línea: para algunos nombres, tenemos que sacar uno de cada varios (si es uno de cada uno, todos); para otros nombres, hay que sacar los primeros elementos (los cuatro primeros, los cinco primeros…); finalmente, para otros nombres, hay que sacar los últimos elementos (los dos últimos, los tres últimos…).
Sería conveniente definir las instrucciones, las reglas sobre qué hay
que filtrar para cada nombre, en un pequeño ficherito. Así podremos
separar la lógica general (cómo se filtra) de la particular (qué se
filtra). La sintaxis será sencilla; el fichero con las instrucciones
tendrá varias líneas, cada una con el siguiente contenido:
nombre regla parámetro
El nombre es el nombre a buscar; la regla será una
entre every
(para la regla de uno de cada varios),
first
(para coger los primeros elementos) y
last
(para coger los últimos elementos); y el
parámetro será la frecuencia con la que sacaremos elementos
de la lista para la regla every
, el número de primeros
elementos para la regla first
y el número de últimos elementos
para la regla last
. Por ejemplo, para sacar un elemento de
cada cuatro para el nombre FOOBAR, los tres primeros elementos de nombre
WOMBAT y los cinco últimos elementos de nombre QUUX, el fichero de reglas
tendría el siguiente aspecto:
FOOBAR every 4
WOMBAT first 3
QUUX last 5
Definido esto, ahora tenemos que crear nuestro filtro. De todas las formas que hay de hacerlo, vamos a usar una que es muy poco eficiente desde el punto de vista computacional, pero que nos llevará tan poco tiempo que merecerá la pena: leeremos el fichero con las reglas línea por línea y filtraremos la lista completa cada vez. Podemos hacer esto muy rápidamente con las herramientas de texto de *nix. Como vamos a filtrar muy pocas listas y el ordenador no tarda mucho en terminar su tarea, una solución más sofisticada y eficiente desde el punto de vista computacional pero más lenta de elaborar está en clara desventaja.
Leeremos el fichero de reglas. Para cada línea, extraeremos el nombre, la regla y el argumento numérico. Después, dependiendo de la regla que tengamos, aplicaremos un filtro u otro. Para filtrar, lo primero que haremos será ordenar la lista y después aplicaremos los filtros correspondiente. El programa admitirá dos argumentos: el nombre del fichero con las reglas y el nombre del fichero con la lista. Sacará sus resultados por su salida estándar.
El programa será un script de la shell. Leeremos el
fichero con las reglas línea por línea en un bucle. De cada línea
extraeremos los campos (nombre, regla y argumento numérico) con una
herramienta sencilla para extraer campos (cut).
Después, ordenaremos la lista con sort y filtraremos el
resultado con grep para sacar las líneas con el nombre
que estamos buscando. La salida de grep irá dirigida al
filtro apropiado para cada regla. Los filtros first
y
last
son fáciles de aplicar con las herramientas
head y tail, respectivamente. El filtro
every es un poquito más complicado, pero podemos hacerlo
cómodamente con awk. El resultado final puede tener un
aspecto como el siguiente:
#!/bin/sh
# En primer lugar, obtenemos los nombres de los ficheros de
# los argumentos.
RULES_FILE="$1"
LIST_FILE="$2"
# Leemos el fichero de reglas línea por línea.
cat "$RULES_FILE" | while read line; do
# Extraemos los campos de la línea.
name=`echo $line | cut -d' ' -f1`
rule=`echo $line | cut -d' ' -f2`
argument=`echo $line | cut -d' ' -f3`
# Ordenamos numéricamente según el campo 2
# usando el guión como separador. La
# salida ordenada pasa por grep, que busca
# las líneas que empiezan por el nombre
# especificado en la regla. La salida
# de grep va a los filtros finales.
sort -n -k2 -t- "$LIST_FILE" | grep "^$name" | case $rule in
# Para sacar las primeras incidencias.
first) head -n $argument
;;
# Para sacar las últimas incidencias.
last) tail -n $argument
;;
# Para sacar una incidencia de cada
# tantas.
every) awk 'BEGIN { N = '$argument' }
(NR % N) == 1 { print }'
;;
esac
done
¡Ya está! Si ignoramos los comentarios y las líneas en
blanco, podemos decir que hemos creado el filtro en 17 líneas.
Si hemos llamado al script filterit
, al
fichero de reglas rules
y al fichero con la lista
list
, podemos hacer lo siguiente para tener el
resultado en un fichero, results
:
./filterit rules list > results
Para hacer pruebas, aquí hay ficherillos útiles:
Categorías: Informática
Permalink: https://sgcg.es/articulos/2009/10/03/un-pequeno-filtro/