Re: [#!/fr] Tri de fichiers lourds

Page principale
Supprimer ce message
Répondre à ce message
Auteur: Patrick Proniewski
Date:  
À: La liste francophone des scripts shell
Sujet: Re: [#!/fr] Tri de fichiers lourds
On 16 mars 2010, at 19:13, Yvan KOENIG wrote:

> Puis-je écrire que, comme la solution de patpro, pour le béotien que je suis, c'est du cunéiforme ( je ne tiens pas à heurter un utilisateur d'un langage encore vivant )?


si on repart du script AWK dont je numérote les lignes :

   1: #!/usr/bin/awk -F; -f
   2: BEGIN { while ((getline < ficb) > 0)
   3:                fic2[$1]=$2}
   4: {if (fic2[$1])
   5:        printf "%s%s%s\n",$1,FS,fic2[$1]
   6: else
   7:        print $0}


on peut expliquer les choses comme cela :

À la ligne 1, on déclare l'interpréteur, et ses options : on veut que le contenu du script soit interprété par awk, avec les options -F; (séparateur = ";"), et -f indique que les lignes suivantes sont les instructions du script.

Ligne 2, un BEGIN est déclaré. Cela permet de poser certaines variables, ou de faire des initialisations qui ne doivent être faites qu'une seule fois. Dans le cadre de ce BEGIN, on lit le fichier "ficb" ligne par ligne. C'est le code "(getline < ficb)".
Tant qu'il y a des lignes (c'est le while), on exécute "fic2[$1]=$2" qui a pour fonction de créer un tableau qui s'appelle fic2 et qui associe l'intitulé contenu dans le champ 1 d'une ligne donnée ("[$1]") avec le contenu du champ 2 de la même ligne ("$2").
Donc si une ligne du fichier est "toto";"titi", alors la valeur de fic2["toto"] sera "titi". C'est la ligne 3, l'accolade finale ferme le bloc BEGIN.

En ligne 4, un if est ouvert, il teste "fic2[$1]". Attention, $1 ici est le champ 1 d'une ligne du fichier "a" !
Le script est exécuté en injectant les lignes du fichier "a" dans le programme awk. Dans le bloc BEGIN, on lit le fichier "b", mais les autres blocs du programme sont destinés à traiter les données injectées à partir de l'extérieur du script, ici la source est le fichier "a".

Si "fic2[$1]" existe, donc si le tableau fic2 (créé à partir du fichier "b") un enregistrement existe, aillant pour index le premier champs de la ligne du fichier "a", alors (ligne 5) le script imprime en sortie la concaténation de $1, du séparateur ";" et du contenu de fic2[$1].
Si il n'existe pas, alors (ligne 7) le script imprime en sortie la ligne du fichier "a", telle quelle.

Une fois la ligne N du fichier "a" traitée, c'est au tour de la ligne N+1 de passer par les mêmes étapes (lignes 4 à 7).

Je ne sais pas si j'ai clarifié les choses... :)

Pour mon script à base de SQLite les explications sont peut être encore moins claires :) :

1: #!/bin/bash
2: # quelques definitions
3: MY_FILEDB=":memory:"
4: MY_SQLITE="/usr/bin/sqlite3"
5:
6: ${MY_SQLITE} "${MY_FILEDB}" <<EOF | sed 's,\([^|]*\)\|\([^|]*\),"\1";"\2",' > $3
7: CREATE TABLE file1(ref varchar(120),desc varchar(120));
8: CREATE TABLE file2(ref varchar(120),desc varchar(120));
9: $(sed 's/\([^;]*\);\([^;]*\)/INSERT INTO file1 VALUES(\1,\2);/' $1)
10: $(sed 's/\([^;]*\);\([^;]*\)/INSERT INTO file2 VALUES(\1,\2);/' $2)
11: SELECT file1.ref, IFNULL(file2.desc, file1.desc) FROM file1 LEFT JOIN file2 ON file1.ref=file2.ref;
12: EOF

Lignes 1 à 4, je défini l'interpréteur (bash), et deux variables. Les variables c'est un peu inutile ici. C'est bien pour les long scripts, ça permet de changer un élément du script à un seul endroit sans en chercher toutes les occurrences partout. Mais le script est trop court pour que ça soit utile ici.

La ligne 6 est la plus complexe. Elle s'articule en 3 parties, et c'est à la fois la première vraie ligne de code, et la dernière !
La première partie de la ligne 6 est la suivante : 
    ${MY_SQLITE} "${MY_FILEDB}" <<EOF
et correspond au code : 
    /usr/bin/sqlite3 ":memory:" <<EOF


Elle signifie : ouvre une base de donnée de type ":memory:" dans SQLite, et injecte lui toutes les lignes que tu liras ensuite, jusqu'à ce que tu trouves la chaîne "EOF".
Cela me permet d'injecter dans SQLite plusieurs commandes à la suite des autres sans "lâcher" la base de données. Comme je travaille en ":memory:", il ne faut pas que j'interrompe ma transaction avec la base de données, sinon elle disparait définitivement (c'est le propre des bases de données temporaires stockées en RAM).
Voyons les lignes 7 à 12 avant de remonter à la ligne 6.

Les lignes 7 et 8 créent simplement chacune une table (file1 et file2), avec les champs "ref" et "desc", en texte, sur 120 caractères de long.
Les lignes 9 et 10 disent à bash d'exécuter respectivement :
    sed 's/\([^;]*\);\([^;]*\)/INSERT INTO file1 VALUES(\1,\2);/' $1
et    sed 's/\([^;]*\);\([^;]*\)/INSERT INTO file1 VALUES(\1,\2);/' $2
$1 et $2 sont les deux premiers arguments passés au script (script.sh arg1 arg2 arg3). Ils sont sensés être les chemins du fichier A et du fichier B.
ces lignes disent à sed de remplacer pour chaque ligne des fichiers A et B les occurences de référence;description par le code SQL "INSERT INTO file1 VALUES(référence,description);"
Comme les commandes sed sont encapsulées dans $( ) et bien le résultat de chaque commande sed remplace le contenu des lignes 9 et 10. Donc SQLite reçoit directement les INSERT, de la même manière qu'il a reçu les CREATE TABLE.


Enfin, la ligne 11 fait la recherche SQL qui répond au problème initial, à savoir faire la synthèse des fichiers A et B. La commande joint les deux fichiers en s'appuyant sur le critère d'identité des références, et elle affiche la référence du fichier A, la description correspondante dans le fichier B si elle existe, et la description correspondante du fichier A si celle du fichier B n'existe pas.

La ligne 12 indique à bash qu'il n'y a plus de ligne à injecter dans le "<<" (HERE DOCUMENT). On revient donc à la ligne 3.

La seconde partie de la ligne 3 est : 
    | sed 's,\([^|]*\)\|\([^|]*\),"\1";"\2",'
Le "pipe" reçoit la sortie de la commande SQLite (le résultat du SELECT de la ligne 11) , et l'injecte dans le sed. 
La commande sed prend le résultat :
    référence|description 
et le transforme en :
    "référence";"description"


J'aurai pu remplacer le sed par un SELECT mieux ficelé, mais j'ai eu la flemme.

La fin de la ligne 3 "> $3" injecte la sortie du sed dans le fichier dont le chemin est précisé par le 3ème argument au lancement du script.


patpro






_______________________________________________
archives :
http://listes.patpro.net/list/sshfr.fr.html
http://listes.patpro.net/mailman/listinfo/script_shell_fr