10 minutos
Procesar Archivos CSV desde la CLI
Introducción
Saber procesar archivos separados por comas del inglés (Comma
Separated Value) es de gran ayuda cuando trabajamos con datos,
y si lo sabemos hacer desde la línea de comandos o CLI (Command
Line Interface) que mejor. En una ocasión me tocó trabajar con
archivos CSV
tan grandes que Libreoffice Calc se quedaba colgado a
la hora de intentar abrirlo; Excel sí lo abría pero cualquier
operación (por sencilla que fuera) se tardaba al menos cinco
minutos. La otra opción que tenía era cargar la información a una base
de datos, pero necesitaba analizar la información para crear una tabla
donde hacer el dump, y crear índices y otras cosas para que se pudiera
trabajar. ¡Es entonces cuando la CLI
vino al rescate!
¿Qué es un archivo CSV
Es un archivo de texto plano con información en columnas, cada columna está separada de otra usando un carácter específico, tradicionalmente una coma (de ahí su nombre), pero en la práctica puede ser cualquier carácter, incluso el tabulador. Las herramientas que vamos a usar requieren que se especifique el carácter de separación.
Analizando la información del archivo
Lo primero que tenemos que hacer es ver la información que tiene el
archivo para determinar que operaciones realizaremos. Como los
archivos pueden ser muy grandes, abrirlos en un editor de texto no es
una opción (aunque Emacs tiene un modo llamado vlf
, por Very
Large Files).
¿Cuántas líneas tiene mi archivo?
Simplemente hay que ejecutar el comando wc -l archivo_muy_grande.csv
.
Ver las primeras líneas del archivo
Para mostrar las primeras n líneas del archivo tenemos el comando
head
. Su uso es muy sencillo:
head -n 25 archivo_muy_grande.csv
donde 25
es el número de líneas del inicio que queremos mostrar. Si
omitimos el modificador -n
por default se mostrarán las primeras 10
líneas. Algo que hay que tener muy en cuenta es que el tipo de fin
de línea del archivo es muy importante, si quieres saber por que lee
el post Convertir finales de línea.
Ver las últimas líneas del archivo
El comando tail
funciona exactamente igual que head
, con la única
diferencia que nos mostrará las últimas n líneas del archivo:
tail -n 25 archivo_muy_grande.csv
Procesando la información
Ahora que ya sabes que estructura tiene el archivo, podemos empezar a trabajar con el. Existen varias formas de obtener el mismo resultado, yo les mostraré lo que mejor me ha funcionado.
Limpiando la información
En la mayoría de las veces no necesitamos trabajar con todas las columnas de nuestro archivo, por lo que es buena idea eliminar las columnas innecesarias para que el procesado sea más ligero.
cut
Este comando nos permite indicar que columnas son las que queremos y al procesar el archivo imprime en la salida estándar dichas columnas, por lo que es necesario redirigir la salida a un archivo o usar una tubería para que otra herramienta procese los datos. Redirigir la salida es muy simple:
cut -d "," -f 1,3,4 archivo_muy_grande.csv >archivo_resumido.csv
En este caso -d
es para indicar el tipo de separador del archivo y
","
indica que el separador es una coma. Las comillas no son
necesarias pero es una buena práctica. El modificador -f
especifica
el número de columnas a seleccionar, y su valor puede ser un solo
número, una lista (como se usó en el ejemplo), o un rango.
Tipo | Descripción |
---|---|
N |
Una columna única. La columna inicial es 1 |
N- |
Selecciona todas las columnas a partir de N |
N-M |
Selecciona un rango desde la columna N hasta M , inclusive |
-M |
Selecciona desde la primera columna a M , inclusive |
Es posible combinar todas las opciones de la tabla, por ejemplo, si quiero las tres primeras columnas, la quinta columna y las columnas siete y ocho, lo puedo hacer con:
cut -d "," -f -3,5,7-8 archivo_muy_grande.csv
# si 7 y 8 son las últimas columnas:
cut -d "," -f -3,5,7- archivo_muy_grande.csv
awk
Mas que un comando, awk
es un lenguaje de programación orientado a
texto. Uno de sus creadores es Brian Kernighan, fue quien escribió el
famoso libro El lenguaje de programación C. Los programas awk
se
pueden escribir como un parámetro a la hora de ejecutar awk
, o si el
programa es un poco más largo, entonces se puede guardar en un archivo
por separado. Al igual que cut
, awk
trabaja con las columnas de
nuestro archivo pero lo hace de una forma un poco diferente.
Estructura de un programa
Un programa awk
tiene tres partes:
BEGIN { }
. Se ejecuta una sola vez, al inicio del programa, útil si queremos agregar un texto al inicio de los datos procesados, o para indicar variables que usarán los otros bloques.END { }
. Se ejecuta una sola vez, pero después de que los datos se hayan procesado.{ }
. Cuerpo. Contiene las instrucciones que trabajarán con el contenido del archivo.
Las secciones BEGIN
y END
son opcionales.
Columnas
Las columnas se identifican con la notación $n
donde n es un
número que especifica una columna (la primera columna empieza en
uno). La columna $0
es especial, por que realmente no hace
referencia a una columna, pero sí a toda la línea procesada. awk
no
tiene rangos de filas, pero se pueden simular usando funciones RE
y/o ciclos, pero esto está fuera del alcance de este post. Si queremos
reproducir el ejemplo anterior con awk
tenemos que especificar cada
una de las columnas:
awk -F "," '{ print $1, $2, $3, $5, $7, $8 }' archivo_muy_grande.csv
Nota: el comando
cat
es de los más incomprendidos, ya que su función es concatenar dos archivos y el resultado de esta concatenación se muestra en la salida estándar; si solo se especifica un archivo,cat
manda todo el contenido a esta salida. Es por esto que muchas veces vemos casos comocat archivo_muy_grande.csv | cut -f 2
, cuando esto se tiene que reemplazar concut -f 2 archivo_muy_grande.csv
.
En este caso en particular resulta más cómodo usar cut
, sin embargo,
donde brilla awk
es en analizar los datos para decidir si se
deben de incluir una línea en particular. Continuando con el mismo
ejemplo, ahora no solo nos interesan las mismas columnas, pero también
queremos únicamente las líneas donde la primera columna sea igual a
foobar
:
awk -F "," '$1 == "foobar" { print $1, $2, $3, $5, $7, $8 }' archivo_muy_grande.csv
Si lo único que queremos es filtrar unas líneas pero mantener el archivo completo, lo podemos hacer así:
awk -F "," '$1 == "foobar" { print $0 }' archivo_muy_grande.csv
Como ya comenté, $0
significa “toda la línea”. También he estada
usando -F
para indicar que mi archivo está separado por una coma,
pero puedo usar cualquier carácter, como un pipe: -F "|"
.
Al realizar comparaciones tenemos una variedad de funciones
disponibles a
nuestra disposición. Si queremos filtrar a las líneas que en la
segunda columna contenga la cadena xyz
en cualquier parte, usamos la
función index()
la cual regresa un entero > 1
si encuentra una
coincidencia, y cero si no la encuentra:
awk -F "," 'index($1, "xyz") >= 1 { print $0 }'
incluso si nuestro archivo tiene cadenas que representen fechas, podemos filtrar a las líneas que se encuentren dentro de un rango de fechas. Esto lo vamos a implementar en un programa:
function crea_fecha(fecha) {
split(fecha, datos, "-");
return mktime(sprintf("%s %s %s 0 0 0", datos[1], datos[2], datos[3]));
}
{
horario = mktime("2020 03 6 0 0 0");
inicio = crea_fecha($2);
final = crea_fecha($3);
if (horario >= inicio && horario <= final) print $0
}
y esto lo podemos ejecutar así:
awk -F "," -f valida_fechas.awk mi_archivo_grande.csv
Analicemos que hace este archivo:
- Supongamos que nuestro archivo tiene dos fechas en las columnas dos y
tres, y están en el formato
YYYY-mm-dd
. - La fecha que usaremos como filtro es
2020-03-06
. Esto lo convertimos a un timestamp conmktime()
. - La función
crea_fecha()
descompone en tokens la cadena con la fecha para poder construir un timestamp. - Por último, validamos que la variable
horario
esté dentro del rango de los timestamps deinicio
yfinal
, si se cumple la validación, imprimimos toda la línea.
grep/egrep
Si lo único que necesitamos es filtrar las líneas que contengan un
solo texto, pero sin importarnos el concepto de columnas, podemos
usar egrep
. Si queremos todas las líneas que contengan el texto
2020-05-04
:
egrep "2020-05-04" mi_archivo_grande.csv
head y tail
Si solo nos interesa quitar unas cuantas líneas del inicio o del final
del archivo, head
y tail
también lo pueden hacer.
Eliminar líneas del final
# Elimina la última línea del archivo
head -n -1 mi_archivo_grande.csv
Esto lo podemos leer como muestra las líneas desde el inicio hasta el
final - 1. Podemos reemplazar -1
por cualquier número para eliminas
N líneas al final.
Eliminar líneas al inicio
tail -n -1 mi archivo_grande.csv
La interpretación de esto es: muestra todas las líneas a partir de inicio - 1 hasta el final.
Transformando la información
Hasta ahorita hemos visto como se puede filtrar el archivo para obtener un subconjunto de datos que nos interese, pero sin alterar la información de las columnas. Ahora veremos como transformar la información. Vamos a usar cuatro ejemplos:
- Cambiar el separador del archivo.
- Reemplazar nombres de host de un archivo.
- Usar una expresión regular para insertar
****
en los datos de una tarjeta de crédito.
Cambiar el separador
La transformación más sencilla es cambiar el carácter separador del archivo. Esto es útil si los datos los va a procesar un sistema que no podemos modificar y espera los datos con un separador en particular. Supongamos que nuestro archivo está separado por comas pero necesitamos cambiar las comas por pipes.
# Usando cut
cut -d "," -f 1,3,4 --output-delimiter="|" archivo_muy_grande.csv
# Usando awk
awk -F "," 'BEGIN { OFS="|" } { print $1, $2, $3, $5, $7, $8 }' archivo_muy_grande.csv
Reemplazar nombres de host
Aunque este ejemplo no utiliza un archivo CSV, igual se puede utilizar con uno. Voy a tomar de ejemplo un archivo de configuración de apache:
<VirtualHost *:80>
ServerAdmin [email protected]
ServerName local.example.com
ServerAlias www.local.example.com
DocumentRoot /var/www/operaciones
ErrorLog ${APACHE_LOG_DIR}/local.example.com.log
</VirtualHost>
y me interesa reemplazar todas las ocurrencias del dominio
local.example.com
por test.org
:
sed 's/local.example.com/test.org/g' local.example.com.conf >test.org.conf
Este comando se compone cuatro partes:
s/
indica que tiene que buscar un texto.local.example.com
es la cadena a buscar.test.org
es la cadena a reemplazar./g
indica que debe de reemplazar el texto en todas las ocurrencias del archivo.
Reemplazar los datos de una tarjeta de crédito
Vamos a ver dos formas de hacer esto, una considerando que el número
de la tarjeta está en una columna, y otra donde buscaremos en
cualquier parte del archivo. El formato del número de la tarjeta es
NNNN-NNNN-NNNN-NNNN
donde N
es un número entre cero y nueve, y lo
que queremos es reemplazar los tres primeros bloques de números por
asteriscos.
Buscando en una columna
awk -F "," { print $1, $2 sprintf("****-****-****-%s", substr($3, 16)), $4 }' archivo_muy_grande.csv
Buscando en todo el archivo
Aquí tenemos que crear una expresión regular con grupos para poder referenciar el último grupo en la parte de la sustitución:
sed -E "s/(([0-9]{4}-){3})([0-9]{4})/****-****-****-\3/" fechas.csv
Los grupos se identifican con \N
, donde n es un número que
corresponde a un grupo en particular. En nuestro ejemplo estamos
haciendo referencia al tercer grupo, y como solo estamos poniendo este
grupo en la sustitución lo demás queda descartado.
Otras operaciones
Ordenando los datos
En este caso usaremos el comando sort
, y lo que tenemos que hacer es
indicar la o las columnas que usará para el ordenamiento. Si queremos
ordenar por la segunda columna:
sort -t "," -k 2 archivo_muy_grande.csv
Filtrando los duplicados
Si queremos conocer los valores únicos de una columna usamos el
comando uniq
. Como los datos deben de estar ordenados y solo vamos a
usar una columna, nos apoyaremos en sort
y cut
:
cut -d "," -f 2 | sort | uniq
Si lo que queremos ver son los valores repetidos:
cut -d "," -f 2 | sort | uniq -u
O para ver cuantas veces se repiten los duplicados:
cut -d "," -f 2 | sort | uniq -c
Conclusión
Como ya comenté, hay muchas formas de trabajar con los archivos, incluso las herramientas aquí presentadas tienen muchos modificadores y formas de uso, por lo que los invito a revisar la documentación de cada una de ellas para descubrir todo su potencial.