Biblioteca digital (I)

Enviado por pvaldes el 12 Junio, 2013 - 19:17.

Un pequeño programa perl para hacer una búsqueda "natural" de archivos PDF

El formato de documento portátil de Adobe cumple este año 20 años desde el lanzamiento de su primera versión. Tras desplazar progresivamente a postscript y rebajar las expectativas de crecimiento de dvi, PDF es en la actualidad El Formato, con mayúsculas, usado para publicar y diseminar libros, revistas, artículos y todo tipo de conocimiento técnico entre ordenadores. Es habitual acabar reuniendo una pequeña biblioteca personal de referencia con cientos de archivos sobre nuestras neuras personales, que habremos descargado, solicitado directamente a sus autores o adquirido on-line y que se van acumulando a buen ritmo en nuestro sistema.

El caso es que como las revistas a menudo usan nombres poco descriptivos cuando queremos revisitar algún documento concreto y ha pasado cierto tiempo puede costar volver a localizarlo en el disco duro, de hecho puede que sea incluso más rápido volver a descargarlo desde la red. En principio sacar un listado de PDFs de un directorio es una tarea trivial (find . -iname '*.pdf') para la que no haría falta molestarse en escribir un post, pero hay un problema: Find es una herramienta de tipo técnico que no nos permite una búsqueda 'natural', simplemente no fue ideado con ese propósito y la mayor parte de sus opciones son inadecuadas en nuestro contexto. Si el articulo "Fernández et al. 1985" reside en un archivo llamado "AD113FE5C-2000.pdf", por ejemplo, no llegaremos muy lejos con find.

Pensemos en cambio en cómo buscaríamos realmente un libro conocido en nuestra biblioteca favorita. En la mayor parte de las veces probablemente ignoraremos los cajones de las fichas y nos desplazamos a la zona por la que debería estar. Una vez allí no iremos leyendo todas las etiquetas de referencias a ver si coinciden, sino que usaremos nuestro conocimiento previo del objeto. Buscaremos, por ejemplo un libro delgado azul con una banda blanca en el lomo, y eso nos permite discriminar más rápidamente porque lo natural para el cerebro es memorizar una imagen de los objetos que conocemos. Del mismo modo, ya que un PDF no es otra cosa que una representación de un objeto real, aunque es muy poco probable que recordemos su -mtime, o cuando lo hemos abierto por última vez, sin duda sabremos sin esfuerzo si el documento que buscamos era un libro gordo, una revista, un artículo, un póster de congreso o una tabla de referencia tamaño tarjeta de crédito con un resumen de comandos básicos.

Y ese es el asunto en el que me quiero centrar hoy. Vamos a ver como resolver problemas del tipo: "Encontrar entre los cientos de archivos en formato PDF diseminados por varios árboles de directorios de nuestro disco duro aquellos que representen a un libro de al menos 18x25 cm de portada y entre 75 y 90 páginas de extensión", o "encontrar el documento de mayor tamaño (físico) de nuestro sistema", y lo vamos a hacer con Perl.



Hay que empezar advirtiendo de las limitaciones, a diferencia de find, a menudo vamos a trabajar sobre un subconjunto de todos los archivos del directorio que cumplan nuestras condiciones, no sobre el total de archivos. Esto se debe a que lo que lo que se entiende por PDF en realidad es un conjunto heterogéneo de archivos de versiones diferentes, creados con diferentes programas de un modo más o menos fiel al estándar. Algunas versiones más modernas, o no totalmente conformes al protocolo, no tendrán soporte, y además está el asunto de los permisos, claro...

Este formato fue diseñado con un sofisticado sistema de permisos, superior al clásico "lectura-escritura-ejecución", para proteger su contenido de modificaciones posteriores (algo muy importante a la hora de transmitir fielmente un conocimiento científico o un texto literario). Sin entrar mucho en detalles, el acceso a la información interna del archivo puede bloquearse de varias maneras sin evitar su lectura, en estos casos el programa no tiene modo de obtener la información que necesita, descarta el archivo y lo intentará con el siguiente de la lista. A menos que nos molestemos en romper la contraseña del propietario, tendremos que asumir que algunos archivos pueden no aparecer en el resultado final.

Y sin más dilaciones, aquí está el código que usaremos, podemos examinarlo, copiarlo a un archivo y darle permisos de ejecución antes de lanzarlo en un terminal bash:

#!/usr/bin/perl

=encoding utf8

=head1 NAME

B<buscapdf> - Buscar documentos pdf que cumplan determinadas condiciones dentro de uno o varios directorios

=cut

use strict; use Getopt::Long; use Pod::Usage; use CAM::PDF; use File::Find; use Term::ANSIColor qw(:constants);

our $VERSION = '1.73';

# factor de conversion de pto a mm.
use constant mm => 25.4/72;

my %lista = (); my (@pags, @dir); my $cont = 0; my $err = 0;

local $SIG{__WARN__} = sub {
    warn(print RED, "\n@_", RESET);
    $err++;
    return 1;
};

# valor de opciones por defecto
my %opcion = (man => 0, help => 0, verbose => 0, resumen => 0, ancho => 1, alto => 1);

Getopt::Long::Configure('auto_version');
GetOptions(
    'h|help|?'       => \$opcion{help},
    'man'            => \$opcion{man},
    'pags=i{2}'      => \@pags,
    'a|ancho=f'      => \$opcion{ancho},
    'l|alto|largo=f' => \$opcion{alto},
    'D|dir=s'        => \@dir,
    'v|verbose'      => \$opcion{verbose},
    'r|resumen'      => \$opcion{resumen},
    ) or pod2usage(-verbose => 0);

if(!$pags[0]){$pags[0] = "1"};
if(!$pags[1]){$pags[1] = "100000"};

pod2usage(-verbose => 1, -exitstatus => 0) if $opcion{help};
pod2usage(-verbose => 2, -exitstatus => 0) if $opcion{man};

my $mensaje = "Buscando archivos con número de págs entre $pags[0] y $pags[1] y al menos $opcion{ancho} x $opcion{alto} mm...\n";

if($opcion{verbose}){
       print BOLD, BLACK, $mensaje, RESET; print BLACK, "\n", RESET;
   }

   find (sub {
if (-f && !-l && $File::Find::name =~ m/\.pdf$/i){
            my $pdf = CAM::PDF->new($File::Find::name) or
warn "$File::Find::name,\n    $CAM::PDF::errstr";
    if ($pdf && $pdf->numPages()>=$pags[0] && $pdf->numPages()<=$pags[1]) {
    (undef,undef,$lista{$File::Find::name}{ancho}, $lista{$File::Find::name}{alto}) = $pdf->getPageDimensions('1') or
warn "$File::Find::name, $CAM::PDF::errstr\n";
    if (!$lista{$File::Find::name}{ancho}){$lista{$File::Find::name}{ancho} = 1};
    if (!$lista{$File::Find::name}{alto}){$lista{$File::Find::name}{alto} = 1};
    if($lista{$File::Find::name}{ancho}*mm >= $opcion{ancho} && $lista{$File::Find::name}{alto}*mm >= $opcion{alto}){
print "\n", $File::Find::dir,"\/";
print BOLD $_," ", RESET;
if ($opcion{verbose}){
    print GREEN "\n   ",length $pdf->{content}," bytes,", RESET;}
if ($pdf->numPages() == 1){
    print BLUE " ", $pdf->numPages()," pág ", RESET}
else {
    print BLUE " ", $pdf->numPages()," págs ",RESET}
printf "de %d x %d" ,$lista{$File::Find::name}{ancho}*mm, $lista{$File::Find::name}{alto}*mm;
print " mm", RESET;
if($opcion{verbose}){
    print "    permisos: prnt ",$pdf->canPrint(),", modif ",$pdf->canModify(),", copy ", $pdf->canCopy(),", add ", $pdf->canAdd();
    print "\n";}
$cont++;
    }}}}, @dir);

print "\nAcabado.\n";
if ($opcion{resumen}){
    print "   Se encontraron $cont archivos de $pags[0]-$pags[1] págs y al menos $opcion{ancho} x $opcion{alto} mm en @dir.\n";
    print "   $err archivos adicionales no pudieron abrirse y fueron descartados.\n\n";}

__END__

=head1 SYNOPSIS

Busca documentos pdf por núm de páginas y dimensiones de la portada dentro de los directorios indicados.

B<buscapdf> [opciones] -d directorio  [-d dir...]

B<buscapdf> --help

B<buscapdf> --man

B<buscapdf> --version

=head2 OPTIONS

=over 11

=item B<--verbose, -v>         Opcional. Imprime más información sobre los archivos

=item B<--resumen, -r>         Opcional. Imprime un resúmen final

=item B<--pags> min max        Opcional. Especifica rango min y max de págs deseadas

=item B<--ancho> num           Opcional. Especifica ancho min (en mm) de la primera página del documento.

=item B<--largo> num           Opcional. Especifica largo min (en mm) de la primera pagina del documento.

=item B<-d, --dir> directorio  Directorio en el que vamos a buscar

=back

=head1 DESCRIPTION

Este programa lista los documentos de tipo pdf presentes en uno o varios directorios (y sus subdirectorios) que cumplan determinadas condiciones. Para buscar en varios árboles de directorios a la vez se puede repetir la opción --dir tantas veces como sea necesaria (tiene que haber una al menos).

La opción B<pags> permite acotar la búsqueda a un rango de páginas dado. Lleva dos números enteros separados por un espacio indicando el número mínimo y máximo de págs que puede tener el documento buscado (ej --pags 2 4 busca documentos de 2, 3 y 4 págs).

La salida indica la ruta completa al archivo, el núm de páginas y sus dimensiones. Si se incluye la opcion B<verbose> se indican además el peso en bytes del archivo y los valores para los campos: imprimible, copiable, modificable y extensible (donde 1 indica verdadero y 0 falso). Si se incluye la opcion B<resumen> se imprime ademas un breve resumen final.

=head1 CAVEATS & BUGS

El número de archivos mostrados puede ser menor al real indicado por find, ya que algunos archivos protegidos o no conformes al protocolo pueden no permitir que se intente acceder a sus metadatos internos. En ese caso el archivo se descartará indicando su nombre y la causa del error. Debido a la propia naturaleza del formato PDF no hay una solución sencilla para evitar esto.

Los documentos en formato pdf pero sin extensión .pdf no son tenidos en cuenta por el programa.

=head1 EJEMPLOS

Para buscar posters de congresos en los directorios A y B use -pags 1 1 y un valor grande de longitud:

./buscapdf -pags 1 1 -L 2000 --dir /dirA --dir /dirB

Para buscar artículos científicos consulte en la bibliografía el número exacto de páginas del archivo de interés o bien use un rango bajo que sea aproximado

./buscapdf -pags 2 20 -d /dir

Las opciones verbose y resumen muestran información adicional (pero hay que escribirlas por separado, la opción -rv no es reconocida)

./buscapdf -v -r -d /dir

=head1 VEA TAMBIEN

CAM::PDF y FILE::FIND

=head1 AUTHOR

Copyright 2013 Pvaldes.

This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut

La opción --man del programa nos da acceso a toda la documentación.

La salida con verbose tendrá este aspecto:

...
/usr/share/doc/texlive-doc/latex/tools/enumerate.pdf
   59262 bytes, 3 págs de 210 x 297 mm    permisos: prnt 1, modif 1, copy 1, add 1
/usr/share/doc/texlive-doc/latex/ctanify/ctanify.pdf
   14801 bytes, 3 págs de 215 x 279 mm    permisos: prnt 1, modif 1, copy 1, add 1


Apéndice: Cómo encontrar un modulo perl en Debian

Para usar el programa necesitamos instalar los módulos indicados. Existen literalmente cientos de módulos disponibles para Perl que podemos instalar directamente desde CPAN. Su uso es muy sencillo pero tendremos que tener en mente actualizar el módulo por separado de vez en cuando.

Sin embargo si hay un módulo empaquetado y probado en Debian lo más seguro es usarlo y evitarnos así mezclar varias versiones y tener que actualizar los módulos a mano. Pero para eso tenemos que encontrarlo y Debian en esto tiene algunas particularidades que conviene comentar. Por ejemplo, si tratamos de hacer lo siguiente obtendremos un error:

apt-get install cam-pdf o apt-cache search ^cam-pdf

Sin embargo el paquete del módulo existe y sólo hay que saber como buscarlo. El nombre de los paquetes debian con modulos de perl sigue (casi siempre) el siguiente esquema

lib___-perl

donde el nombre del módulo ocupa el lugar intermedio señalado por los guiones bajos, por tanto para instalar el módulo cam-pdf usaremos:

apt-get install libcam-pdf-perl

Y ahora sí funcionará. A veces en Debian no tendremos empaquetado exactamente el módulo deseado, sino otro similar basado en éste pero que proporcionan una funcionalidad similar, por ejemplo para instalar GetOpt::Long podemos usar el similar:

apt-get install libgetopt-long-descriptive-perl

Un tercer tipo de módulos que no siguen esta regla, son aquellos como Term::ANSIColor, que debido a su popularidad o importancia entran juntos bien en perl-modules o bien en perl y ya tendremos instalados.

Y un cuarto tipo directamente van por libre, como perlmagick del que hablaremos probablemente en futuras entradas.

Imagen de Debish
Enviado por Debish el 15 Junio, 2013 - 17:07.

Como siempre una original entrada. Voy pilladísimo de tiempo, en cuanto pueda miro con detenimiento el script y lo pruebo con mi macrobiblioteca, a ver qué tal.

Gracias.

Imagen de Black Rider
Enviado por Black Rider el 16 Junio, 2013 - 22:17.

No sé yo, la idea está curiosa en principio, pero practicidad, practicidad... Lo suyo sería programar un motor de búsqueda que escanease los contenidos de los PDF.

En la vida real, para ubicar un artículo en mi colección de Linux Magazines, lo que hago es ir al número de la revista donde me suena que está y mirar la portada mrgreen

Imagen de pvaldes
Enviado por pvaldes el 16 Junio, 2013 - 23:00.

Gracias a ambos por los comentarios

> Lo suyo sería programar un motor de búsqueda que escanease los contenidos de los PDF.

Esto es sólo un principio, nada impide ampliar el programa para que extraiga texto o fotos o permita buscar por patrones, pero ten en cuenta que es un método mucho más propenso a errores.

Si tienes una cita bibliográfica tienes el número exácto de páginas. Eso simplifica bastante la búsqueda... salvo en el caso que indicas, que sea una revista entera.

Escanear un PDF a texto en varias columnas sin perder el sentido de la lectura o ignorar parte del contenido puede ser complicado pero es posible y si puedes vivir con unos cuantos errores es relativamente simple hacerlo... pero es matar moscas a cañonazos, (imagina que tienes que escanear un libro de 300 pags). Tengo un par de modificaciones de este script en desarrollo, una de ellas extrae los primeros N caracteres de la página X, para cualquier N y X pasados como argumentos del programa... de momento falla y cuelga el script con excesiva frecuencia.

Imagen de Black Rider
Enviado por Black Rider el 17 Junio, 2013 - 00:33.

Lo suyo sería que el script identificase los índices (en caso de haberlos), los marcadores y esas cosas. Tampoco estoy muy metido en las especificaciones del formato, pero me suena que era un poco informe y caótico, así que a saber cómo lo agarra uno.

Cuando tengo muchos archivos de texto puros y duros y no sé dónde se encuentra algo, corro grep sobre ellos sin más...

Imagen de pvaldes
Enviado por pvaldes el 17 Junio, 2013 - 10:39.

Totalmente de acuerdo, grep va muy bien con archivos de texto