Perles de Mongueurs (34)

Article publié dans Linux Magazine 96, juillet/août 2007.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

La perle de ce mois-ci a été rédigée par Laurent Gautrot, (l.gautrot@free.fr), de Paris.pm.

Suppression des doublons d'un agenda au format ICalendar

La RFC 2445 [1] rappelle qu'il y a un vrai besoin de standard pour l'interopérabilité des services d'agenda ou de calendriers.

Elle définit en outre le format iCalendar pour permettre d'échanger ou de stocker des informations variées comme les calendriers, des listes de choses à faire ou des évènements.

De fait, le format iCalendar est utilisé par de nombreuses applications de gestion d'informations personnelles, qui savent lire et produire des fichiers .ics.

Le format reste très libre, et l'interprétation de la sémantique reste à définir pour l'unicité des évènements, en particulier.

Justement, notre grand chef, après une migration de son système sur une nouvelle version a essayé de synchroniser son Palm Treo680 avec son calendrier Evolution, mais après plusieurs essais infructueux, le système lui présente désormais ses rendez-vous en double, ou en triple. C'est mal.

Heureusement, Perl et CPAN sont là. Une brève recherche (Merci Jérôme) sur CPAN, grâce à l'excellent CPAN Suggest [3] fournit rapidement une réponse et nous aiguille entre autres sur quelques modules excellents et bien documentés de Jesse Vincent (papa entre autres de Request Tracker).

Data::ICal

Pour explorer le contenu du calendrier, une ligne comme la suivante suffit (bon, deux, pour préciser le module qui va être utilisé).

    use Data::ICal;
    my $calendar = Data::ICal->new($filename);

iCalendar définit de nombreux composants, rebaptisés Entries qui généralement n'ont pas de sous-composants, mais certains peuvent en avoir. Chaque composant peut alors avoir des propriétés, dont certaines sont uniques et d'autres peuvent apparaître plusieurs fois.

Pour filtrer les méchants doublons, il faut commencer par identifier ce qui fait de deux enregistrements similaires des doublons.

Evolution va assez loin dans le respect des standards en utilisant iCalendar comme format de stockage des données. Un export n'est alors même pas nécessaire.

Pour commencer, je ne m'intéresse qu'aux enregistrements de type VEVENT, qui sont les seuls qui apparaissent dans mon fichier de données. Il existe d'autres types de composants (VTODO, VTIMEZONE, etc.) et chacun a une structure particulière.

Je suis parti du principe que deux VEVENT seraient identiques si leurs dates de début, de fin et leur titre étaient identiques. En fait, c'est erroné parce que certains VEVENT n'ont pas de titre.

Après investigation, il s'avère que ces évènements sont des scories inutiles qui peuvent être supprimées sans état d'âme.

Le résultat

Le script suivant décrit l'intégralité du traitement. Après avoir ouvert un calendrier en entrée et un autre en sortie, je passe en revue tous les composants de mon fichier d'entrée. Le script est normalement invoqué avec deux fichiers en arguments, le fichier d'entrée et le fichier de sortie. À défaut, on utilisera un fichier nommé calendar.ics dans le répertoire courant en entrée et la sortie sera écrite dans le fichier output_calendar.ics.

Dans le cas des VEVENT, il faut vérifier la présence de la propriété summary, et prévenir de son absence.

Une table de hachage permet de conserver un index constitué des dates de début, de fin et du titre. Pour les évènements qui n'apparaissent pas dans cette liste, il suffit de passer son chemin. Dans le cas contraire, une sauvegarde s'impose, réalisée à l'aide de la méthode add_entry().

Pour tous les autres composant, une conservation à l'identique est réalisée.

En fin de traitement, l'ensemble du calendrier est sauvegardé et un petit résumé est affiché, pour rappeler le nombre de composants de chaque type.

  #!/usr/bin/perl

  use strict;
  use warnings;
  use Data::ICal;

  my $input_calendar = Data::ICal->new( filename => shift || 'calendar.ics' );
  my $output_calendar = Data::ICal->new();
  my %component = ();
  my %event     = ();

  open my $output, '>', shift || 'output_calendar.ics'
    or warn "Could not open output file: $!\n";

  ENTRY:
  foreach my $entry ( @{ $input_calendar->entries } ) {
      $component{ $entry->ical_entry_type }++;

      if ( $entry->ical_entry_type =~ /VEVENT/ ) {
          my $check = '';
          if ( $entry->property('summary') ) {

              foreach my $property (qw/ summary dtstart dtend /) {
                  foreach ( @{ $entry->property($property) } ) {
                      $check .= $_->value;
                  }
              }

              $component{' Duplicate VEVENT'}++, next ENTRY if exists $event{$check};
              $event{$check} = undef;
          }
          else {
              $component{' Without summary'}++;
              next ENTRY;
          }
      }

      $component{' Backed up entry'}++;
      $output_calendar->add_entry($entry);

  }

  END {

      print "Saving output calendar... ";
      print $output $output_calendar->as_string;
      close $output or die "Could not close output file: $!\n";
      print "done.\n";

      print "Processed entries:\n";
      foreach my $key ( reverse sort keys %component ) {
          printf "%10s %i\n", $key, $component{$key};
      }
  }

Au départ, j'avais tenté d'afficher un résumé des événements rencontrés. Le problème, c'est que j'avais une erreur un peu méchante quand j'ai essayé de pointer sur une propriété inexistante (précisément la propriété summary).

Avec le bloc END, j'avais au moins mon bilan, ce qui m'a permis de savoir que j'avais parcouru 148 VEVENT et que le 149ème était un VEVENT sans summary.

Bien entendu, il pourrait y avoir de nombreuses améliorations dans la récupération des fichiers de calendrier en entrée et en sortie, mais en l'état, ça fonctionne plutôt bien, et pour un calendrier comportant près de 8500 évènements, le fichier d'origine pesant un peu moins de 4 Mo, la suppression des doublons est opérée en 24 secondes sur mon ordi portable.

 % time perl remove_duplicates_from_ical
 Saving output calendar... done.
 Processed entries:
                VTIMEZONE 5
                   VEVENT 8482
          Without summary 1518
         Duplicate VEVENT 3808
                Backed up 3161
 perl remove_duplicates_from_ical  23,54s user 0,40s system 99% cpu 24,004 total

Faciliter le déploiement d'un script à l'aide de PAR et PAR::Packer

Comme notre grand chef ne goûte certainement pas aux joies du shell CPAN, il faut trouver un moyen d'empaqueter dans un programme prêt à l'emploi, sans se soucier des dépendances de modules.

Ça tombe plutôt bien, PAR est fait pour cela. Ce module, permet de trouver et d'empaqueter dans un binaire, ou dans un énorme script Perl toutes les dépendances identifiées d'un script.

Initialement écrit par Audrey Tang, et actuellement maintenu par Steffen Mueller, PAR dispose d'une commande pour générer directement des exécutables ou des binaires pour quelques plate-formes. Il semble y avoir quelques restrictions pour construire des binaires avec des interfaces graphiques sur quelques plate-formes, consultez la perldoc pour plus d'informations.

Un module a particulièrement retenu mon attention. PAR::Packer contient la commmande pp, dont l'aide en ligne est accessible par perldoc également.

Dans le cas de ce script, j'ai choisi de générer un binaire, qui bien qu'il soit compressé (compression généraliste gzip, niveau de compression 9), pèse tout de même ses 3 Mo.

 % pp -C -x -z 9 -o remdupical  remove_duplicates_from_ical
 Saving output calendar... done.
 Processed entries:
                VTIMEZONE 5
                   VEVENT 8482
          Without summary 1518
         Duplicate VEVENT 3808
                Backed up 3161
 % ls -l remdupical remove_duplicates_from_ical
 -rwxr-xr-x 1 lg lg 3111695 mai 21 11:37 remdupical
 -rw-r--r-- 1 lg lg    1495 mai 21 11:36 remove_duplicates_from_ical

La taille du binaire généré impressionne un peu (enfin, moi, surtout), et j'ai eu la mauvaise idée de vouloir lui faire subir une cure d'amaigrissement. Un passage de strip sur le binaire permet d'enlever les symboles de debogage.

 % strip rempdupical
 % ll remdupical remove_duplicates_from_ical
 -rwxr-xr-x 1 lg lg 1316988 mai 21 11:55 remdupical
 -rw-r--r-- 1 lg lg    1495 mai 21 11:36 remove_duplicates_from_ical

Sauf que le résultat est le suivant :

 % ./remdupical
 Usage: ./remdupical [ -Alib.par ] [ -Idir ] [ -Mmodule ] [ src.par ] [
 program.pl ]
        ./remdupical [ -B|-b ] [-Ooutfile] src.par

Le script Perl généré est un peu moins imposant que le binaire.

Conclusion

Avec un petit script en Perl, et des outils respectueux des standards, j'ai pu facilement récupérer un calendrier utilisable.

Reste à trouver les raisons des problèmes de synchronisation d'agenda. Mais ceci est une autre histoire.

Références

[1] RFC 2445 - ftp://ftp.rfc-editor.org/in-notes/rfc2445.txt

[2] CPAN Suggest - http://cpantools.com/

[3] Data::ICal sur CPAN - http://search.cpan.org/~jesse/Data-ICal/

[4] PAR sur CPAN - http://search.cpan.org/~smueller/PAR/lib/PAR.pm

[5] PAR::Packer - http://search.cpan.org/~smueller/PAR-Packer/lib/PAR/Packer.pm

À vous !

Envoyez vos perles à perles@mongueurs.net, elles seront peut-être publiées dans un prochain numéro de Linux Magazine.

Auteur: Laurent Gautrot

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]