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:

  1. 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.
  2. END { }. Se ejecuta una sola vez, pero después de que los datos se hayan procesado.
  3. { }. 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 como cat archivo_muy_grande.csv | cut -f 2, cuando esto se tiene que reemplazar con cut -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 con mktime().
  • 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 de inicio y final, 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:

  1. Cambiar el separador del archivo.
  2. Reemplazar nombres de host de un archivo.
  3. 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.