Truco SAS: Convertir fechas datetime de Unix a SAS

Como sabemos, los formatos de fechas en SAS y en otros sistemas operativos o bases de datos se interpretan y se almacenan como números que representan el número de días (o de segundos, en el caso de los datetime) transcurridos desde una fecha de origen. Las fechas anteriores a ella se almacenan como números negativos.

Lo que puede cambiar entre estos distintos sistemas es esa fecha origen a partir de la que se empieza a contar, de forma que una misma cifra puede representar fechas distintas dependiendo de qué sistema haya generado esa fecha. Esto da lugar a algunos problemas que debemos resolver cuando se realizan migraciones de datos de un sistema a otro. En nuestro caso, como programadores en SAS, debemos tener cuidado al importar fechas generadas con el timestamp de Unix y tener en cuenta que Unix no comienza a contar las fechas desde la misma fecha origen que SAS. Mientras SAS comienza a contar en ’01jan1960′; Unix comienza a contar en ’01jan1970′. Esos 10 años de diferencias hay que tenerlos en cuenta.

Por ello, si tenemos un datetime generado en Unix será necesario aplicar la siguiente corrección para que al convertir el número en fecha SAS éstas sean equivalentes:

fecha_SAS = fecha_Unix + 315619200;

Esa cantidad que sumamos es el número de segundos que representan esos 10 años. Existe otra forma de hacerlo, quizás un poco más elegante, que es utilizando la función dhms():

fecha_SAS = dhms('01jan1970'd, 0, 0, fecha_Unix);

La función dhms() construye un datetime a partir de una fecha y un número de horas, minutos y segundos. Con la instrucción anterior, se generará un datetime sumando el número de segundos expresados en la fecha_Unix a la fecha origen de Unix, que es el ’01jan1970′. Es una forma más elegante y además resulta más fácil recordar la fecha de origen de Unix, que el número tan grande de segundos que representan esa diferencia.
Si las fechas de Unix están representadas en milisegundos solo debemos utilizar una pequeña variante sobre lo anterior:

fecha_SAS = dhms('01jan1970'd, 0, 0, fecha_Unix/1000);

SAS: Obtener el listado de los últimos ficheros por fecha en Unix

¿Cómo podemos obtener y cargar los últimos archivos de un cierto periodo? Seguro que podemos encontrarnos este problema en algunas de sus variantes y podemos resolver todas ellas basándonos de la siguiente forma. Voy a basar este artículo en el supuesto de una máquina SAS corriendo sobre un sistema operativo Unix, que creo que es lo más común, aunque tiene también la misma solución bajo Windows, con algunas diferencias.

Lo primero que vamos a hacer es obtener el listado de ficheros csv que nos interesa cargar, utilizando la línea de comandos del sistema operativo del servidor SAS, y vamos a incluir ese listado en un fichero lista_ficheros.txt. Utilizaremos el comando x de SAS que envía las instrucciones al sistema operativo para que este los interprete.

x"cd /[ruta_ficheros]";
x"ls *.csv > lista_ficheros.txt";

En el paso anterior el comando «cd» indica la ruta en la vamos a trabajar, que será la ruta en donde tenemos guardados esos ficheros. El comando «ls» lista los ficheros que cumplan con el patrón indicado y la salida la vuelca en un fichero nuevo llamado lista_ficheros.txt. Sin embargo, el paso anterior no discrimina los ficheros por fecha y para poder obtener, por ejemplo, los ficheros de los últimos 15 días utilizaremos lo siguiente:

x"cd /[ruta_ficheros]";
x"find *.csv -mtime +15 > lista_ficheros.txt";

El comando «find» busca todos aquellos ficheros en la ruta indicada que cumplan el patrón de nombre de fichero y que estén en el periodo temporal indicado en «-mtime» volcando todos los nombres de esos ficheros en un nuevo fichero llamado listado_ficheros.txt. Una vez llegado a este punto solo nos queda importar el fichero txt como una tabla SAS para poder procesar la información que contiene fácilmente:

data LISTA_FICHEROS;
    length fichero $ 50 ;
    format fichero $CHAR50. ;
    informat fichero $CHAR50. ;
    infile "[ruta_ficheros]/ficheros.txt"
        lrecl=50 encoding="LATIN9" missover;
    input fichero : $CHAR50. ;
run;

Ahora tenemos una tabla (LISTA_FICHEROS) que contiene una lista de nombres de ficheros csv que queremos importar. Como son varios, más de uno, debemos utilizar un bucle para recorrer la tabla de los nombres a la vez que importamos los csv y los vamos guardando como tablas SAS. Pero, ¿cómo se hace eso? ¿Cómo podemos recorrer una tabla a la vez que creamos otra? Utilizaremos para recorrer la primera tabla una especie de puntero que recorre LISTA_FICHEROS:

%let dsid = %sysfunc(open(LISTA_FICHEROS));
%macro importar_ficheros;
    %let num_reg = %sysfunc(attrn(&dsid,NOBS));
    %do x=1 %to &num_reg;
        %let rc = %sysfunc(fetch(&dsid));
        %let fichero = %sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,F1))));

        data FICHERO_&x;
            length campo $ 20;
            format campo $CHAR20. ;
            informat campo $CHAR20. ;
            infile"&fichero"
                lrecl=20 encoding="UTF8" missover;
            input campo : $CHAR20. ;
        run;
%mend importar_ficheros;
%importar_ficheros;

Sobre como utilizar estos punteros de datos en SAS que hemos utilizado en este último paso podéis revisar este link: «Punteros de datos en SAS» donde se explican los distintos comandos que se utilizan para gestionar los punteros.

Por otro lado, el bucle anterior va recogiendo el nombre de cada fichero csv que está listado en la tabla LISTA_FICHEROS e importándolo a una tabla SAS. En este supuesto todos los ficheros son del mismo formato.

Ejecutar un programa SAS pasando parámetros

La entrada de hoy es la solución a un problema muy fácil: se trata de planificar un programa sas pasándole algún parámetros desde línea de comandos, en este caso, la solución aplica a Unix/Linux.

Para ello, debemos crear un fichero de script (.sh) que contenga la llamada y los parámetros a enviar:

sh [ruta_SAS]/Lev1/SASApp/sas.sh -set fecha 20201111 -set hora 17:28 -sysin [ruta_programa]/programa.sas -log [ruta_log]/fichero.log

La llamada a sas.sh ejecuta sas.exe con las opciones que estén configuradas para el usuario. El parámetro set contiene una dupla clave valor con el nombre de la variable a enviar al programa en ejecución y su valor. Se pueden concatenar varios parámetros set. En el ejemplo se pasa la fecha y la hora. Finalmente, los parámetros sysin y log indican la ruta y fichero del programa sas y del log que se debe generar, respectivamente.

Una vez hecho esto, solo queda recoger en el programa los valores que hemos enviado. Esto podemos hacerlo de la siguiente forma:

%let fecha=%sysget(fecha);
%let hora=%sysget(hora);

Con ello, podemos utilizar los parámetros que hemos enviado como simples macrovariables: &fecha y &hora.

Unix: Extensión de un fichero Unix

En esta ocasión vamos a ver cómo podemos hacer con un script de Unix una tarea que en realidad también podemos hacer con SAS. Se trata de renombrar ficheros *.zip a *.zip.old, en este caso modificando su extensión. Para ello, debemos poder extraer las dos partes del nombre de un fichero: el nombre y la extensión.

FICHERO="${i%%.*}"
EXTENSION=$([[ "$i" = *.* ]] && echo "${i#.}" || echo "${i##.}")

Si queremos procesar todos los ficheros zip de un directorio para renombrarlos recursivamente a zip.old (por ejemplo) podemos usar el siguiente script. Lo primero que hace es listar los ficheros que tienen zip en su nombre (o extensión). Luego revisa, para cada uno de ellos, cual es exactamente su extensión, y si finalmente tienen extensión zip los renombra (con mv) a *.zip.old:

for i in $(ls -1 | grep .zip);
do FICHERO="${i%%.*}";
EXTENSION=$([[ "$i" = *.* ]] && echo "${i#.}" || echo "${i##.}");
if [ "$EXTENSION" == "zip" ];
then mv $FICHERO.zip $FICHERO.zip.old;
fi;
done

Este script puede ser llamado también desde SAS para hacer esa misma tarea con el comando X:

x"for i in $(ls -1 | grep .zip); do FICHERO="${i%%.*}"; EXTENSION=$([[ "$i" = *.* ]] && echo "${i#.}" || echo "${i##.}"); if [ "$EXTENSION" == "zip" ]; then mv $FICHERO.zip $FICHERO.zip.old; fi; done";

Automatización del borrado de ficheros

La automatización del borrado de ficheros es una tarea necesaria cuando, por ejemplo, estamos generando en un proceso planificado ficheros fechados periódicamente. Hacer exhaustivamente esta tarea es complicado y no siempre la realizamos por esa razón (yo al menos). Esta macro da solución a esa necesidad, os animo a usarla.

Lo primero que hacemos es crear un stream de datos, df, que recoge el resultado del comando ls. El parámetro F marca los directorios con una barra «/» al final, de forma que podemos eliminarlos de la salida del ls con un grep -v.

Usando df como entrada para nuestro paso data todo lo que queda es procesar las información que hemos generado con un scan, que divide la cadena por los espacios. Finalmente solo nos queda montar la fecha de creación del fichero teniendo en cuenta lo siguiente: Linux devuelve la fecha en dos formatos distintos dependiendo de si hace menos de 6 meses que se ha generado el fichero o si hace más. Este es un ejemplo:

Formatos de fecha en un ls

Finalmente, filtramos los registros que contienen los nombres de los ficheros que queremos eliminar: ficheros anteriores a 2020 y que no sean tablas SAS. Un call system ejemcutará un comando rm con cada registro resultante. Esta es la macro:

%macro borrado(ruta=,fecha_limite=);
    x "cd &ruta";
    filename df pipe "ls -lahF &ruta | grep -v /";

    data _null_;
        infile df;
        input todo $300.;
        format fecha date9. fichero $100.;
        dia = scan(todo,7," ");
        if index(scan(todo,8," "),":") then anyo=year(date());
           else anyo=input(put(scan(todo,8," "),$5.),8.);
        if compress(scan(todo,6," "))='Jan' then mes=1;
        else if compress(scan(todo,6," "))='Feb' then mes=2;
        else if compress(scan(todo,6," "))='Mar' then mes=3;
        else if compress(scan(todo,6," "))='Apr' then mes=4;
        else if compress(scan(todo,6," "))='May' then mes=5;
        else if compress(scan(todo,6," "))='Jun' then mes=6;
        else if compress(scan(todo,6," "))='Jul' then mes=7;
        else if compress(scan(todo,6," "))='Aug' then mes=8;
        else if compress(scan(todo,6," "))='Sep' then mes=9;
        else if compress(scan(todo,6," "))='Oct' then mes=10;
        else if compress(scan(todo,6," "))='Nov' then mes=11;
        else if compress(scan(todo,6," "))='Dec' then mes=12;
        fecha = mdy(mes,dia,anyo);
        if fecha > date() then fecha=intnx('year',fecha,-1,'S');
        fichero = compress(scan(todo,9," "),'*');
        if fecha < &fecha_limite and not index(fichero,'.sas7bdat');

        call system('rm '||fichero);
    run;
%mend;

%borrado(ruta=[rute],fecha_limite='1jan2020'd);

Administración SAS: Detección sesiones pesadas

Este procedimiento sirve a aquellos administradores de un sistema SAS a determinar que sesión y que usuario pueden llegar a estar dando problemas al resto en un momento dado por estar consumiendo demasiado espacio en disco en el servidor.

Para poder ejecutarlo es necesario tener un usuario en el sistema operativo de la máquina SAS con permisos sudo. Este procedimiento está, por tanto, dirigido a máquinas Linux y se lo debo a mi compañero y amigo Christian.

Tras logarnos nos adjudicamos permisos sudo: sudo su -

sesiones1

Nos dirigimos a la ruta donde está la work, típicamente /opt/sas/saswork/. En esta carpeta se encuentran almacenadas las carpetas Linux que contienen todos los datos almacenados en todas las work de todas las sesiones SAS abiertas en ese momento por todos los usuarios en esa máquina. Listamos los nombres de esas work y determinamos cuales pueden ser más grandes y dar problemas, por ejemplo hacemos un grep para coger aquellas sesiones que están expresadas en Gb: du -sh * | grep G

sesiones2

Podemos ver que efectivamente las work están almacenadas en esa ruta de nuestro servidor si desde Enterprise Guide sacamos las propiedades de nuestra work con el botón derecho. La ruta que nos indica es exactamente esa.

sesiones3

Finalmente para identificar al usuario ‘infractor’ hacemos un grep del log de su sesión buscando su CLIENTMACINE, que no es más que su identificador de usuario. Se puede hacer también un grep a CLIENTUSERID que contiene el nombre del usuario:

grep CLIENTUSERID SASApp_WorkspaceServer_2018-11-07_sas_5889.log

En el caso de que estuviéramos buscando una sesión que se haya quedado colgada la tarea de identificarla es siempre muy fácil si los usuarios ejecutan un %put &sysjobid; porque reflejará el PID de esa tarea en el log y sabremos que tarea matar. Lo realmente idóneo es incluir esta instrucción en el autoexec.sas.

Automatización de procesos con SAS

SAS Enterprise Guide tiene un planificador de tareas que no está mal pero también se puede planificar tareas en el propio servidor. La solución que os ofrezco está basada en un servidor con un sistema operativo Linux.

Una automatización tiene 3 partes: el código SAS a ejecutar, un script que llama al programa y un planificador de tareas que llama al anterior.

Programa SAS (fichero .sas)

Es necesario eliminar de él cualquier rastro de parámetro o fecha variable que hallamos estado empleando cuando lo desarrollamos y debemos calcular esos valores en función de la información que tendremos en la máquina en el momento de la ejecución: como por ejemplo la fecha de sistema. Si queremos hacer un cierre de mes lo más sensato será calcular la fecha de cierre como:

fecha_ejecución = intnx('month',date(),-1,'E');

 
Además recomiendo incluir al principio del programa la siguiente línea que imprime en el log del proceso el número de tarea que se está ejecutando. De esa manera podremos matar un proceso colgado con un comando kill.

%put &sysjobid;

 
TIP: Al hacer una primera automatización es recomendable verificar cual es la fecha del sistema en el servidor SAS, porque podría ser distinta a la actual y eso puede generar problemas.

Script (fichero .sh)

Este pequeño programa es el encargado de identificar la ruta donde se encuentra [sas.exe] y en este caso utiliza además otro script [sas.sh] para que se ejecuten los parámetros de ejecución de sas.exe. También necesita la ruta y el nombre del programa .sas a ejecutar y la ruta y nombre del fichero que se creará con el log de la ejecución. Es importante tener en cuenta esto porque sino nunca sabremos por qué fallan nuestros procesos.

#!/bin/sh
f_job=[ruta_programa]/[nombre_programa].sas
f_log_sas=[ruta_log]/`date +%Y%m%d%H%M`_"$1"[nombre_log].log
sh [ruta_base_sas]/sas.sh -sysin ${f_job} -log ${f_log_sas}

 
Ambos ficheros el .sas y el .sh deben ser copiados con FileZilla u otra herramienta similar a las rutas que hemos visto.

Planificador de tareas

Utilizo el planificador de tareas de Linuz, CRON. Cron tiene un demonio corriendo en el sistema operativo que lanza las tareas que se han indicado en un fichero de texto: crontab. Cada usuario del sistema tiene un fichero crontab para sus propias tareas.

En nuestro caso, para que SAS tenga permisos sobre las ficheros y tablas que necesitamos utilizar en nuestro proceso es necesario utilizar un usuario de Linux que sea también usuario en SAS. Existen varios usuarios de esta naturaleza en una instalación de SAS: sasbatch, sasdemo, etc. El adecuado para planificar un proceso es sasbatch.

Para editar el fichero crontab debemos logarnos en la máquina y acceder al shell para introducir este comando:

crontab

El programa se abre con el visor vi. Para editarlo pulsamos [INSERT]. A continuación introduciremos una línea por programa sas a planificar. Finalmente introducimos las combinaciones de teclas: [END]:q! para salir sin guardar y [ESC]:wq para salvar y salir. No voy a entrar definir aquí como se planifica una tarea pero os dejo un enlace a la Wikipedia donde se explica muy bien:

En un próximo post comentaré errores típicos que se pueden producen al planificar.