Flavio Poletti
Cinque cose che già sapevate di poter fare in Perl
http://www.perl.it/documenti/articoli/2006/10/cinque-cose-che-1.html
© Perl Mongers Italia. Tutti i diritti riservati.

Vincitore Contest2005

Non riscrivere i PDF - riusali!

Di recente mi sono lasciato sedurre dal libro Getting things done: The Art of Stress-Free Productivity di David Allen. Di che si tratta? Di un sistema di accorgimenti utili per non dimenticare di fare le cose e ridurre lo stress creato dalle stesse, che tendono a perseguitarti finché non le hai completate. La soluzione, senza temere di rovinare niente del libro, consiste semplicemente nello scriversele. Geniale, no?

Nella mia strada verso la redenzione dalla smemoratezza ho avuto anche la fortuna di incontrare qualche buon samaritano. Un ottimo esempio è costituito dal sito dedicato al D*I*Y Planner (http://www.diyplanner.com/), una vera e propria agenda/planner che potete stamparvi da soli, includendo i moduli che volete. Non sto qui a tediarvi sul come e sul cosa, ma io sono rimasto particolarmente colpito dalla versione hipster PDA del planner suddetto, che viene fornito in varie forme, la più interessante è quella in PDF con quattro pagine per foglio (trovate il tutto qui: http://www.diyplanner.com/templates/official/hpda).

Questo bel documentino PDF ha 29 facciate, ognuna fondamentalmente dedicata ad un differente aspetto dell'organizzazione; ognuno può sceglierne le parti che ritiene più utili, tralasciando le altre. A questo punto sono sorti i miei problemi:

  • isolare solamente le pagine che io ritenevo utili;
  • raddoppiare alcune delle pagine suddette, per fare in modo da avere schedine stampate su tutti e due i lati e risparmiare spazio (posso portare la metà delle schede, no?).

E qui, finalmente, entra in ballo Perl insieme al suo modulo PDF::Reuse.

Un po' di contesto

Il modulo in questione lo potete trovare su CPAN, consiglio una ricerca tipo PDF::Reuse. La descrizione è particolarmente illuminante, mi permetto di tradurla:

Questo modulo può essere utilizzato quando volete produrre, in massa, documenti PDF simili ma non identici fra loro e riutilizzare modelli, script JavaScript e qualche altra componente. È progettato per essere veloce, e per dare ai vostri programmi la capacità di produrre molte pagine al secondo e documenti PDF molto grandi, se necessario.

Magari non abbiamo bisogno di andare tanto veloci, però l'idea del riutilizzo è stuzzicante!

Getting (this) thing done!

Andiamo ad utilizzare il modulo per i nostri biechi scopi, dunque. Il nostro obiettivo è scrivere uno script che ci consenta di selezionare le pagine desiderate dal file di partenza, in un ordine dato da noi e con ripetizioni (per poter stampare fronte-retro senza troppo lavoro di stampante). Tanto per perdere un po' di tempo, faremo in modo da implementare la seguente sintassi sulla riga di comando:

  • specificare un numero di pagina;
  • specificare una pagina ripetuta con PPxNN (PP è il numero di pagina, NN il numero di ripetizioni);
  • specificare un intervallo di pagine con PI-PF (PI è il numero di pagina iniziale, PF è il numero di pagina finale)
  • impostare un fattore di ripetizione con xNN (NN è un numero);
  • poter separare ciascun comando con spazi o virgole

Mi sembra abbastanza, non indugiamo oltre:

  1     #!/usr/bin/perl
  2     use strict;
  3     use warnings;
  4     use PDF::Reuse;
    
  5     my $ifile = shift;
  6     my $reps  = 1;
    
  7     # Apro il documento PDF di uscita - viene inviato su STDOUT
  8     # perche' non diamo alcun argomento a prFile()
  9     prFile();
   
 10     # Itero sui vari comandi in ingresso, che possono essere stati
 11     # separati in chiamata (suddivisi dunque in @ARGV), oppure possono
 12     # essere intervallati da spazi o virgole (da cui la map/split).
 13     foreach my $cmd (map { split /[ ,]/, $_ } @ARGV) {
       
 14        # Per default, il comando e` semplicemente un numero di pagina,
 15        # per cui $start e $end sono inizializzate con essa. $itreps
 16        # memorizza il numero di ripetizioni per questa particolare
 17        # iterazione, e per default e` uguale all'impostazione globale
 18        my ($start, $end, $itreps) = ($cmd, $cmd, $reps);
    
 19        # Analizza se il comando non specifica semplicemente un numero
 20        # di pagina
 21        if ($cmd =~ /^x(\d+)$/) {    # xNN
 22           $reps = $1;               # Imposta ripetizioni
 23           next;                     # e prosegui con il prossimo comando
 24        }
 25        elsif ($cmd =~ /^(\d+)x(\d+)$/) {    # PPxNN
 26           $start = $1;                      # Inizio e fine coincidono con $1,
 27           $end   = $1;                      # la prima parte di PPxNN
 28           $itreps *= $2;    # Le ripetizioni per questa iterazione
 29        }
 30        elsif ($cmd =~ /^(\d+)-(\d+)$/) {    # PI-PF
 31           $start = $1;
 32           $end   = $2;
 33        }
    
 34        # Posso determinare quali sono le pagine da inserire in questa
 35        # iterazione. Ciascuna pagina nell'intervallo $start .. $end
 36        # viene ripetuta per un numero $itreps di volte
 37        my @pages = map { ($_) x $itreps } $start .. $end;
    
 38        # Itero sulle pagine determinate e le estraggo dal documento
 39        # di input. Per ciascuna di esse stampo anche un feedback su
 40        # STDERR
 41        foreach my $page (@pages) {
 42           prDoc($ifile, $page, $page);
 43           print STDERR "$page ";
 44        }
 45     } ## end foreach my $cmd (map { split...
 46     print STDERR "\n";
    
 47     # Chiudo il documento PDF di uscita
 48     prEnd();

Il primo blocco (righe 1-4) è roba già vista, vero? Si imposta l'interprete di default per Unix, si utilizzano strict e warnings tanto per stare sicuri, e si utilizza PDF::Reuse perché.... altrimenti non andremo molto lontano.

Il primo argomento a linea di comando deve essere il nome del file PDF di partenza, per cui lo mettiamo in $ifile e lo togliamo da @ARGV (il tutto alla riga 5), che a questo punto conterrà solamente i vari comandi del tipo elencato in precedenza. La variabile $reps mantiene il numero di ripetizioni globalmente impostato, e viene fatto partire da 1 (riga 6).

La generazione di un file PDF prevede l'apertura e la chiusura del file stesso, che può essere mandato su STDOUT. L'apertura viene effettuata utilizzando la funzione prFile (riga 9), la relativa chiusura - avete indovinato? - si ha con prEnd (riga 48). In mezzo... scorre il fiume delle pagine!

La riga 13 suddivide i vari comandi impostati. Poiché abbiamo detto che possiamo separare i campi con spazi e virgole, filtriamo @ARGV utilizzando map, suddividendo ciascun argomento singolo in sotto-campi, considerando spazi e virgole come separatori. In uscita dalla map ci ritroveremo dunque ciascun singolo comando, pronto per l'analisi.

Per generalità, lavoreremo sempre come se stessimo trattando un intervallo di pagine, anche se la prima e l'ultima possono coincidere. In queste ipotesi, per default assumiamo che il comando sia un semplice numero di pagina, ed impostiamo $start e $end al valore di $cmd stesso (riga 18). La variabile $itreps contiene il numero di ripetizioni valido per questo comando (sempre riga 18), sarà più chiaro in seguito. Per default, tale numero di ripetizioni coincide con quello globale.

Successivamente, andiamo a verificare se, per caso, l'utente ha specificato in realtà altri tipi di comandi:

  • impostazione del fattore di ripetizione (controllo alla riga 21): in questo caso estraiamo il numero di ripetizioni, lo salviamo in $reps e passiamo direttamente al comando successivo (next alla riga 23);
  • pagina ripetuta un determinato numero di volte (controllo alla riga 25): di nuovo, sia $start che $end coincidono, ma viene impostato il valore di ripetizione per questa iterazione pari al prodotto fra il valore attuale e quanto richiesto nel comando (riga 28);
  • range di pagine (controllo alla riga 30): in questo caso, estraggo i valori e li imposto in $start e $end (righe 31 e 32).

A questo punto sono pronto per generare pagine, ma faccio l'operazione in due tempi per maggiore leggibilità.. L'intervallo $start .. $end viene espanso (riga 37) mediante la map, che applica eventuali ripetizioni impostate per il ciclo particolare (l'impostazione memorizzata in $itreps); il risultato di questa espansione viene impostato in @pages, che conterrà dunque la lista delle pagine da estrarre per il dato comando, opportunamente moltiplicate.

Successivamente si itera (riga 41) su tale intervallo, per poter estrarre la pagina dal file iniziale con la funzione prDoc e stampare un feedback su STDERR (righe 42 e 43). Abbiamo finito - basterà concludere la stampa del feedback, chiudere il file PDF di uscita e terminare.

Prendi e manda!

Mi è capitato di dover automatizzare dei processi di questo tipo:

  • prendi una determinata risorsa disponibile in una certa rete;
  • impacchettala in un file .zip;
  • spediscila via e-mail ad uno o più indirizzi.

Si, esatto: era qualcosa che mi è stato richiesto per due giorni di seguito, il che ha fatto scattare il principio della lazyness: lavorare subito (di programmazione) per non lavorare più (fa tutto lo script). Il piano d'azione è il seguente:

  • prendi: quale miglior candidato di LWP?
  • impacchetta: anche qui CPAN è generoso, mettendo a disposizione Archive::Zip;
  • spedisci: del gran numero di moduli a disposizione, mi è caduto l'occhio su Mail::Sender, che devo dire fa bene il suo lavoro.

Il tutto senza dover passare per file temporanei e simili! Vediamo come...

  1     #!/usr/bin/perl
  2     use strict;
  3     use warnings;
  4     use LWP::Simple;
  5     use Mail::Sender;
  6     use Archive::Zip;
    
  7     # Un po' di configurazione
  8     my $resource    = "http://www.example.com/resource.pdf";
  9     my $smtp_server = '10.20.30.40';
    
 10     # Prendi il documento
 11     print "Prendo $resource\n";
 12     my $content = LWP::Simple::get($resource)
 13       or die "niente da scaricare, riprovare piu` tardi";
    
 14     # Impacchetta
 15     print "Impacchetto\n";
 16     my $zip = Archive::Zip->new();
 17     $zip->addString($content, "risorsa.pdf");
 18     my $zipped;
 19     {
 20        open my $fh, ">", \$zipped;
 21        die "impossibile impacchettare!"

 22          unless $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK;
 23     }
    
 24     # Spedisci via email
 25     print "Spedisco\n";
 26     my $sender = Mail::Sender->new(
 27        {
 28           from    => '"Pinco Pallino- Automatic" <pinco@example.com>',
 29           to      => '"Tizio Caio" <tizio@example.com>',
 30           subject => "Risorsa trovata!",
 31           smtp    => $smtp_server,
 32        }
 33       )
 34       or die "niente da fare!";
    
 35     # Vogliamo avere un messaggio ed un allegato sul quale vogliamo
 36     # mantere pieno controllo, per cui utilizziamo OpenMultipart(),
 37     # Body() e Part() per costruire il messaggio, e Close() per
 38     # concluderlo.
 39     $sender->OpenMultipart()
 40       or die "impossibile chiamare OpenMultipart()";
 41     $sender->Body({msg => "Beh, che si dice?\n"});
 42     $sender->Part(
 43        {
 44           description => "Rassegna stampa del $date",
 45           ctype       => 'application/x-zip-encoded',
 46           encoding    => 'Base64',
 47           disposition =>

 48             qq{attachment; filename="$date.zip"; type="ZIP Archive"},
 49           msg => $zipped,
 50        }
 51     );
 52     $sender->Close() or die "impossibile chiamare Close()";
    
 53     # Finito!
 54     print "Fatto\n";

Il primo blocco (righe 1-6) dichiara le nostre intenzioni: fare le cose pulite (strict e warnings), ed utilizzare tanti bei moduli disponibili. Dopo una prima fase di configurazione (righe 7-9), in particolare riguardante la risorsa da prendere e il server SMTP da utilizzare per inviare l'e-mail, si entra nel vivo dello script.

La parte prendi si risolve in poche righe (dalla 10 alla 13): si utilizza LWP::Simple, che mette a disposizione un'interfaccia minimale ma efficace per scaricare una risorsa su web. Se il download non va a buon fine (nel qual caso la funzione LWP::Simple::get restituisce undef) si esce subito dallo script, con un messaggio di errore. Altrimenti... si prosegue.

La parte impacchetta si avvale dei servigi di Archive::Zip. Tale modulo è in grado di lavorare direttamente sui dati in memoria, senza bisogno dunque di generare file temporanei. In questo caso abbiamo a che fare con un'interfaccia orientata agli oggetti, per cui ci facciamo generare un oggetto (riga 16) e chiamiamo il metodo di aggiunta di un elemento in memoria all'archivio Zip in costruzione mediante il metodo addString (riga 17), impostando anche il nome che tale sequenza deve avere come "file" nell'archivio ("risorsa.pdf" nel nostro caso).

Il blocco di codice successivo (righe 18-23) realizza un piccolo inganno per Archive::Zip, che è abitutato a scrivere su file gli archivi compressi. Si apre dunque un file con open (riga 20), ma invece di un vero file nel filesystem si apre, in realtà,, una variabile scalare utilizzata come destinazione, ossia $zipped. Tale possibilità si ha solamente dalla versione 5.8 di Perl, quindi attenzione! Il resto della "scrittura su file" procede come di consueto. Anche qui, se qualcosa va storto si esce immediatamente con un messaggio di errore.

A questo punto la variabile $zipped contiene il file compresso, come se l'avessimo appena letto da un file vero e proprio. Siamo pronti alla spedizione, che si avvale - l'avreste mai detto? - di un'interfaccia ad oggetti. Questa volta il costruttore richiede un po' più di parametri (righe 26-34); niente di trascendentale come si può vedere, con la solita interruzione se qualcosa va male.

Per spedire un'e-mail con un messaggio ed un allegato potremmo utilizzare il metodo MailFile, che ci consente di fare tutto in un colpo solo; in questo caso, però, vogliamo avere un controllo più stretto sull'header relativo al file da spedire, per cui scegliamo la strada più lunga costruendo il messaggio pezzo per pezzo.

Utilizziamo allora OpenMultipart (riga 39), che imposta appunto il messaggio come composto da più parti. Il corpo del messaggio è impostato alla riga successiva con il metodo Body (ok, in questo caso ce lo potevamo evitare, visto il messaggio!), mentre l'allegato viene impostato con il metodo Part (righe 42-51), che può essere chiamato anche più volte per aggiungere ulteriori allegati. Anche in questo caso i parametri passati sono abbastanza ovvi.

La chiusura del messaggio con il metodo Close (riga 52), infine, ha anche l'effetto di far partire l'invio vero e proprio dell'e-mail. Il che termina anche la lista di quello che dovevamo fare!

Che fanno al cinema?

Premessa: l'esempio dato si avvale di informazioni liberamente disponibili su Internet. L'autore dell'esempio non ha trovato nessun tipo di indicazione avversa al trattamento automatico dei dati disponibili sul sito Virgilio.it, nemmeno un semplice robots.txt.

Se vi piace il cinema, sicuramente utilizzerete qualche servizio on-line per vedere dove, e quando, fanno il film che vi interessa tanto vedere. Personalmente utilizzo il servizio messo a disposizione da Virgilio.it (http://www.virgilio.it/), che consente di restringere la ricerca alla propria città, e di ordinare i risultati sia per sala che per titolo. Come al solito, però, a me non basta; prima di tutto perché mi piace lavorare a riga di comando (ad esempio per utilizzare l'onnipresente grep), secondo poi perché a volte ho voglia di restringere la ricerca a determinate fasce orarie. Cosa meglio di uno script Perl, allora?

I film per le sale di Roma sono reperibili all'indirizzo http://film.spettacolo.virgilio.it/cinema/insala.php/citta=10661. Diamo un'occhiata alla parte interessante di una pagina di esempio:

 <</span><span style="font-weight: bold; font-size: 10pt; font-family:
 'Courier New'">table</span><span style="font-size: 10pt; font-family:
 'Courier New'"> cellpadding=5 cellspacing=1 border=0 width=100%
 class=listaElenco>

 <tr>
 <td width=15%><a href="/cinema/insala.php?citta=10661">Roma</a></td>
 <td width=30%><a href=#
 onclick="PopUpWin('popsala.php?id=469','Sale','scrollbars=yes,width=300,height=350')">Alhambra</a>

 - Sala 2<br>
 Via Pier delle Vigne, 4 <br>
 <i>Tel.</i> 0666012154<br>
 <i>Orari:</i> 16:00 18:15 20:15 22:40</td>

 <td width=55%><a
 href="http://film.spettacolo.virgilio.it/cinema/scheda.php?film=30689">Bambole
 russe</a> (Francia/Gran Bretagna, 2005)<br>
 <i>con</i> R. Duris, C. De France, A. Tautou<br>

 <i>genere</i> commedia-sentimentale</td>
 </tr>

Quello che segue si avvale di tutta la potenza di LWP::Simple (per scaricare la pagina degli spettacoli), e di HTML::TableExtract per analizzare il file scaricato senza doversi destreggiare troppo nell'infida terra del parsing HTML.

  1     #!/usr/bin/perl
  2     use strict;
  3     use warnings;
  4     use HTML::TableExtract;
  5     use LWP::Simple;
    
  6     # L'URL di base per Virgilio Cinema
  7     my $baseurl = 'http://film.spettacolo.virgilio.it/cinema/insala.php';
    
  8     # La citta` di roma e` all'indice 10661
  9     my $url = "$baseurl?citta=10661";
 10     print STDERR "scarico da [$url]... ";
 11     my $html = get($url) or die "\ncould not get the page";
 12     print STDERR "fatto\n";
    
 13     # Effettua il parsing del file, isolando le tabelle con classe
 14     # "listaElenco" - ce ne dovrebbe essere una sola
 15     my $te = HTML::TableExtract->new(
 16        attribs   => {class => 'listaElenco'},
 17        keep_html => 1
 18     );
 19     $te->parse($html);
    
 20     # Estrai i dati dalle tabelle trovate
 21     my %data;
 22     foreach my $table ($te->tables) {
 23        print STDERR "Trovata tabella\n";
 24        foreach my $row ($table->rows) {
 25           my $title = film_title($row->[2]);
 26           my $place = film_place($row->[1]);
    
 27           # $data{$title}{$place} contiene/deve contenere un riferimento
 28           # ad un array contenente gli orari degli spettacoli
 29           push @{$data{$title}{$place}},
 30             split(/\s+/, film_schedule($row->[1]));
 31        } ## end foreach my $row ($table->rows)
 32     } ## end foreach my $table ($te->tables)
    
 33     # Stampe finali, ordinate per titolo, sala e orario
 34     foreach my $title (sort keys %data) {
 35        foreach my $place (sort keys %{$data{$title}}) {
 36           my @schedules = sort @{$data{$title}{$place}};
 37           print "[$title] [$place] [@schedules]\n";
 38        }
 39     } ## end foreach my $title (sort keys...
    
 40     # Estrazioni di titolo, sala ed orari
 41     sub film_title {
 42        return ($_[0] =~ /<(?:a|b).*?>(.*?)<\/(?:a|b)>/i ? $1 : '');
 43     }
 44     sub film_place    { return ($_[0] =~ /<a.*?>(.*?)<\/a>/i ? $1 : '') }
 45     sub film_schedule { return ($_[0] =~ /\s+([\d :]+)$/i    ? $1 : '') }

L'inizio è il solito (righe 1-5): utilizziamo strict e warnings perché siamo coscienziosi, ed includiamo anche i due moduli che ci permetteranno di scrivere lo script senza troppe ansie. Alla riga 7 viene impostata l'URL di base del servizio di Virgilio, che viene poi personalizzata alla riga 9, dove viene anche impostata la città da analizzare. Il download viene effettuato mediante il metodo get (riga 11), importato automaticamente da LWP::Simple; questo restituisce undef se qualche cosa va storto, nel qual caso lo script terminerà con un messaggio di errore.

A questo punto la pagina scaricata si trova nella variabile $html. Il modulo HTML::TableExtract mette a disposizione un'interfaccia ad oggetti, per cui prima di tutto dobbiamo procurarcene uno (riga 15), per poi seguire i passi necessari all'analisi della tabella che ci interessa. Si noti che, nel costruttore dell'oggetto, vengono impostate le condizioni di filtraggio: avendo notato che la tabella che ci interessa contiene l'attributo di classe 'listaElenco', indichiamo tale restrizione in modo da eliminare le altre tabelle (possibilmente) presenti nella pagina.

Dopo aver effettuato il parsing della pagina scaricata (riga 19), iteriamo sulle varie tabelle individuate (riga 22). Questa iterazione è in realtà non necessaria, visto che la tabella dovrebbe essere unica, ma è meglio andare sul sicuro. Nell'iterazione, la variabile $table itera sulla lista delle tabelle restituita dal metodo tables: nel nostro caso, come detto, tale lista conterrà solo un elemento.

La tabella $table viene analizzata riga per riga (linea 24); il metodo rows consente infatti di estrarre una lista di tutte le righe disponibili. Il titolo e la sala vengono identificati dai relativi campi nella riga della tabella (righe 25 e 26), utilizzando delle funzioni ad-hoc implementate in fondo allo script (righe 41-44). Queste due informazioni consentono di accedere ad una lista di orari contenuta nella struttura multilivello rappresentata dall'hash %data. La prima chiave è costituita dal titolo, la seconda dalla sala e l'elemento puntato è un riferimento anonimo ad un array di orari, che viene riempito con gli orari desunti dalla riga sotto analisi (righe 29-30).

Quando l'analisi della pagina è terminata, la struttura %data contiene tutto quello che ci serve per sbizzarrirci: inserire i dati in un database, mettere a disposizione un'interfaccia di ricerca... Nel nostro caso siamo molto più prosaici: facciamo una semplice stampa dei dati. Poiché la struttura è multilivello, annidiamo un ciclo per ciascuno di essi (tranquilli, sono solo due): al livello esterno troviamo i titoli (riga 34), che iteriamo in forma ordinata, mentre al livello interno troviamo la sala (riga 35), anche qui iterata in ordine alfabetico. Per comodità, estraiamo l'array degli orari (riga 36) e procediamo con la stampa (riga 37).

Si noti che la stampa di un array fra virgolette doppie fa sì che gli elementi siano separati fra loro dalla stringa contenuta nella variabile speciale $", che per default corisponde ad uno spazio. Una cosa in meno a cui pensare!

Vietato ai minori

C'è un modulo su CPAN che andrebbe vietato ai minori. Prima che vi prendiate a gomitate per raggiungere il browser più vicino e dare fondo alla vostra fantasia per cercare roba tipo Free::Nude su CPAN fatemi andare avanti, potreste perdere il vostro tempo!

Ricorderete che a scuola, specialmente alle elementari ed alle medie, la parola calcolatrice era stata bandita dal vocabolario di un qualsiasi insegnante di matematica. La ragione è semplice: se devi imparare a fare i conti, ossia se devi imparare come si fanno i conti e soprattutto perché si fanno in quel modo, avere la pappa pronta in una macchinetta che sa sempre la risposta giusta non aiuta molto. Quando hai capito, invece, puoi benissimo permetterti di dimenticare tutto ed usarla!

Con la scrittura credo debba essere uguale: dare delle scorciatoie prima che certi concetti siano ben radicati può essere molto, molto pericoloso. E qui veniamo al modulo proibito, quello che mette ordine nel testo al posto nostro: Text::Beautify. I minori sono gentilmente pregati di tornare alle grammatiche ed alle antologie, grazie.

Text::Beautify consente di eliminare quelle noiosissime violazioni delle regole di buona scrittura che rendono alcuni testi tanto noiosi da leggere. Se aprite un qualsiasi libro stampato (o quasi), potete infatti notare che vengono rispettate queste regole semplicissime:

  • i periodi iniziano con la lettera maiuscola (chi ha voglia di premere quel dannato tasto?);
  • prima di un carattere di interpunzione (sì, la punteggiatura) non va mai messo uno spazio...
  • ... ma dopo si (stiamo scherzando? Non ho tempo da perdere!!!);
  • se c'è un carattere di interpunzione, normalmente non ne serve un altro subito dopo (Totò docet: punto, anzi no: due punti!);
  • le parole sono separate da un solo spazio;
  • gli spazi all'inizio ed alla fine normalmente non servono.

Sono regole semplici, ma pur sempre regole - che vengono regolarmente infrante in moltissimi testi scritti "di fretta", come ad esempio nelle e-mail o nei commenti che vengono lasciati in giro su Internet.

Text::Beautify viene allora incontro alla pigrizia imperante: dategli un testo e tirerà fuori una versione più umana e gradevole da leggere. Vediamo un esempio:

     1  #!/usr/bin/perl
     2  use strict;
     3  use warnings;
     4  use Text::Beautify qw( beautify );
       
     5  my $text = "   qualcosa  che non  va bene ,direi ! ";
     6  print "Originale:   '$text'\n";
     7  $text = beautify($text);
     8  print "Trasformato: '$text'\n";

Lo script di esempio è di una banalità sconvolgente. Da notare che, in fase di utilizzo del modulo (riga 4), viene specificato il parametro beautify, che ha l'effetto di importare la funzione e renderla direttamente visibile alla riga 7. Se non avessimo specificato il parametro, visto che l'autore del modulo (José Alves de Castro, anche noto come cog) fa le cose per bene, non avremmo avuto modifiche sul nostro spazio dei nomi ed avremmo dovuto cambiare la riga 7 in:

     7bis   $text = Text::Beautify::beautify($text);

Niente di sconvolgente, ma in uno script così semplice possiamo permetterci di importare la funzione senza temere di incappare in collisioni.

L'uscita è semplicemente:

  Originale:   '   qualcosa  che non  va bene ,direi ! '

  Trasformato: 'Qualcosa che non va bene, direi!'

Comodo vero? Ma che non sia una scusa per scrivere male!

E se...

... volessi mettere a posto la punteggiatura, ma lasciare le lettere di inizio periodo così come sono? O tenere le punteggiature multiple?

Non sono richieste così campate in aria, mi rendo conto; fortunatamente se n'è reso conto anche cog, che ha messo a disposizione la possibilità di disabilitare le caratteristiche che non interessano. Una rapida occhiata al manuale (che, ricordo, è accessibile con il comando perldoc Text::Beautify dopo che si è installato il modulo) ci indica subito la strada: utilizzare la funzione disable_feature().

     1  #!/usr/bin/perl
     2  use strict;
     3  use warnings;
     4  use Text::Beautify qw( beautify disable_feature );
       
     5  my $brutto = " cog ,sapete,,  va scritto sempre   minuscolo ! ";
     6  print "Brutto: '$brutto'\n";
       
     7  my $errato = beautify($brutto);
     8  print "Errato: '$errato'\n";
       
     9  disable_feature('uppercase_first');
    10  my $bello = beautify($brutto);
    11  print "Bello : '$bello'\n";

La riga 4 non ci sorprende: poiché abbiamo bisogno di un'altra funzione da quel modulo, indichiamo che vogliamo importarla nel nostro spazio dei nomi.

Alla riga 7 utilizziamo beautify() esattamente come prima, ma il risultato che otteniamo è errato, perché la parola cog viene trasformata con l'iniziale maiuscola. È qui che si innesta la funzione disable_feature(): alla riga 9 si disabilita la trasformazione in maiuscola della prima lettera del periodo, con buona pace di cog. Il risultato finale è:

  Brutto: ' cog ,sapete,,  va scritto sempre   minuscolo ! '
  Errato: 'Cog, sapete, va scritto sempre minuscolo!'
  Bello : 'cog, sapete, va scritto sempre minuscolo!'

Attenzione agli oggetti

Il modulo mette a disposizione un'interfaccia orientata agli oggetti: mi è sembrata una cosa piuttosto golosa vista la possibilità di abilitare o disabilitare determinate caratteristiche di trasformazione del testo. Purtroppo, però, nella versione 0.08 (quella disponibile al momento) il supporto per l'interfaccia ad oggetti sembra limitarsi alla funzione beautify() e non si estende all'impostazione delle feature di interesse. Tutto sommato, quindi, consiglierei di evitare questa interfaccia per il momento, ed utilizzare quella puramente funzionale.

Upload, che passione!

Mi è capitato di recente di dover spedire dei file molto grandi ad un paio di colleghi; con molto grandi intendo maggiori di una ventina di megabyte, che è già abbastanza al di là della dimensione massima di un allegato che non mi infastidisce ricevere. Visto che ho un server a disposizione, mi è sembrato naturale mettere su un piccolo "servizio" personale per l'upload dei file. Ecco cosa è venuto fuori:

     1  #!/usr/bin/perl -T
     2  use strict;
     3  use warnings;
     4  use CGI;
     5  use File::Basename qw( basename );
     6  use Readonly;
       
     7  # Configuration
     8  Readonly my $fieldname      => 'uploaded_file';
     9  Readonly my $base_directory => "/percorso/per/upload";
    10  Readonly my $base_url       => "http://mio.server.it/upload";
       
    11  my $q = CGI->new();
       
    12  my $msg;
    13  $msg = gestisci_upload_entrante() if $q->param('uploaded_file');
       
    14  print $q->header(),
    15     $q->start_html('Upload semplice semplice');
    16  print $q->h1($msg), $q->hr() if $msg;
    17  print $q->h1("Upload"),
    18     $q->start_multipart_form(),
    19     'File: ', $q->filefield(-name => $fieldname), $q->br(),
    20     $q->submit(-name => 'Invia'),
    21     $q->end_form(),
    22     $q->end_html();
       
    23  sub gestisci_upload_entrante {
    24     chdir $base_directory or return "chdir(): $!";
       
    25     my $fn = basename($q->param($fieldname))
    26        or return "nessun nome di file";
    27     $fn =~ s/[^\w\d .-]//g;
    28     ($fn) = $fn =~ /([\w\d .-]+)/;
    29     return "Errore: nessun nome di file valido" unless $fn;
    30     my @alphabeth = ('a' .. 'z');
    31     while (-e $fn) {
    32        $fn = $alphabeth[rand @alphabeth] . $fn;
    33     }
       
    34     my $fh = $q->upload($fieldname)
    35        or return "Problemi nell'upload di '$fn'";
    36     binmode $fh;
    37     open my $out, '>', $fn or return "open(): $!";
    38     binmode $out;
    39     while (read $fh, my $data, 8192) { print {$out} $data }
       
    40     return "Upload ok: $base_url/$fn";
    41  }

L'inizio (righe 1..6) è più o meno il solito: usiamo strict, warnings ed i moduli CGI, visto che stiamo realizzando uno script CGI, e File::Basename, che ci sarà utile per estrarre il nome del file da un path completo.

Avete fatto caso all'opzione -T sulla prima riga? Serve ad attivare il cosiddetto taint mode, ossia una modalità un po' paranoica che considera tutto quello che viene dall'esterno dello script come potenzialmente pericoloso, e quindi impedisce che questi dati siano utilizzati in determinate funzioni (come, per fare un esempio, open() nella riga 37). Questa modalità non vi impedisce di fare stupidaggini, ovviamente, ma vi costringe a prestare una particolare attenzione ai dati che provengono dal browser, impedendovi di abbassare la guardia.

Le righe 7..10 riportano alcune configurazioni: in particolare, il campo nel form CGI che servirà per l'upload verrà chiamato uploaded_file, i file verranno messi tutti dentro la directory /percorso/per/upload e questi saranno poi visibili attraverso la URL base http://mio.server.it/upload.

Per evitare di spargere queste stringhe in giro per lo script, correndo il rischio di sbagliare qualcosa, ho deciso di utilizzare delle variabili. L'approccio migliore, in questi casi, consiste nell'utilizzare un sistema che forzi l'utilizzo di queste variabili come costanti; usiamo qui il modulo Readonly, che fa sì che le tre variabili $fieldname, $base_directory e $base_url non possano essere (involontariamente o meno) modificate.

La riga 11 dichiara e definisce $q, la variabile che verrà utilizzata in tutto lo script per la gestione dell'interfaccia CGI. La riga 13 chiama la funzione gestisci_upload_entrante() nel caso sia definito il parametro CGI in ingresso uploaded_file; in questo modo, saremo in grado di utilizzare questo script sia per presentare una pagina per l'upload che per gestire l'upload stesso.

La funzione gestisci_upload_entrante(), che vedremo fra poco, restituisce un messaggio che può essere utilizzato come riscontro per l'utente; questo messaggio viene raccolto nella variabile $msg.

Le righe 14..22 si occupano di generare la risposta da inviare al browser, utilizzando le funzioni standard del modulo CGI. Se la variabile $msg è non vuota, il messaggio contenuto viene incluso all'interno della pagina (riga 16). Le righe 18..21 includono nella pagina il form per l'upload: poiché questo si deve occupare di inviare dei file, occorre prestare attenzione ad utilizzare il metodo start_multipart_form() invece del semplice start_form(), altrimenti non verrà impostato il Content-Type corretto e non riceveremo nulla! Il resto del form è banale: un campo filefield per il file, ed un tasto per l'invio.

Le righe 23..41 contengono l'implementazione della funzione di gestione dell'upload, ossia gestisci_upload_entrante(). Per prima cosa ci si sposta nella directory obiettivo $base_directory, con il consueto controllo dell'errore (riga 24).

Le righe 25..33 si occupano di determinare il nome del file da utilizzare per salvare l'upload entrante, con alcuni controlli paranoici. In particolare, attraverso la funzione basename() ci si assicura che non si cerchi di mettere il file in una directory differente da quella in cui ci siamo spostati alla riga 24; la sostituzione alla riga 27 elimina tutto quello che non mi piace in un nome di file (di base vengono mantenute solamente lettere, cifre e poco più)

La riga 28 merita un commento. Abbiamo detto che stiamo operando in modalità taint, a causa dell'opzione -T impostata nella prima riga, e che questa modalità fa sì che qualsiasi dato "esterno" sia considerato non sicuro da utilizzare con certe funzioni, come ad esempio open() alla riga 36. Questo crea un problema: come facciamo a liberare $fn da questo marchio infamante? Semplice: dobbiamo estrarre del testo utilizzando un'espressione regolare, perché questo è l'unico modo a disposizione per prendere dati da una variabile marcata senza che il risultato sia anch'esso marcato. Si effettua allora un semplice match di tutti gli elementi consentiti (che sono anche gli unici rimasti nella stringa a questo punto, ma la prudenza non è mai troppa) all'interno di una parentesi di cattura, ed il risultato viene re-immesso dentro $fn. Le parentesi intorno al nome della variabile prima dell'assegnazione servono a forzare un contesto lista sul match, in modo che questo restituisca la lista ($1, $2, ...), che è proprio quel che ci serve.

Se non avanza proprio niente, alla riga 29 si esce. Le righe 30..33 servono a trovare un nome di file che sia differente da quello dei file già presenti, in modo da evitare sovrascritture. Questo sistema, ovviamente, non mette al riparo da upload contemporanei, ma sinceramente non era un mio requisito quando ho fatto lo script!

La riga 34 raccoglie un filehandle da cui è possibile leggere il file inviato con l'upload; ci assicuriamo di effettuare una lettura binaria senza conversioni implicite (riga 36), apriamo il file di uscita (riga 37), lo impostiamo in modalità binaria (riga 38) e poi siamo pronti a fare la copia. Potevamo utilizzare altri sistemi, lo so, ma questo risolve!