"Después del juego es antes del juego"
Sepp Herberger

lunes, 30 de junio de 2014

Importar registros de libros a tu programa de Bibliotecas con MARC y Z39.50

Tras este largo parón, vamos a ver como resolví un problema que se me presentó hace tiempo: obtener de forma automatizada los datos de una lista de libros a partir de su ISBN para cargarlos en lotes en un programa de gestión de bibliotecas.

El problema es el siguiente: tenemos un programa de gestión de bibliotecas, (por ejemplo, Abies), y tenemos varios miles de libros por catalogar e introducir en su base de datos. El Abies funciona bien metiendo libros sueltos, ya que permite cargar registros bibliográficos con todos sus datos, sacados de una pagina web como la Biblioteca Nacional de España, Rebeca (una base de datos del Ministerio de Cultura) o las bibliotecas de diversas universidades.

En dichas páginas podemos buscar un libro, normalmente por su ISBN y obtener un registro con sus datos para bajar en formato MARC que luego será importado en nuestro software de gestión de bibliotecas.

El problema de todo esto es que el método es válido para importar libros esporádicamente, ya que todo el proceso es casi manual (buscar el ISBN del libro en la página web, marcarlo, descargar el archivo, importarlo al depósito de Abies, buscarlo en Abies e importarlo al catálogo desde el depósito - en otros programas de gestión de bibliotecas el proceso será similar), pero tremendamente improductivo cuanto mayor es el número de libros a traspasar. Como buen administrador de sistemas odio los trabajos repetitivos, así que había que encontrar la manera de evitarlo.

Para agilizar este proceso podemos al menos automatizar la importación de registros desde ISBN a MARC.  El formato MARC es muy antiguo y nada mas echarle un vistazo se ve que está pensado para una época de informática cavernícola. Aquí, un registro en formato MARC:


Para conectarnos a las bases de datos que nos devuelven datos en este formato MARC no usaremos servicios web, ni llamadas a procedimiento remoto: nos las veremos con un protocolo llamado Z39.50, que usa un lenguaje de consultas bastante esotérico.

Nuestro primer reto está en encontrar un lenguaje que tenga unas librerías que implementen un API que entienda Z39.50 y MARC. Como siempre que buscamos algo raro, la respuesta es Perl y sus librerías ZOOM y MARC.




Haremos pues un script en Perl que recibe de entrada un fichero con múltiples ISBN de libros, uno en cada línea, y los recorre secuencialmente, buscándolos en distintas bases de datos Z39.50 para recuper sus datos en formato MARC si los encuentra. Este registro en formato MARC será formateado a un fichero CSV, que luego será mucho más fácil de tratar desde una herramienta ofimática (hoja de cálculo o base de datos como Access) para incorporarlo a Abies o a cualquier otra aplicación de catálogo de libros.

Tan solo hay que lanzar el script en el mismo directorio donde esté el fichero libros.txt (que contiene los ISBN en formato numérico sin guiones, uno por línea - esos ISBN pueden haber sido leídos con un lector de código de barras) y, tras finalizar el proceso, tendremos la salida en dos ficheros:

  • libros.csv: todos los libros. En los encontrados aparecerán todos los datos que se han podido recuperar del registro MARC. En los no encontrados aparecerá solamente el ISBN y como título "NOTFOUND", para que podamos localizarlos fácilmente.
  • libros-notfound.csv: aparecen solo los libros no encontrados, por si queremos hacer algún proceso especial con ellos.
  •  
Para obtener los datos hacemos conexiones con tres bases de datos Z39.50:

  • sign.bne.es: base de datos de la Biblioteca Nacional, es de libre acceso.
  • rebeca.mcu.es: base de datos del Ministerio de Cultura, es de acceso restringido. Para conseguir el acceso hay que solicitar unas credenciales que te envían por correo ordinario (¿todavía andamos así?).
  • biblio15.uned.es: base de datos de la UNED, es de libre acceso.

Podemos añadir los servidores que queramos (muchas universidades tienen un servidor Z39.50), simplemente hay que buscar en Google los parámetros  de conexión necesarios, que son:

  • Nombre o IP del servidor
  • Puerto a usar
  • Nombre de la base de datos
  • Subformato MARC usado
  • Dado el caso, credenciales de acceso

Antes de incorporarlo un nuevo servidor al script está bien que  probemos la conexión y las búsquedas con el programa de consola yaz-client que viene en el paquete "yaz" de cualquier Linux. Es como cualquier programa de consola: permite conectar a una BBDD Z39.50 como si hiciésemos una conexión FTP y lanzar consultas sobre ella con un lenguaje propio de búsqueda bastante simple.

En la salida podemos volcar en el fichero csv cualquiera de los campos presentes en el registro MARC, en nuestro caso yo intento obtener los siguientes campos (que son los que tiene Abies):

  • Fecha 
  • Depósito Legal
  • ISBN
  • CDU
  • Autor
  • Título
  • Subtítulo
  • Resto Portada
  • Edición
  • Lugar de Edición
  • Editorial
  • Año de Edición
  • Extensión
  • Características Físicas
  • Tamaño
  • Serie
  • Número de Serie

Pero en principio podemos recuperar cualquiera de los datos especificados en el formato IBERMARC, que es el usado en la mayoría de las bases de datos españolas. La lista completa de campos IBERMARC es ésta.

Conviene advertir que, dependiendo de la diligencia del bibliotecario que hizo el catálogo, los campos pueden estar mejor o peor cumplimentados (o no rellenados en absoluto), pero siempre hay presente un mínimo de campos que nos darán información básica.

Además, cada base de datos puede tener sus particularidades de acceso. Viendo el código del script se comprobará que Rebeca es un poco quisquillosa con el formato del ISBN usado para la consulta.  Dado el caso, cada cual tendrá que retocar el script conforme a sus necesidades.

El script en cuestión es:
 #!/usr/bin/perl  

 use ZOOM;
 use MARC::Record;
 use feature qw{ switch };  

 #Para conectar y consultar con Rebeca, hace falta un usuario y una contraseña.
 #Que se solicita en http://www.mcu.es/bibliotecas/MC/Rebeca/ComoExtraerRegistros.html

 $usuRebeca="usuario-rebeca";
 $pwdRebeca="pasword-rebeca";  

 #Usaremos 3 conexiones distintas para probar: la BNE, Rebeca y la Biblioteca de la UNED
 #Cada una es un array con 3 valores
 #             1- Conexión a Z39.50 con el servidor en cuestión.
 #             2- Campo booleano: 0: el servidor acepta isbn sin formatear (ristra de números)
 #                                1: el servidor solo acepta isbn perfectamente formateados (con todos lo guiones bien puestos).
 #             3- Etiqueta que identifica la conexión.
 #
 @connBne = ( new ZOOM::Connection("sigb.bne.es", 2200, preferredRecordSyntax => "MARC21"), 0, "BNE");
 @connRebeca = (new ZOOM::Connection("rebeca.mcu.es", 210, databaseName => "absysrebeca", user => $usuRebeca, pass => $pwdRebeca, preferredRecordSyntax => "MARC21"), 1, "REBECA");
 @connUned = (new ZOOM::Connection("biblio15.uned.es", 2200, databaseName => "unicorn", preferredRecordSyntax => "MARC21"), 0, "UNED");  

 #Creamos un pool con las conexiones
 @conexiones = ( \@connUned, \@connBne, \@connRebeca );  

 $contlinea = 0;
 #La lista de ISBN de entrada está en el fichero "libros.txt", uno por línea en formato numérico
 $FILE = "libros.txt";
 open(FILE) or die("No puedo abrir el fichero de entrada.");  

 #Los datos de salida van al fichero libros.csv, en formato csv. Además, los libros no encontrados
 #se guardan también en libros-notfound.csv.
 open(OUTFILENOTFOUND, ">libros-notfound.csv");
 open(OUTFILE,">libros.csv");  

 #Cabecera del CSV. El separador usado es %%, es muy improbable que aparezca en un registro MARC
 print OUTFILE "Fecha1info%%Deposito%%ISBN%%CDU%%Autor%%Titulo%%Subtitulo%%RestoPortada%%Edicion%%LugarEdicion%%Editorial%%AnoEdicion%%Extension%%CaractFisicas%%Tamano%%Serie%%NumeroSerie\n";  

 print "Empezando..\n";
 foreach $codigo (<FILE>) {
   chomp($codigo);
   $codigo=trim($codigo);
   if ($codigo != "") {
           #Leemos el ISBN y lo formateamos con sus guiones, por si hace falta.

           $codigoformateado=formateaISBN($codigo); # Por ejemplo, Rebeca lo necesita con formato correcto, es bastante quisquillosa en eso.
           print "Procesando $codigo/$codigoformateado (linea $contlinea):";
           $consultaformateada='@attr 1=7 '.$codigoformateado;
           $consulta='@attr 1=7 '.$codigo;
           $encontrado=0;  

           #Para cada conexión del pool....
           for $elemento (0..@conexiones-1) {
                 $conexion = $conexiones[$elemento][0];  

                 #Hacemos la búsqueda usando el ISBN formateado o sin formatear, según el caso.
                 if ($conexiones[$elemento][1]) {
                      $rs = $conexion->search_pqf($consultaformateada);
                 }
                 else {
                      $rs = $conexion->search_pqf($consulta);
                 };              

                 #Si se ha encontrado
                 if ($rs->size() > 0 ) {
                         $linea = procesaRegistro($rs);
                         print OUTFILE $linea;
                         $encontrado=1;
                         last;
                  };
           }; #for
           if (!$encontrado ) {
                     print OUTFILENOTFOUND "%%%%$codigo%%%%%%NOTFOUND%%%%%%%%%%%%%%%%%%%%%%\n";
                     print OUTFILE "%%%%$codigo%%%%%%NOTFOUND%%%%%%%%%%%%%%%%%%%%%%\n";
           };
   }; #if
   $contlinea++;
 }; #foreach
 close(FILE);
 close(OUTFILENOTFOUND);
 close(OUTFILE);  

 #Procesa un registro en formato MARC.
 #y lo devuelve como un string con todos los campos interesantes separados por %%.
 sub procesaRegistro {
      local($resultset) = @_;
      $rec = $resultset->record(0);
      $raw = $rec->raw();
      $marc = new_from_usmarc MARC::Record($raw);
      $trans = $rec->render("charset=latin1,utf8");
      #Fecha publicacion, 6 primeros caracteres en formado aammdd
      $fecha1info = substr $marc->field("008")->as_string(),7,4; #Fecha publicación: los 6 primeros caracteres en formato aammdd
      #Depósito legal, en 019 segun Abies. 017 segun MARC. Se coge el 017.
      $depositolegal = $marc->subfield("017","a"); #
      $isbn = $marc->field("020")->as_string();
      if (defined($marc->field("080"))) {
              $cdu = $marc->field("080")->as_string();
      }
      else {
                 $cdu="";
      };
      $autor = $marc->subfield("100","a");
      $titulo = $marc->subfield("245","a");
      $subtitulo = $marc->subfield("245","b");
      $restoportada = $marc->subfield("245","c");
      $edicion = $marc->subfield("250","a");
      $lugaredicion = $marc->subfield("260","a");
      $editorial = $marc->subfield("260","b");
      $anoedicion = $marc->subfield("260","c");
      $extension = $marc->subfield("300","a");
      $caractfisicas = $marc->subfield("300","b");
      $tamano = $marc->subfield("300","c");
      # Titulo de la serie, en 490 segun Abies, en 440 segun marc. El 440 es obsoleto, pero ahi viene
      # la información de la BNE, por tanto usamos el 440.
      $serie = $marc->subfield("440","a");
      $numeroserie = $marc->subfield("440","c");
      return "$fecha1info%%$depositolegal%%$isbn%%$cdu%%$autor%%$titulo%%$subtitulo%%$restoportada%%$edicion%%$lugaredicion%%$editorial%%$anoedicion%%$extension%%$caractfisicas%%$tamano%%$serie%%$numeroserie\n";
 }  

 #Formatea en ISBN en un formato que pueda leer sin problemas REBECA.
 sub formateaISBN {
      local($codigo) = @_;
      $retorno = "";
      if ((length $codigo == 13) || (length $codigo == 10) )  {
           if (length $codigo == 13) { $retorno = "978-" };
           $isbn=substr $codigo,3;
           $pais=substr $isbn,0,2;
           if ($pais == "84") {
               $retorno = $retorno . "84";
               $editor = substr $isbn,2,1;
               given ($editor) {
                  when (/[01]/) {
                     $editor = substr $isbn,2,2;
                     $libro = substr $isbn,4,5;
                     $control = substr $isbn,9,1;
                  };
                  when (/[23456]/) {
                     $editor = substr $isbn,2,3;
                     $libro = substr $isbn,5,4;
                     $control = substr $isbn,9,1;
                  };
                  when (/[78]/) {
                     $editor = substr $isbn,2,4;
                     $libro = substr $isbn,6,3;
                     $control = substr $isbn,9,1;
                  };
                  when (/9/) {
                     $editor = substr $isbn,2,5;
                     $libro = substr $isbn,7,2;
                     $control = substr $isbn,9,1;
                  };
               };
               $retorno = $retorno . "-" . $editor . "-" . $libro . "-" . $control;
           }
           else  { $retorno= "ERROR"; }
      }
      else   { $retorno= "ERROR"; }
      return $retorno;
 }
 #Quitamos espacios en blanco al final y al principio de un string
 sub trim($)
 {
      my $string = shift;
      $string =~ s/^\s+//;
      $string =~ s/\s+$//;
      return $string;
 }


Un problema adicional con el que me encontré es que al usar Z39.50 unos puertos no convencionales, el firewall corporativo me cortaba el acceso. Para solucionar esto tendremos que solicitar que nos abran dichos puertos o bien ejecutarlo en un PC con salida directa a Internet.

El fichero csv de salida usa como separador de campos la cadena %%, hecho que debemos tener en cuenta al importarlo a otro programa. Elegí ese separador ya que es muy improbable que aparezca esa pareja de caracteres en los registros de un libro y nos fastidie el  procesado posterior.

La tasa de aciertos en las pruebas que he hecho ha sido del 80-85% de los libros buscados, lo cual no está nada mal. Nuestro bibliotecario nos lo agradecerá.

Una vez tenemos el fichero libros.csv viene la parte final. Esto dependerá del programa de gestión de bibliotecas dónde vayamos a cargar los datos. En el caso del Abies2 el método de importación a partir de una fuente externa viene explicado en http://pinakes.educarex.es/numero4/articulo18.htm.

Bueno, pues que ustedes lo importen bien.