Raccogliere metadati con Perl, l'esempio Bibliotime

di Gianni Colussi

1   Introduzione

Bibliotime [1] è una rivista elettronica italiana per le biblioteche, gratuita e a libero accesso sul World Wide Web. Perl [2] è un linguaggio di programmazione col quale si possono fare tante cose, ma in una soprattutto si rivela utilissimo: il parsing di file testuali come sono quelli scritti in linguaggio di codifica HTML. Si è intrapreso questo lavoro per fornire file strutturati al servizio DoIS [3] .

È vero che si può accorpare il tutto in una sola megaistruzione, ma per semplicità di analisi ho diviso in due parti l'applicazione, una riguardante il modo in cui si vanno a spulciare i link relativi ai singoli articoli del fascicolo, la seconda che riguarda l'analisi vera e propria di ogni articolo e l'estrazione dei dati di nostro interesse [4] .

2.1   Prime linee di codice: contattare la pagina indice di Bibliotime

Guardiamo adesso da vicino il codice utilizzato. Chiamerò questo script copialink.pl .

Prima di tutto bisogna sempre richiamare l'interprete, trascrivendo il percorso completo che porta alla sua locazione.

Nelle piattaforme UNIX di solito sta qui:

#!/usr/bin/perl

Il cancelletto (simbolo di commento) immediatamente seguito dal punto esclamativo (simbolo di negazione) vuol dire che si tratta dell'unico commento che il sistema può riconoscere, mentre tutti gli altri vengono ignorati: in pratica un non-commento.

La locazione dell'interprete non è standard e potrebbe anche trovarsi a:

#!/usr/local/bin/perl

secondo le necessità dell'amministratore di un server.

Nelle piattaforme WINDOWS questa prima linea dello script non dovrebbe essere importante, a meno che non lo si voglia far riconoscere a un server web (quando per esempio lo script Perl deve agire come uno script CGI), nel qual caso si deve essere precisi e scrivere il percorso (con le stesse barre inclinate a destra di unix):

#!c:/perl/perl.exe

Entriamo qui in una sezione dedicata all'invocazione di direttive interne, dette in inglese pragmas, che servono a rafforzare il controllo del codice, in modo che il programmatore non lasci dubbi sulla natura di un costrutto. Ad esempio il pragma strict serve a costringere a dichiarare variabili locali, di uso non globale.

use strict;

Il pragma warnings è di uso più intuitivo. L'interprete ci avviserà di ogni non chiaro funzionamento nella compilazione che avverrà in fase di run-time.

use warnings;

È ora il momento di richiamare una delle librerie più importanti per i nostri scopi: LWP. Si tratta di una collezione di moduli Perl che realizzano un interfacciamento con il Web. Trasformano il nostro script in un client che chiede documenti ad un server (proprio come i più conosciuti browsers (Internet Explorer, Mozilla o Netscape che tutti conoscono). Nel nostro caso LWP::UserAgent consiste in un oggetto di programmazione più completo e ricco di funzionalità rispetto all'uso di LWP::Simple che renderebbe disponibile solo una semplice funzione get(). Quest'ultima, come si sa, prende un URL, ne ricupera i contenuti e restituisce il corpo della risposta.

use LWP::UserAgent;
use HTML::LinkExtor;
HTML::LinkExtor è un parser che estrae links da un documento HTML. Creando un oggetto (di programmazione) di questo tipo gli si assegnano le proprietà contenute nella routine callback
use URI::URL;
my $url = "http://path/to/Bibliotime/index.html";; 
my $ua = LWP::UserAgent->new;
my @links = ();
sub callback {
     my($tag, %attr) = @_;
     return if $tag ne 'a';  
     push(@links, values %attr);
}
my $p = HTML::LinkExtor->new(\&callback);
my $res = $ua->request(HTTP::Request->new(GET => $url),
             sub {$p->parse($_[0])});

my $base = $res->base;
@links = map { $_ = url($_, $base)->abs; } @links;
open(OUT, ">btlist.txt");
print OUT join("\n", @links), "\n";
close(OUT);
exit;

2.2   Altre linee di codice: trovare, estrarre e riscrivere meta-dati per DoIS

Invocazione dell'interprete perl, come nell'esempio precedente:

#!/usr/bin/perl 

Invocazione delle direttive di controllo, come nell'esempio precedente:

use strict;
use warnings;

Le linee sono mandate a capo dopo un numero di caratteri indicato nella variabile scalare $columns.

use Text::Wrap qw(wrap $columns);$columns=92;
use LWP::Simple;

Qui serve semplicemente il metodo get()...

my ($blist, $page, $outf, $nrtem);
my ($jnl, $vol, $iss, $au, $ti, $pubmo, $pubyr, $url);
my ($str, @str, $auth, @auv);

Sulla base di una lista di URL forniti da btlist.txt questo script, che chiamerò btlist.pl , ottiene una copia di ciascuna risorsa e costruisce una scheda della stessa in formato ReDIF (.rdf) .

$blist='btlist.txt';
open(IN,$blist) || die 'cannot open $blist';

Inizializza un contatore delle maschere ReDIF create.

$nrtem=0;

Inizia un ciclo while di lettura linea per linea di $blist, mentre la funzione chomp taglia il carattere finale della linea che non interessa e disturba

while (<IN>) {
chomp;
print "processing $_ \n";

Nel frattempo scrive sullo schermo (lo standard output) il nome del file che si sta processando

$page = get "$_";
$url = "$_";
($outf) = $_  =~ m!^.+?/num.+?/(.+?)\.htm!;
open(OUT,">>bt.rdf") || die "Cannot open bt.rdf for output";

Si poteva trovare forse un algoritmo più compatto, come anche usare un modulo capace di lavorarci su, ma ho seguito l'impostazione elementare degli script del sito RePEC, anche per mostrare in cosa effettivamente consista il lavoro di sostituzione di certi caratteri che ISO 8859-1 non supporta

$page =~ s/\n//ig;
che significa: rintraccia tutte le occorrenze del carattere newline in tutta la variabile, ignorando anche la differenza tra maiuscole e minuscole, e sostituiscilo con un carattere null.

di seguito sono riportate altre combinazioni:

$page =~ s/\r//ig;
$page =~ s/*//ig;
$page =~ s/ö/o/ig;
$page =~ s/ò/o/ig;
$page =~ s/&/&/ig;
$page =~ s/ç/c/ig;
$page =~ s/ä/a/ig;
$page =~ s/ü/u/ig;
$page =~ s/é/e/ig;
$page =~ s/à/a/ig;
$page =~ s/è/e/ig;
$page =~ s/ñ/n/ig;
$page =~ s/é/e/ig;
$page =~ s/è/e/ig;
$page =~ s/ü/u/ig;
$page =~ s/<i>//ig;
$page =~ s/<\/i>//ig;
(@str) = split(/\012/, $page);
foreach $str (@str) {
$jnl = "";
($jnl) = ($str =~ /<TITLE>(.+?),/igm);
($vol) = ($str =~ /<TITLE>.+?,\s(.+?),/igm);
($iss) = ($str =~ /<TITLE>.+?,\s.+?,\s(.+?)\s-/igm);
($au) = ($str =~ /<TITLE>.+?,\s.+?,\s.+?\s-\s(.+?),/igm);
if ($url =~ /editoria.htm$/) {
($ti) = ($str =~ /<TITLE>.+?,\s.+?,\s.+?\s-\s(.+?)<\/TITLE>/igm);
}
else {
($ti) = ($str =~ /<TITLE>.+?,\s.+?,\s.+?\s-\s.+?,\s(.+?)<\/TITLE>/igm);
}
($pubmo) = ($str =~ /anno\s.+?,\snumero\s.+?\s\((.+?)\s.+?\)/igm);
($pubyr) = ($str =~ /anno\s.+?,\snumero\s.+?\s\(.+?\s(.+?)\)/igm);
if (length $jnl > 1) {
print OUT "Template-Type: ReDIF-Article 1.0\n";

Il formato ReDIF richiede l'indentatura dei paragrafi più lunghi di una linea (cioè la riga delimitata da un carattere di a capo, il newline). Se il valore del campo di un record è lungo più di una linea, allora le linee successive alla prima cominciano con uno spazio o una tabulazione. Per la bisogna ci soccorre il modulo della libreria interna denominato Text::Wrap. L'indentazione è controllata in maniera differente per la prima riga (con la variabile scalare $initial_tab) e per le successive (con la variabile scalare $subsequent_tab). La funzione viene dunque applicata a quei campi che si prevedono multilineari.

print OUT ("Title:", wrap($initial_tab,$subsequent_tab,$ti));
oppure, sostituendo alle variabili delle semplici stringhe scalari separate da virgole:
print OUT ("Title:", wrap(""," ",$ti));
	print OUT "\n";
if ($url =~ /editoria.htm$/) {
	print OUT "Author-Name: Michele Santoro \n";
	}
	elsif ($au =~ / e /) {
	(@auv) = split(" e ", $au);
	foreach $auth (@auv) {
	print OUT "Author-Name: $auth \n";
	}
	}
	else {
	print OUT "Author-Name: $au \n";
	}
print OUT "Journal: $jnl \n";
print OUT "Issue: $iss \n";
print OUT "Volume: $vol \n";
print OUT "Month: $pubmo \n";
print OUT "Year: $pubyr \n";
print OUT "File-URL: $url \n";
print OUT "File-Format: text\/html \n";
print OUT "Handle:
ReLIS:btm:bibtim:v:$vol:y:$pubyr:m:$pubmo:i:$iss:p:$outf \n";
print OUT "\n";
$nrtem++;
}
}
close(OUT);
}
print " $nrtem templates processed \n";
exit;

Così si presentano per esempio due record di file in formato ReDIF pronti per il caricamento su DoIS.

Template-Type: ReDIF-Article 1.0
Title: Canone inverso
Author-Name: Michele Santoro 
Journal: Bibliotime 
Issue: 2 
Volume: V 
Month: luglio 
Year: 2002 
File-URL: http://www.spbo.unibo.it/bibliotime/num-v-2/editoria.htm 
File-Format: text/html 
Handle: ReLIS:btm:bibtim:v:V:y:2002:m:luglio:i:2:p:editoria 

Template-Type: ReDIF-Article 1.0
Title: Open Archive. Per una comunicazione scientifica 'free online'
Author-Name: Antonella De Robbio 
Journal: Bibliotime 
Issue: 2 
Volume: V 
Month: luglio 
Year: 2002 
File-URL: http://www.spbo.unibo.it/bibliotime/num-v-2/derobbio.htm 
File-Format: text/html 
Handle: ReLIS:btm:bibtim:v:V:y:2002:m:luglio:i:2:p:derobbio 

3.   Conclusione

Come si e visto è stato possibile ricavare comunque dei meta-dati anche da chi non li aveva coscientemente inseriti, non disdegnando talvolta di arrampicarsi sui vetri. Conditio sine qua non per riuscire nell'impresa era ed è il mantenimento di una rigorosa coerenza editoriale, la disposizione ordinata del filesystem (cartelle contenenti fascicoli e annate), il libero accesso, pregi che non si possono negare.

4.   Appendice

Riporto qui interamente il codice di copialink.pl:

#!/usr/bin/perl 
use strict;
use warnings;
use LWP::UserAgent;
use HTML::LinkExtor;
use URI::URL;
my $url = "http://path/to/Bibliotime/index.html";; 
my $ua = LWP::UserAgent->new;
my @links = ();
sub callback {
     my($tag, %attr) = @_;
     return if $tag ne 'a';  
     push(@links, values %attr);
}
my $p = HTML::LinkExtor->new(\&callback);
my $res = $ua->request(HTTP::Request->new(GET => $url),
             sub {$p->parse($_[0])});
my $base = $res->base;
@links = map { $_ = url($_, $base)->abs; } @links;
open(OUT, ">btlist.txt");
print OUT join("\n", @links), "\n";
close(OUT);
exit;

E di seguito il codice di btlist.pl.

#!/usr/bin/perl 
use strict;
use warnings;
use Text::Wrap qw(wrap $columns);
$columns=92;
use LWP::Simple;
my ($blist, $page, $outf, $nrtem);
my ($jnl, $vol, $iss, $au, $ti, $pubmo, $pubyr, $url);
my ($str, @str, $auth, @auv);
$blist='btlist.txt';
open(IN,$blist) || die 'cannot open $blist';
$nrtem=0;
while (<IN>) {
chomp;
print "processing $_ \n";
$page = get "$_";
$url = "$_";
($outf) = $_  =~ m!^.+?/num.+?/(.+?)\.htm!;
open(OUT,">>bt.rdf") || die "Cannot open bt.rdf for output";
$page =~ s/\n//ig;
$page =~ s/\r//ig;
$page =~ s/*//ig;
$page =~ s/ö/o/ig;
$page =~ s/ò/o/ig;
$page =~ s/&/&/ig;
$page =~ s/ç/c/ig;
$page =~ s/ä/a/ig;
$page =~ s/ü/u/ig;
$page =~ s/é/e/ig;
$page =~ s/à/a/ig;
$page =~ s/è/e/ig;
$page =~ s/ñ/n/ig;
$page =~ s/é/e/ig;
$page =~ s/è/e/ig;
$page =~ s/ü/u/ig;
$page =~ s/<i>//ig;
$page =~ s/<\/i>//ig;
(@str) = split(/\012/, $page);
foreach $str (@str) {
$jnl = "";
($jnl) = ($str =~ /<TITLE>(.+?),/igm);
($vol) = ($str =~ /<TITLE>.+?,\s(.+?),/igm);
($iss) = ($str =~ /<TITLE>.+?,\s.+?,\s(.+?)\s-/igm);
($au) = ($str =~ /<TITLE>.+?,\s.+?,\s.+?\s-\s(.+?),/igm);
if ($url =~ /editoria.htm$/) {
($ti) = ($str =~ /<TITLE>.+?,\s.+?,\s.+?\s-\s(.+?)<\/TITLE>/igm);
}
else {
($ti) = ($str =~ /<TITLE>.+?,\s.+?,\s.+?\s-\s.+?,\s(.+?)<\/TITLE>/igm);
}
($pubmo) = ($str =~ /anno\s.+?,\snumero\s.+?\s\((.+?)\s.+?\)/igm);
($pubyr) = ($str =~ /anno\s.+?,\snumero\s.+?\s\(.+?\s(.+?)\)/igm);
if (length $jnl > 1) {
print OUT "Template-Type: ReDIF-Article 1.0\n";
print OUT ("Title:", wrap(""," ",$ti));
	print OUT "\n";
if ($url =~ /editoria.htm$/) {
	print OUT "Author-Name: Michele Santoro \n";
	}
	elsif ($au =~ / e /) {
	(@auv) = split(" e ", $au);
	foreach $auth (@auv) {
	print OUT "Author-Name: $auth \n";
	}
	}
	else {
	print OUT "Author-Name: $au \n";
	}
print OUT "Journal: $jnl \n";
print OUT "Issue: $iss \n";
print OUT "Volume: $vol \n";
print OUT "Month: $pubmo \n";
print OUT "Year: $pubyr \n";
print OUT "File-URL: $url \n";
print OUT "File-Format: text\/html \n";
print OUT "Handle:
ReLIS:btm:bibtim:v:$vol:y:$pubyr:m:$pubmo:i:$iss:p:$outf \n";
print OUT "\n";
$nrtem++;
}
}
close(OUT);
}
print " $nrtem templates processed \n";
exit;

Note

[*] Per la sintassi degli script sono debitore di tutti gli esempi forniti liberamente al sito <http://ideas.uqam.ca/ideas/data/rpcscript.html>.

[1] L'indirizzo di Bibliotime è <http://www.spbo.unibo.it/bibliotime/>.

[2] Per una introduzione su cosa è Perl e sulla maniera di renderne l'ortografia vedi
<http://pod2it.sourceforge.net/pods/perlfaq1.html#cos'%20il%20perl >.

[3] "DoIS è un servizio internazionale che raccoglie articoli di periodici, comunicazioni presentate a convegni e conferenze, rapporti di ricerca, disponibili in formato elettronico in rete" [Antonella De Robbio] <http://eprints.rclis.org/archive/00001732/>.
DoIS è raggiungibile a: <http://dois.mimas.ac.uk/>.

[4] Se Bibliotime avesse esposto i metadati nel classico elemento META e nel formato Dublin Core, la raccolta sarebbe stata certamente facilitata e avrei forse potuto adoperare HTML::DublinCore, il modulo Perl di Ed Summers, release del 25/8/2003. Ma né l'uno né gli altri erano disponibili al momento in cui ho affrontato per la prima volta questo compito (primavera 2002). Il modulo di Ed Summers è all'indirizzo: <http://search.cpan.org/~esummers/HTML-DublinCore-0.3/>.