[couverture de Linux Magazine 66]

Perles de Mongueurs (7)

Article publié dans Linux Magazine 66, novembre 2004.

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

Découper un fichier en blocs de n lignes

Récemment, un collègue a eu besoin de découper un gros fichier en blocs de 65534 lignes (car Excel tronque les fichiers texte CSV qu'il importe à 65535, c'est embêtant).

Nous avons donc écrit le petit script suivant :

    #!perl -wn
    BEGIN { $file = "partie00"; }
    if( $. % 65534 == 1) {    # NOTE: $. commence à 1
        close F;    # ferme le fichier précédent
        open F, "> $file.csv"
          or die "Impossible de créer $file.csv: $!";
        $file++;    # auto-incrément magique
    }
    print F;

Notez l'utilisation de l'option -n pour ajouter une boucle implicite sur les données en entrée, ainsi que l'utilisation de l'opérateur ++ sur une chaîne de caractères, qui incrémente magiquement (et avec propagation des retenues !) la chaîne concernée.

L'avantage de ce script par rapport à split -l 65534 partie, c'est que l'on peut avoir un peu plus de contrôle sur le nom des fichiers (partie00.txt, partie01.txt, etc. au lieu de partieaa, partieab) et que mon collègue n'avait pas les outils GNU installés sur son Windows 2000.

Puisque nous parlons de split, voici comment modifier le script pour gérer des parties à la taille mesurée en octets plutôt qu'en lignes.

La variable $/ (séparateur d'enregistrements en entrée) a une autre spécificité (en plus de celles présentées dans l'article de GNU/Linux Magazine 52) : si sa valeur est une référence à une constante numérique n ou à une variable scalaire contenant le nombre n, l'opérateur diamant renvoie un bloc de n octets. Voici le script précédent modifié pour découper un gros fichier en morceaux tenant sur une disquette :

    #!perl -wn
    BEGIN {
        $file = "partie00";
        $/ = \1024;         # lecture par blocs de 1 Ko
        $n = 0;
    }
    unless( $n++ % 1440 ) { # une disquette contient 1440 Ko
        close F;
        open F, "> $file.csv"
          or die "Impossible de créer $file.csv: $!";
        $file++;
    }
    print F;

Merci à Didier Arenzana d'avoir d'aussi gros fichiers à traiter sur des systèmes d'exploitation pas faits pour ça. ;-)

(Philippe "BooK" Bruhat, Paris.pm & Lyon.pm - book@mongueurs.net)

Sélectionner une tranche d'un fichier texte

Découper un fichier texte en morceaux, c'est bien, mais il y a des fois où on voudrait pouvoir simplement ne retenir qu'une partie du fichier, ne conserver qu'un bloc contenu entre certaines lignes. Il peut y avoir moyen de bricoler avec des outils comme tail(1) et head(1), mais pourquoi perdre du temps à s'escaguasser avec ça quand il est si facile de le faire en Perl.

Une première méthode simple et rapide est d'utiliser l'uniligne suivant :

    $ perl -ne '18..21 and print' long_texte.txt

Dans ce cas-ci, il n'affichera que les lignes 18 à 21 du fichier long_texte.txt. Toutefois il serait plus pratique d'en faire un script auquel on pourrait passer les lignes à afficher en paramètres. Écrivons donc ce script, que nous nommons splice pour faire référence à la fonction du même nom en Perl, mais qui travaille elle sur les tableaux.

    #!/usr/bin/perl
    my($first,$last) = (shift,shift);
    $.==$first .. $.==$last and print while <>

Quelques explications ne sont pas superflues. Ce programme utilise l'opérateur flip-flop (..) avec comme tests de bascule des comparaisons sur le numéro de la ligne courante ($.). À noter que .. n'agit comme un flip-flop qu'en contexte scalaire, puisqu'en contexte de liste il est l'opérateur de génération de liste. Dans l'uniligne montré au début on n'écrivait pas les tests $. == 18 car lorsque l'opérateur flip-flop reçoit des valeurs numériques en arguments, il effectue tout seul la comparaison avec $.. Comme cela ne marche plus quand on remplace les valeurs par des variables, il faut écrire le test explicitement.

Si on invoque ce script ainsi :

    $ splice 185 202 long_texte.txt

il affichera les lignes 15 à 20 (incluses) du fichier long_texte.txt. On peut même l'utiliser dans un tube :

    $ man perl | splice 319 322
    NOTES
        The Perl motto is "There's more than one way to do it."
        Divining how many more is left as an exercise to the
        reader.

C'est pas mal, mais on peut faire mieux. Bien mieux. Si on change la manière d'indiquer les lignes à afficher, et qu'on adopte une syntaxe similaire à celle de cut(1), on peut alors indiquer plusieurs blocs de lignes.

    #!/usr/bin/perl
    sub usage { print STDERR "usage: splice LINES [file ...]\n" and exit -1 }
    my $lines = shift || usage();
    my(@first,@last,$i) = ();
    for my $block (split ',', $lines) {
        my @l = split '-', $block;
        push @first, $l[0];
        push @last, $l[1] || $first[-1];
    }
    ($.==$first[$i]||($.==$first[$i+1]&&++$i)) .. $.==$last[$i] and print while <>

L'exemple précédent s'écrit maintenant :

    $ man perl | splice 319-322
    NOTES
        The Perl motto is "There's more than one way to do it."
        Divining how many more is left as an exercise to the
        reader.

Plus intéressant, on peut maintenant indiquer plusieurs blocs de lignes à afficher. Pour illustrer cela, on crée d'abord un fichier qui ne contient que ses numéros de lignes :

    $ pseq 1 20 "line %d" >text

ou, pour ceux qui n'auraient pas conservé la Perle correspondante :

    $ perl -le 'print"line $_"for 1..20' >text

Exécutons maintenant splice en sélectionnant les lignes 8 à 9, 12 et 15 à 17.

    $ splice 8-9,12,15-17 text
    line 8
    line 9
    line 12
    line 15
    line 16
    line 17

Comme on le voit, seules les lignes indiquées sont affichées. Quant à ceux qui voudraient maintenant sélectionner des tranches non plus en fonction des numéros de lignes, mais en fonction du texte (en quelque sorte un mélange des fonctionnalités de splice et de grep(1)), il y a moyen de faire quelque chose, mais c'est plus délicat de trouver une manière générique de l'exprimer.

Par exemple l'uniligne suivant affiche le premier paragraphe de la section Author de perl(1) :

    $ man perl | col -b | perl -ne '/AUTHOR/../^$/ and print'
    AUTHOR
        Larry Wall <larry@wall.org>, with the help of oodles of
        other folks.

Comme précédemment, tout repose sur l'opérateur flip-flop, mais en lui passant cette fois en arguments des expressions régulières. Les plus observateurs auront remarqué la présence de col(1) dans le tube afin de supprimer le formatage écran de man(1).

En suivant la même route que pour splice, il est simple de transformer cet uniligne en petit script mgrep (comme multi-grep :

    #!/usr/bin/perl
    my($first,$last) = (shift,shift);
    /$first/../$last/ and print while <>

L'exemple précédent s'écrit alors :

    $ man perl | col -b | sgrep 'AUTHOR' '^$'
    AUTHOR
        Larry Wall <larry@wall.org>, with the help of oodles of
        other folks.

L'étape suivante, accepter plusieurs expressions régulières, est celle qu'il est plus difficile de rendre aussi élégante que pour splice. En effet, dans l'idéal nous voudrions pouvoir accepter n'importe quelle expression régulière, mais certains caractères sont nécessaires pour la syntaxe de délimitation de ces expressions à passer en argument à mgrep (en reprenant celle de splice, on utilise le tiret pour délimiter les expressions d'un couple et la virgule pour délimiter les couples). Ces caractères ne pourront donc pas être utilisés au sein des expressions régulières, à moins de vouloir coder un mécanisme d'échappement. Nous nous en tenons à la syntaxe de splice, en connaissant et acceptant ses limitations.

    #!/usr/bin/perl
    use strict;
    sub usage { print STDERR "usage: mgrep PATTERNS [file ...]\n" and exit -1 }
    my $patterns = shift || usage();
    my(@first,@last,$i) = ();
    for my $block (split ',', $patterns) {
        my @l = split '-', $block;
        push @first, $l[0];
        push @last, $l[1] || $first[-1];
    }
    (/$first[$i]/||(/$first[$i+1]/&&++$i)) .. /$last[$i]/ and print while <>

Un exemple d'exécution de mgrep ressemblera à ceci :

    $ man perl | col -b | mgrep AUTHOR-'^$',motto,virtues-why
    AUTHOR
        Larry Wall <larry@wall.org>, with the help of oodles of
        other folks.

        The Perl motto is "There's more than one way to do it."
        The three principal virtues of a programmer are Laziness,
        Impatience, and Hubris.  See the Camel Book for why.

Les arguments signifient : afficher la ligne qui contient « AUTHOR » et le paragraphe qui suit (paramètre AUTHOR-'^$'), afficher la ligne qui contient « motto » (paramètre motto), afficher le texte de la ligne qui contient « virtues » à la ligne qui contient « why » (paramètre virtues-why).

(Sébastien Aperghis-Tramoni (Maddingue), Marseille.pm - sebastien@aperghis.net)

À vous !

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

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