Tratamiento masivo de datos con AWK, alternativa parcial a ACL o SAS

GNU awk es una herramienta muy útil para modificar archivos, buscar y transformar datos y, en general, realizar cualquier tipo de tratamiento masivo de ficheros. Con un programa awk es posible contar el número de líneas de un archivo, seleccionar columnas, aplicar filtros, realizar cruces, borrar el último campo de cada línea, hacer sumarizaciones, comprobar duplicados, muestreos, etc.

Para aprender a utilizar AWK mediante esta pequeña guía, lo mejor es copiar los ficheros de ejemplo que aparecen y ejecutar las instrucciones o programas que se comentan para ver directamente cual es el resultado. Únicamente leyendo la guía es bastante más complicado entender el funcionamiento de AWK.

Índice de contenidos

Estructura y ejecución
Separadores de registros
Separadores de campos y columnas de tamaño fijo
Campos computados
Agrupación de acciones
Getline
Getline y ejecución de programas
Ficheros de salida
Expresiones
Conversiones de formato
Control del flujo
Arrays asociativos
Otras acciones/funciones
Funciones
Expresiones regulares
Rendimiento / Profiling
Nombres de columnas
Ordenar los registros de un fichero
Sumarizaciones
Cruces de ficheros (join match + unmatch)
Muestra aleatorias
Conexiones de red

Estructura y ejecución

Un programa awk presenta la siguiente estructura de reglas:

patrón { acción }
patrón { acción }
...

Los patrones corresponden a expresiones regulares (p.ej. /foo/) o condiciones (p.ej. $1 = “Jan”) y las acciones son funciones del lenguaje.

En una regla puede omitirse el patrón o la acción, pero no ambos. Si el patrón se omite, entonces la acción se realiza para todas las líneas. Si se omite la acción, la acción por defecto es imprimir las líneas que cumplan el patrón.

Fichero_A.txt

	Jan 13 25 15 115
	Feb 15 32 24 226
	Mar 15 24 34 228
	Apr 31 52 63 420
	May 16 34 29 208
	Jun 31 42 75 492
	Jul 24 34 67 436
	Aug 15 34 47 316
	Sep 13 55 37 277
	Oct 29 54 68 525
	Nov 20 87 82 577
	Dec 17 35 61 401
	Jan 21 36 64 620
	Feb 26 58 80 652
	Mar 24 75 70 495
	Apr 21 70 74 514

Por ejemplo, para las lineas que contienen “Jan” las imprimimos por pantalla (simulamos comando ‘grep’):

	awk '/Jan/ { print $0 }' Fichero_A.txt

Si quisiéramos únicamente imprimir la segunda columna utilizamos $1 (el resto las podemos encontrar en $2, $3, etc.):

	awk '/Jan/ { print $1 }' Fichero_A.txt

También podemos realizar sumarizaciones, por ejemplo, sumamos los valores de la quinta columna de las lineas que tengan en la primera columna “Jan”:

	awk '$1 == "Jan" { sum += $5 } END { print sum }' Fichero_A.txt

Como se puede observar, se han definido 2 reglas:

  • Si la primera columna es “Jan” entonces acumulamos el quinto valor en una variable.
  • Si es el final del fichero (END {…}), imprimimos el valor de la variable.

Es muy habitual ver ejemplos de AWK en una única línea, y justamente por ese motivo se ha ganado la fama de complejo. Pero en realidad podemos crear ficheros de texto con todas las instrucciones bien tabuladas y que permiten una mejor comprensión del objetivo del programa. Por ejemplo, podemos crear el fichero ‘test.awk’:

	#!/usr/bin/awk -f
	$1 == "Jan" { sum += $5 }
	END { print sum }

* Los comentarios dentro del programa awk se marcan con “#”.

Para ejecutarlo, utilizaremos la siguiente sentencia:

	awk -f test.awk Fichero_A.txt

O si el fichero tiene permisos de ejecución:

	chmod 755 test.awk
	./test.awk Fichero_A.txt

El primer argumento siempre corresponde al fichero sobre el cual vamos a ejecutar el programa. De hecho, desde el propio programa podemos acceder al listado de argumentos con el que se ha llamado:

	#!/usr/bin/awk -f
	BEGIN {
		print ARGC      # Número de argumentos
		print ARGV[0]   # Siempre "awk"
		print ARGV[1]   # Primer argumento
		print ARGV[2]   # Segundo argumento
	}

Incluso, podríamos modificar los argumentos desde el propio programa para que no sea necesario especificar los ficheros por linea de comandos:

	#!/usr/bin/awk -f
	BEGIN {
		ARGC = 2 # Uno más que el número de ficheros de entrada que indiquemos.
		ARGV[1] = "file1"
	}
	{ 
		print $0
	}

Separadores de registros

Awk realiza un tratamiento registro a registro. Por defecto los registros se encuentran delimitados por el final de línea \n y por tanto, un registro = una línea. No obstante, podemos cambiar el símbolo que denota el final de un registro a otro carácter:

    #!/usr/bin/awk -f
	BEGIN {
	        ARGC = 2 ; ARGV[1] = "Fichero_A.txt" 
	        RS = "1"
	}
	{ print $0 }

En el anterior programa, hemos cambiado la variable RS de forma que awk compondrá el primer registro leyendo todos los caracteres hasta encontrar un “1” y así sucesivamente.

¿Qué utilidad tiene este mecanismo? Por ejemplo, en caso de que tengamos un fichero donde los registros se encuentren separados por lineas en blanco como el siguiente:

Fichero_Cortado.txt

	Primer_registro dato1 dato2
	dato3 dato4

	Segudo_registro dato1 dato2

	Tercer_registro dato1 dato2 dato3

Podemos establecer RS = “”, entonces awk compondrá los registros desde el primer carácter hasta que encuentra una línea en blanco. Veámoslo con un ejemplo:

	#!/usr/bin/awk -f
	BEGIN {
	        ARGC = 2 ; ARGV[1] = "Fichero_Cortado.txt"
	        RS = ""
	}
	{ print FNR " " NR " " $1 " " $2 " " $3 " " $4 " " $5 } 

El anterior programa nos reformatearía el archivo a:

	1 1 Primer_registro dato1 dato2 dato3 dato4
	2 2 Segudo_registro dato1 dato2  
	3 3 Tercer_registro dato1 dato2 dato3 

Si os fijáis, hemos utilizado la variable FNR que indica el número de registro del fichero actual y NR que hace referencia al número de registro total (solo tiene sentido en caso de que hayamos utilizado varios ficheros de entrada).

Separadores de campos y columnas de tamaño fijo

Por defecto awk identifica las columnas utilizando como separador el espacio o el tabulador (si hay varios consecutivos son ignorados). Es posible modificar este separador mediante la variable FS:

	#!/usr/bin/awk -f
	BEGIN { FS = ";" }

Incluso podríamos utilizar expresiones regulares para indicar delimitaciones:

	FS = ", \t" # Coma y tabulador
	FS = " " # Por defecto
	FS = "[ ]" # Un único espacio, si hay varios se consideraran campos vacíos

En el caso de que queramos modificar el delimitador de registros y columnas para la salida utilizaremos las variables ORS y OFS respectivamente:

	#!/usr/bin/awk -f
	BEGIN {
	   ARGC = 2 ; ARGV[1] = "Fichero_A.txt"
	   FS = " " # Delimitador columnas
	   OFS = ";"
	   RS = "\n" # Delimitador registros
	   ORS = "[FIN]\n"
	}	
	{ print $1, $2, $3 }

Para la interpretación de ficheros que no disponen de delimitadores, sino que tienen columnas de tamaño fijo como por ejemplo:

Fichero_ColumnasFijas.txt

	937563.51176.9000.0043.9600.0801
	937663.52276.9060.0043.9620.0801
	937763.51476.9360.0043.9660.0801
	937863.53776.9240.0043.9670.0801
	937961.96778.5430.0044.1090.0801
	938061.95178.5420.0044.1080.0801

Podemos utilizar la variable FIELDWIDTHS, donde especificaremos en orden el tamaño de cada uno de los campos:

	#!/usr/bin/awk -f
	BEGIN {
	    ARGC = 2 ; ARGV[1] = "Fichero_ColumnasFijas.txt"
	    FIELDWIDTHS = "4 6 6 5 5 5 1"   # Tamaño de cada campo en orden
	}
	{ print $1, $2, $3, $4, $5, $6, $7 }

Para hacer referencia al registro completo hemos utilizado $0, pero si queremos acceder a columnas concretas se utiliza $1, $2,… $NF (variable que hace referencia al último). Cabe mencionar que si la columna no existe, no se produce ningún error… simplemente se muestra una cadena vacía.

Si bien $NF apunta a la última columna, NF nos indica el número de columnas que tiene el registro. Podemos utilizar ese valor para acceder a un campo anterior. El siguiente ejemplo nos mostraría el penúltimo campo de cada registro:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ print $(NF-1) }

De hecho, podemos utilizar $() para indicar dentro de los paréntesis cualquier tipo de operación (p.ej. $(2*2-1))

Campos computados

Los campos, además de ser mostrados, pueden ser modificados directamente o incluso creados (p.ej. campos computados):

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ $2 = $2 - 10 }
	{ $(NF+1) = "Nuevo campo" }
	{ print $0 }

Agrupación de acciones

No es necesario tener una regla para cada acción ( {acción} {acción} … ), sino que es posible agrupar diversas acciones en una única regla:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ 
		$2 = $2 - 10
		$(NF+1) = "Nuevo campo"
		print $0
	}

O podemos poner todas las acciones en la misma línea pero separadas por “;”, aunque esta opción complica la lectura del código:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ $2 = $2 - 10 ; $(NF+1) = "Nuevo campo" ;  print $0 }

Getline

Como hemos indicado, para cada registro se ejecutan todas las reglas que definimos en nuestro programa. No obstante, podemos variar ese comportamiento utilizando getline:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ 
	    print "Linea impar: " NR " " $0
	    getline
	    print "Linea par:   " NR " " $0
	}

El anterior programa leerá el primer registro y lo imprimirá añadiendo “Linea impar”. A continuación, getline lee el siguiente registro y lo almacena en $0 (columnas en $1, $2… ) para imprimirlo de nuevo añadiendo “Linea par”. Finalmente, no es necesario volver a indicar getline dado que al llegar al final de programa automáticamente awk salta al siguiente registro.

Quizás nos interese que getline no “machaque” la información del registro actual almacenado en $0, para ello:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ 
	    print "Linea impar: " NR " " $0
	    getline nueva
	    print "Linea par:   " NR " " nueva
	}

El nuevo registro se guardará en la variable “nueva” y por tanto podríamos hacer comparaciones de columnas con el anterior almacenado en $0. Por ejemplo, esto nos permitirá invertir el orden de los registros en grupos de dos:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ 
	    getline nueva
	    print "Linea nueva:      " nueva
	    print "Linea anterior:   " $0
	}

También podemos utilizar getline para leer un registro de un fichero que no haya sido indicado por linea de comandos, por ejemplo:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{ 
	    print $0
	    getline datos_aux < "/etc/passwd"
	}

Con esta característica podremos realizar cruces entre ficheros como veremos más adelante.

Es importante saber que el comando getline devuelve un 1 si encuentra un registro, y 0 si se encuentra el final del fichero. Por otra parte, si se produce algún error (p.ej. no existe el fichero) entonces getline devolverá un -1.

Getline y ejecución de programas

Getline también nos permite leer la salida de comandos mediante el uso de pipes (p.ej. "/bin/date" | getline fecha):

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	BEGIN {
	    if (("/bin/date" | getline fecha) == 1) {
	        print "Fichero generado: " fecha
	    } else {
	        print "Fichero generado en fecha desconocida"
	    }
	    close("/bin/date")
	}
	{
	    print $0 
	}

En el ejemplo anterior, validamos que getline lee correctamente un registro e imprimimos la hora. La acción close permite cerrar pipes o ficheros y así, en caso de que volvamos a ejecutar el comando o a leer el fichero empezaría de nuevo desde cero.

Ficheros de salida

Hasta el momento hemos utilizado la acción “print” para mostrar registros por pantalla, si quisiésemos tener un mayor control del output podríamos utilizar “printf” de forma muy similar a como se realiza en el lenguaje C:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	NR == 1 {
	    printf "Entero %i\n", $1 
	    printf "Decimal %f\n", $1
	    printf "Caracter ASCII %c\n", $1
	    printf "Notación exponencial %e\n", $1
	    printf "Cadena %s\n", $1
	    printf "Octal %o\n", $1
	    printf "Hexadecimal %X\n", $1
	    # Modificadores 
	    printf "Cadena a la derecha en columna de 10: %10i\n", $1
	    printf "Cadena a la izquierda en columna de 10: %-10i\n", $1
	    printf "Número con 2 decimales %.2f\n", $1
	}

Como hemos visto, tanto print como printf escriben por defecto a la salida estándar. Quizás nos interese hacer que la salida se escriba en un fichero o se redirija a un comando mediante una pipe:

	#!/usr/bin/awk -f
	BEGIN { ARGC = 2 ; ARGV[1] = "Fichero_A.txt" }
	{
	    print $0 > "001_fichero_salida.txt" # Fichero nuevo (borra si ya existia)
	    print $0 >> "001_fichero_acumulativo.txt" # Append si el fichero existe
	    print $0 | "/bin/cat" # Pipe a un comando
	    print $0 | "sort -r > 001_fichero_salida.ordenado.txt"
	}
	END { close("/bin/cat") ; close("sort -r > 001_fichero_salida.ordenado.txt") }

Como se observa en el ejemplo anterior, podemos aprovechar las ventajas de las pipes para combinar la potencia de awk con otros comandos como “sort”. Hay que tener en cuenta que el comando no se ejecuta hasta que la pipe no es cerrada con close o por el fin del programa, esto implica que toda la salida se guarda en memoria mientras tanto.

Expresiones

Operadores aritméticos

Suma: x+y
Resta: x-y
Negación: -x
Multiplicación: x*y
División: x/y
Resto: x%y
Exponente: x**y

Comparaciones: x<y , x&#60=y, x>y, x>=y, x==y, x!=y, x~y (‘y’ debe ser una expresión regular, p.ej. /^test/)

Expresiones booleanas: &&, ||, ! (negación)

Conversiones de formato

Las conversiones entre números y cadenas son automáticas (en caso de que una cadena no pueda ser convertida se traduce como 0):

	#!/usr/bin/awk -f
	BEGIN {
	    edad = "27"
	    edad = edad + 1.5
	    print "Tengo " edad " años."
	}

En el ejemplo anterior edad es inicializada con una cadena, a continuación se utiliza en una suma y se convierte automáticamente a numérico. Finalment print convierte el número a cadena para permitir la concatenación.

Control del flujo

Las estructuras de control de awk son muy parecidas a las del lenguaje C:

	#!/usr/bin/awk -f
	BEGIN {
		# Condicional
		edad = "27"
		if (edad == 27) {
			print "Si"
		} else {
			print "No"
		}

		# Bucles
		i = 1
		print "While"
		while (i < = 3) {
			print i
			i++
		}

		print "Do while"
		do {
			i--
			print i
		} while (i > 1)

		print "Bucle for"
		for(i=1; i< = 3; i++) {
			print i
		}
	}

En los bucles podemos utilizar "continue" para saltar a la siguiente iteración o "break" para finalizar. A nivel de programa, como awk ejecuta todas las reglas para cada registro de los ficheros de entrada, en cierta forma se trata de un bucle que podemos hacer saltar al siguiente registro con "next" o parar con "exit".

Arrays asociativos

Como estructura de almacenamiento de información ya hemos trabajado con variables, pero awk nos ofrece la posibilidad de usar arrays asociativos (diccionarios):

	#!/usr/bin/awk -f
	BEGIN {
		v[0] = "Elemento"
		v[1] = "Cadena"
		v["Clave"] = "Valor"
		v["key"] = 25

		# Comprobar la existencia de una clave
		if ("Clave" in v) {
			print "Clave encontrada!"
		}

		# Borrado de elementos
		delete v["Clave"]

		# Recorrer un array
		for(key in v) {
			print key " -> " v[key]
		}

		# Arrays multidimensionales
		w[0, 1] = ";-)"
	}

Otras acciones/funciones

Veamos otras acciones útiles de awk:

	#!/usr/bin/awk -f
	BEGIN {
		## Números
		srand() # Inicializa semilla con la fecha/hora actual
		print rand() # Decimal aleatorio entre 0 y 1
		print int(2.45) # Parte entera
		num = sprintf("%.2f", 2.256821)	# Redondeo a 2 decimales
		print num
		print sqrt(4) # Raiz cuadrada
		print "Exponencial " exp(1) " sinus " sin(2) " cosinus " cos(2) " logaritmo " log(2)

		## Strings
		print index("cadena", "de") # Posición donde se encuentra "de" (0 si no lo encuentra)
		print length("cadena") # Tamaño
		print match("cadena", /$.*/) # Compara con expresión regular
		str = "1;2;3"
		split(str, v, ";")  # Divide y guarda en un vector/array
		print v[1] " " v[2] " " v[3]
		sub(/;/, "-", str) # Substituye la primera aparición ";" por "-"
		print str
		gsub(/;/, "-", str) # Substituye todos los ";" por "-"
		print str
		print substr(str, 1, 3) # Desde la posición 1, devuelve los siguientes 3 caracteres
		print tolower("MAYUSCULAS")
		print toupper("minusculas")

		#Fechas y horas
		t1 = systime()  # Fecha/hora actual
		t2 = mktime("2009 01 01 00 00 00") # YYYY MM DD HH MM SS 
		print "Tiempo transcurrido desde 01-01-2009: " 
		print "- Segundos: " t1 - t2
		print "- Minutos: " (t1 - t2)/60
		print "- Horas: " (t1 - t2)/60/60
		print "- Dias: " (t1 - t2)/60/60/24

		# Formatear hora/fecha actual
		print strftime("%Y-%m-%d %H:%M:%S", systime()) # 2009-02-14 17:46:28
	}

Para el formateo de fechas se utiliza la siguiente nomenclatura:

	%a	%A	Nombre del dia (p.ej. Lunes)
	%b	%B	Nombre del mes
	%d		Día del mes
	%H		Hora (24)
	%I		Hora (12)
	%j		Día del año (001-365)
	%m		Mes
	%M		Minuto
	%p		AM/PM
	%S		Segundo
	%u		Número del día de la semana (lunes = 1)
	%U		Número de la semana del año
	%Y		Año

Funciones

Awk nos permite definir nuestras propias funciones:

	#!/usr/bin/awk -f
	function p (str) {
		print str
		return 0    # Opcional
	}

	BEGIN {
		p("Hola mundo!")
	}

Esto nos permitiría hacer funciones genéricas que nos ahorren código repetitivo.

Expresiones regulares

^	Inicio de la cadena
$	Final de la cadena
.	Cualquier carácter único (excepto newline)
[]	Conjunto de caracteres
	[MVX]	Encaja con M, V o X
	[0-9]	Encaja con un dígito
[^]	Conjunto de carácter complementario
	[^0-9]	Encaja con cualquier carácter excepto un dígito
|	Alternativas
	A|[0-9]	Encaja con "A" o un dígito
()	Se pueden utilizar paréntesis para agrupar expresiones
	(A|[0-9])Z	Encaja con "A" o dígito, que uno u otro vaya seguido de Z
*	La expresión precedente se puede repetir 0 o más veces
	A*	Encaja con nada o un número indefinido de A
+	La expresión precedente tendrá lugar 1 o más veces
	A+	Encaja con una o más A
?	La expresión precedente no aparecerá o aparecerá 1 única vez

Para que awk no sea sensible a las mayúsculas/minúsculas podemos jugar con las acciones tolower/toupper o bien establecer la variable IGNORECASE a 1.

Rendimiento / Profiling

Podemos ejecutar nuestro programa con pgawk para que genere un fichero denominado “awkprof.out” donde para cada linea se indica el número de veces que ha sido ejecutada. Así podremos identificar aspectos a optimizar.

Nombres de columnas

Por defecto, awk nos permite acceder a las columnas mediante el uso de $1, $2, etc… pero esto puede ser un inconveniente si cambia el número o orden de las columnas de nuestros ficheros, dado que implicará un cambio forzado en las referencias que usamos en nuestro programa. Para solucionar ese problema, si disponemos de ficheros donde la primera linea es la cabecera y se anotan los nombres de las columnas, podemos crear un array que después nos permita acceder como sigue:

	#!/usr/bin/awk -f
	BEGIN {
		ARGC = 1+1 # Uno más que el número de ficheros de entrada que indiquemos.
		ARGV[1] = "Fichero_Column.txt"
	}
	# Cabecera
	NR==1 {
			# Guardamos nombres de columnas 
			for(i = 1; i< = NF; i++) {
				c[tolower($i)] = i
			}
		 }

	# Podemos acceder a las columnas por $1, $2... o por $c["nombre_columna"]
	NR > 1 { print $c["mes"] }

De esta forma podremos acceder a las columnas usando $c[“nombre_columna”], independientemente que en el futuro la cambiemos de lugar en el fichero de entrada.

Ordenar los registros de un fichero

Utilizaremos una función para ordenar array de menor a mayor, tenemos dos opciones:

  1. según los elementos: asort(array)
  2. según los indices: asorti(array)

Ambas funciones “machacan” los índices originales por índices numéricos (1, 2, 3…), para evitar perder el array original podemos usar dos argumentos: asort(array_origen, array_destino)

Por ejemplo, para ordenar un fichero por dos campos determinados utilizando asorti:

	#!/usr/bin/awk -f
	{ 
		# Queremos ordenar por los campos $2 y $1 y guardamos la linea original correspondiente
		original[$2,$1] = $0
	}

	END {
			# En "ordenado" tendremos del 1 a n las claves $2,$1 ordenadas de menor a mayor
			n = asorti(original, ordenado)
			for (i = 1; i < = n; i++) {
				# Imprimimos las lineas originales pero siguiendo el nuevo orden
				print original[ordenado[i]]
			}
	}

Sumarizaciones

Para realizar una sumarización de un fichero por el campo $1 y sumando los valores del campo $3, podemos utilizar el siguiente código (no requiere que el fichero este ordenado):

	#!/usr/bin/awk -f
	{
		# Si no es la primera vez que tratamos esta clave...
		if (count[$1] != "") {
			count[$1]++
			col_sum[$1] += $3
		} else {
			count[$1] = 1
			col_sum[$1] = $3
		}
	 }

	END {
		for (k in count) {
			print k " " count[k] " " col_sum[k]
		}
	}

Podríamos añadir más arrays para hacer más sumarizaciones de columnas, o utilizar como índice más campos (p.ej. count[$1, $2]) si queremos utilizar varias columnas como campos clave.

Por supuesto, con este mismo ejemplo podríamos validar si existen duplicados dado que en “count” estamos guardando el número de apariciones.

Cruces de ficheros (join match + unmatch)

Veamos como podemos hacer el cruce de los siguientes ficheros utilizando el primer campo como clave primaria:

Fichero_A.sorted.txt

	Apr 21 70 74 514
	Apr 31 52 63 420
	Aug 15 34 47 316
	Feb 15 32 24 226
	Feb 26 58 80 652
	Jan 13 25 15 115
	Jan 21 36 64 620
	Jul 24 34 67 436
	Jun 31 42 75 492
	Mar 15 24 34 228
	Mar 24 75 70 495
	May 16 34 29 208
	Nov 20 87 82 577
	Oct 29 54 68 525
	Sep 13 55 37 277

Fichero_B.sorted.txt

	Apr Abril
	Aug Agosto
	Dec Diciembre
	Feb Febrero
	Jul Julio
	Jun Junio
	Mar Marzo
	May Mayo
	Nov Noviembre
	Oct Octubre

Es importante observar que ambos ficheros se encuentran ordenados por la clave primaria (que será la que utilizaremos para cruzar) y el Fichero_B no contiene duplicados (que será el que utilizaremos como fichero secundario en el cruce).

A continuación el código del cruce:

	#!/usr/bin/awk -f
	BEGIN {
		## Ficheros de entrada
		#  - Ordenados por las claves que van a ser utilizadas
		#  - El fichero secundario no puede tener duplicados
		primary_file = "Fichero_A.sort.txt"
		secondary_file = "Fichero_B.sort.txt"

		# Ficheros de salida
		match_file = "000_A_and_B.txt"
		primary_unmatch_file = "000_A_and_notB.txt"
		secondary_unmatch_file = "000_notA_and_B.txt"

		ARGC = 1+1 # Uno más que el número de ficheros de entrada que indiquemos.
		ARGV[1] = primary_file
	}

	{
		# Clave primaria del fichero primario
		primary_pkey = $1

		# Si el campo clave del fichero primario es más grande que el del secundario, avanzamos el registro del secundario
		if (reg == "" || primary_pkey > secondary_pkey) {
			status = getline reg < secondary_file
			if (status == 1) {
				split(reg, r, " ") # Guardamos los campos en el array r[1..n]

				 # Clave primaria del fichero secundario
				secondary_pkey = r[1]
			} 
		}

		# Si no se ha llegado al final del fichero del secundario
		if (status == 1) {
			# Cruzan
			if (primary_pkey == secondary_pkey) {
				print $0, r[2] > match_file
				vmatch[primary_pkey] = 1 # Control para evitar detectar como no cruzado en el futuro
			}

			# Registro del primario no existe en el secundario
			if (primary_pkey < secondary_pkey && !vmatch[primary_pkey]) {
				print $0 > primary_unmatch_file
			}

			# Registro del secundario no existe en el primario
			if (primary_pkey > secondary_pkey  && !vmatch[secondary_pkey]) {
				print reg > secondary_unmatch_file
			}
		} else {
			# Se ha acabado el fichero secundario, por tanto todo lo pendiente del primario no existe en el secundario
			print $0 > primary_unmatch_file
		}

	}

Como podemos observar, el código necesario para llevar a cabo el cruce es algo más extenso que los ejemplos que hemos visto hasta ahora. No obstante, dado que awk nos da una mayor granularidad que otras herramientas comerciales de tratamiento masivo como ACL o SAS, tenemos como ventaja una mayor eficiencia y un incremento en la flexibilidad y potencia.

El programa anterior nos genera tres ficheros de salida:

  1. “000_A_and_B.txt”: Registros que han cruzado
  2. “000_A_and_notB.txt”: Registros del fichero primario que no se encuentran en el secundario
  3. “000_notA_and_B.txt”: Registros del fichero secundario que no se encuentran en el primario

Tal y hemos visto, los fichero de entrada deben estar ordenados para que el cruce funcione correctamente. A continuación tenemos otro programa que realiza la misma funcionalidad y que no requiere que los ficheros estén ordenados. Para ello el fichero secundario será cargado completamente en memoria antes de iniciar el cruce, esto implica un mayor rendimiento en cuanto al tiempo de ejecución pero un consumo muy superior de memoria. Si tenemos suficiente memoria RAM o los ficheros con los que trabajamos no son excesivamente grandes, vale la pena considerar este algoritmo:

	#!/usr/bin/awk -f
	BEGIN {
		## Ficheros de entrada
		#  - No es necesario que esten ordenados
		#  - El fichero secundario no puede tener duplicados
		primary_file = "Fichero_A.sort.txt"
		secondary_file = "Fichero_B.sort.txt"

		# Ficheros de salida
		match_file = "000_A_and_B.txt"
		primary_unmatch_file = "000_A_and_notB.txt"
		secondary_unmatch_file = "000_notA_and_B.txt"

		ARGC = 1+1 # Uno más que el número de ficheros de entrada que indiquemos.
		ARGV[1] = primary_file

		# Cargamos el fichero secundario en memoria en el array sec
		while ((getline < secondary_file) > 0) {
			sec[$1] = $0
			sec_matched[$1] = 0 # Array que será utilizado para identificar los reg. del secundario que no cruzan
		}

	}

	{
		# Registros que cruzan
		if (sec[$1]) {
			sec_matched[$1]++ # Marcamos registro secundario como cruzado

			# Añadimos el camo 2 del registro secundario a toda la linea del primario
			split(sec[$1], s, " ")
			print $0, s[2] > match_file
		} else {
			# Registros del primario que no existen en el secundario
			print $0 > primary_unmatch_file
		}
	}

	END {
		# Registros del secundario que no existen en el primario
		for(k in sec_matched) {
			if (sec_matched[k] == 0) {
				print sec[k] > secondary_unmatch_file
			}
		}
	}

Muestra aleatorias

Para la obtención de N registros aleatorios de un fichero, podemos utilizar la siguiente implementación del algoritmo R de Waterman, mediante el cual no es necesario conocer de antemano el número total de registros y por tanto solo tendremos que leer una vez el fichero:

	#!/usr/bin/awk -f
	# Waterman's Algorithm R for random sampling
	# by way of Knuth's The Art of Computer Programming, volume 2

	BEGIN {
		ARGC = 1+1 # Uno más que el número de ficheros de entrada que indiquemos.
		ARGV[1] = "Fichero_A.txt"

		n = 2 # Tamaño la muestra
		t = n
		srand() # Inicializamos semilla
	}

	# Si el numero de registros tratados es inferior al tamaño de la muestra
	NR < = n {
		# Construimos el pool inicial de muestras con los primeros registros consecutivos
		pool[NR] = $0
		places[NR] = NR
		next
	}

	# Si el numero de registros tratados es superior al tamaño de la muestra
	NR > n {
		t++ # Decrementamos las probabilidades de que el siguiente numero aleatorio sea inferior al numero de muestras
		M = int(rand()*t) + 1
		if (M < = n) {
			# Substituimos registro del pool
			rec = places[M]         # Borramos la muestra previamente seleccionada
			delete pool[rec]
			pool[NR] = $0           # Añadimos la nueva muestra
			places[M] = NR
		}
	}

	END {
		if (NR < n) {
			print "El numero de registros es inferior al muestreo definido" > "/dev/stderr"
			exit
		}

		# Dado que asorti es una funcion que ordena alfabéticamente y no según el numero real:
		# Convertimos claves numericas del array pool a cadenas con ceros (p.ej. 1 -> 01)
		pad = length(NR)
		for (i in pool) {
			new_index = sprintf("%0" pad "i", i)
			newpool[new_index] = pool[i]
		}
		# Visualizamos la muestra en orden
		x = asorti(newpool, ordered)
		for (i = 1; i < = x; i++)
			print newpool[ordered[i]]
	}

Conexiones de red

Awk también nos permite realizar conexiones de red mediante el uso de la siguiente ruta:

	"/inet/protocol/local-port/remote-host/remote-port"

El protocolo puede ser ‘tcp’, ‘udp’, or ‘raw’ y como puerto local podemos dejarlo a 0 para que el sistema lo establezca por nosotros.

Veamos un ejemplo de conexión a Yahoo Finance para obtener la cotización del índice IBEX35 y el cambio Euro-Dolar:

	#!/usr/bin/awk -f
	BEGIN {
		FS = ","        # Separador de campos

		NetService = "/inet/tcp/0/download.finance.yahoo.com/80"
		print "GET http://download.finance.yahoo.com/d/quotes.csv?f=snl1d1t1c4&e=.csv&s=^IBEX+EURUSD=X" |& NetService
		print "symbol,name,last,date,time,currency"
		while ((NetService |& getline) > 0) {
			gsub(/"/, "", $0)       # Quitamos las " de los strings
			print $0
		}
		close(NetService)
	}

15 thoughts on “Tratamiento masivo de datos con AWK, alternativa parcial a ACL o SAS

  1. hola!!
    alguien podria ayudarme???
    en una columna de datos numericos como podria restar el dato anterior???
    PJ:
    de lo siguiente sumar de la columna 2 y 3 siempre el dato anterior…

    Sat-22:05:04-12/12/09 163652252 154440470
    Sat-22:10:03-12/12/09 163656013 154443781
    Sat-22:15:04-12/12/09 163666077 154453472
    Sat-22:20:03-12/12/09 163685518 154472151
    Sat-22:25:03-12/12/09 163691778 154477788
    Sat-22:30:04-12/12/09 163699900 154484164
    Sat-22:35:02-12/12/09 163704968 154488847
    Sat-22:40:02-12/12/09 163708669 154492211
    Sat-22:45:04-12/12/09 163713052 154496231
    Sat-22:50:02-12/12/09 163717234 154500082

    es decir de las 22:50, restar en la columna 2 lo de las 22:45, igual para la columna 3….
    saludos…

  2. Hola … soy novato en el tema y necesito que me puedan ayudar para el caso de “Cruces de ficheros (join match + unmatch)”. El ejemplo me sirve, pero con la diferencia que los archivos están separados por “|” … ingreso los códigos que aparecen pero nunca crea el archivo de match … será que habra´que agregar un RS = “|” en algún lugar …

    Muchas gracias por la ayuda
    Saludos ,
    Patricio

  3. si tengo un fichero con el siguiente texto:
    juan comio galletas
    juan comio pasta
    pedro comio pan
    jose comio pasta

    como puedo con awk leer la linea previa a fin de identificar que el $1 juan esta en 2 o n lineas???

    para que como resultado imprima:
    juan comio galletas,pasta
    pedro comio pan
    jose comio pasta

    ayuda porfa…..

  4. Hola, estoy haciendo una shell que recibe parámetros y luego en su interior los guarda en variables:

    PARAMETRO1=$1
    PARAMETRO2=$2

    Luego hago un cat en la misma shell y quiero mostrar la primera columna del resultado con awk

    cat /var/log/xferlog* | grep ‘comercial’ | awk ‘{print ‘$1′}’

    El problema es que el $1 lo interpreta como el parámetro que recibió y no como la primera columna del resultado

  5. Hola podrían ayudarme? Gracias
    tengo una tabla similar

    ID M1 M2 M3 M4 M5 M6 M7 M8 M9 Texto
    3 629.0 314.0 164.0 392.0 224.0 360.0 466.0 741.0 295.0 Texto
    7 84.0 439.0 46.0 117.0 139.0 55.0 135.0 20.0 31.0 Texto
    8 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 Texto
    9 1.0 0.0 6.0 0.0 0.0 7.0 0.0 1.0 0.0 Texto
    14 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Texto

    Quisiera saber como eliminar las lineas en las que suma de cada valor de igual a 1. Por ejemplo las lineas 4 y 6

    Muchas gracias por su atención saludos.

Leave a Reply

Your email address will not be published. Required fields are marked *