Infusez vos scripts awk avec Groovy


  • Français


  • Récemment, j’ai écrit une série sur l’utilisation de scripts Groovy pour nettoyer les balises dans mes fichiers musicaux. J’ai développé un cadre qui reconnaissait la structure de mon répertoire musical et l’utilisait pour parcourir les fichiers de contenu. Dans le dernier article de cette série, j’ai séparé ce framework en une classe utilitaire que mes scripts pourraient utiliser pour traiter les fichiers de contenu.

    Ce framework séparé m’a beaucoup rappelé le fonctionnement d’awk. Pour ceux d’entre vous qui ne connaissent pas awk, vous pourriez bénéficier du livre électronique d’Opensource.com, Un guide pratique pour apprendre awk.

    J’ai beaucoup utilisé awk depuis 1984, lorsque notre petite entreprise a acheté son premier “vrai” ordinateur, qui fonctionnait sous System V Unix. Pour moi, awk a été une révélation : il avait une mémoire associative – pensez à des tableaux indexés par des chaînes au lieu de nombres. Il avait des expressions régulières intégrées, semblait conçu pour traiter les données, en particulier dans les colonnes, et était compact et facile à apprendre. Enfin, il a été conçu pour fonctionner dans les pipelines Unix, en lisant ses données à partir d’entrées ou de fichiers standard et en écrivant dans la sortie, sans aucune cérémonie requise pour le faire – les données apparaissaient simplement dans le flux d’entrée.

    Dire que awk a été une partie essentielle de ma boîte à outils informatique quotidienne est un euphémisme. Et pourtant, il y a quelques choses sur la façon dont j’utilise awk qui me laissent insatisfait.

    Le problème principal est probablement que awk est bon pour traiter les données présentées dans des champs délimités mais curieusement pas bon pour gérer les fichiers de valeurs séparées par des virgules, qui peuvent avoir des délimiteurs de champ intégrés dans un champ, à condition que le champ soit entre guillemets. De plus, les expressions régulières ont évolué depuis l’invention d’awk, et le fait de devoir se souvenir de deux ensembles de règles de syntaxe d’expression régulière n’est pas propice à un code sans bogue. Un ensemble de telles règles est déjà assez mauvais.

    Comme awk est un petit langage, il manque certaines choses que je trouve parfois utiles, comme un assortiment plus riche de types de base, de structures, d’instructions switch, etc.

    En revanche, Groovy a toutes ces bonnes choses : l’accès à la bibliothèque OpenCSVqui facilite le traitement des fichiers CSV, des expressions régulières Java et d’excellents opérateurs de correspondance, un riche assortiment de types de base, de classes, d’instructions switch, etc.

    Ce qui manque à Groovy, c’est la vue simple, orientée pipeline, des données en tant que flux entrant et des données traitées en tant que flux sortant.

    Mais mon framework de traitement de répertoire musical m’a fait penser, peut-être que je peux créer une version Groovy du “moteur” d’awk. C’est mon objectif pour cet article.

    Installer Java et Groovy

    Groovy est basé sur Java et nécessite une installation Java. Une version récente et décente de Java et de Groovy peut se trouver dans les référentiels de votre distribution Linux. Groovy peut également être installé en suivant les instructions sur le Page d’accueil géniale. Une bonne alternative pour les utilisateurs Linux est SDKMan, qui peut être utilisé pour obtenir plusieurs versions de Java, Groovy et de nombreux autres outils connexes. Pour cet article, j’utilise les versions du SDK de :

    • Java : version 11.0.12-open d’OpenJDK 11 ;
    • Groovy : version 3.0.8.

    Créer awk avec Groovy

    L’idée de base ici est d’encapsuler les complexités de l’ouverture d’un fichier ou de fichiers pour le traitement, de diviser la ligne en champs et de fournir un accès au flux de données en trois parties :

    • Avant le traitement de toute donnée
    • Sur chaque ligne de données
    • Une fois toutes les données traitées

    Je ne vais pas dans le cas général du remplacement de awk par Groovy. Au lieu de cela, je travaille sur mon cas d’utilisation typique, qui est :

    • Utilisez un fichier de script plutôt que d’avoir le code sur la ligne de commande
    • Traiter un ou plusieurs fichiers d’entrée
    • Définir mon délimiteur de champ par défaut sur | et les lignes fractionnées lues sur ce délimiteur
    • Utilisez OpenCSV pour faire le fractionnement (ce que je ne peux pas faire en awk)

    La classe framework

    Voici le “moteur awk” dans une classe Groovy :

     1 @Grab('com.opencsv:opencsv:5.6')
     2 import com.opencsv.CSVReader
     3 public class AwkEngine {
     4 // With admiration and respect for
     5 //     Alfred Aho
     6 //     Peter Weinberger
     7 //     Brian Kernighan
     8 // Thank you for the enormous value
     9 // brought my job by the awk
    10 // programming language
    11 Closure onBegin
    12 Closure onEachLine
    13 Closure onEnd

    14 private String fieldSeparator
    15 private boolean isFirstLineHeader
    16 private ArrayList<String> fileNameList
       
    17 public AwkEngine(args) {
    18     this.fileNameList = args
    19     this.fieldSeparator = "|"
    20     this.isFirstLineHeader = false
    21 }
       
    22 public AwkEngine(args, fieldSeparator) {
    23     this.fileNameList = args
    24     this.fieldSeparator = fieldSeparator
    25     this.isFirstLineHeader = false
    26 }
       
    27 public AwkEngine(args, fieldSeparator, isFirstLineHeader) {
    28     this.fileNameList = args
    29     this.fieldSeparator = fieldSeparator
    30     this.isFirstLineHeader = isFirstLineHeader
    31 }
       
    32 public void go() {
    33     this.onBegin()
    34     int recordNumber = 0
    35     fileNameList.each { fileName ->
    36         int fileRecordNumber = 0
    37         new File(fileName).withReader { reader ->
    38             def csvReader = new CSVReader(reader,
    39                 this.fieldSeparator.charAt(0))
    40             if (isFirstLineHeader) {
    41                 def csvFieldNames = csvReader.readNext() as
    42                     ArrayList<String>
    43                 csvReader.each { fieldsByNumber ->
    44                     def fieldsByName = csvFieldNames.
    45                         withIndex().
    46                         collectEntries { name, index ->
    47                             [name, fieldsByNumber[index]]
    48                         }
    49                     this.onEachLine(fieldsByName,
    50                             recordNumber, fileName,
    51                             fileRecordNumber)
    52                     recordNumber++
    53                     fileRecordNumber++
    54                 }
    55             } else {
    56                 csvReader.each { fieldsByNumber ->
    57                     this.onEachLine(fieldsByNumber,
    58                         recordNumber, fileName,
    59                         fileRecordNumber)
    60                     recordNumber++
    61                     fileRecordNumber++
    62                 }
    63             }
    64         }
    65     }
    66     this.onEnd()
    67 }
    68 }

    Bien que cela ressemble à un bon bout de code, de nombreuses lignes sont la continuation d’une division de lignes plus longues (par exemple, vous combineriez normalement les lignes 38 et 39, les lignes 41 et 42, etc.). Regardons cela ligne par ligne.

    La ligne 1 utilise le @Grab annotation pour récupérer la version 5.6 de la bibliothèque OpenCSV à partir de Maven centrale. Aucun XML requis.

    En ligne 2, j’importe des OpenCSV CSVReader classer.

    A la ligne 3, comme pour Java, je déclare une classe d’utilité publique, AwkEngine.

    Les lignes 11 à 13 définissent les instances Groovy Closure utilisées par le script comme crochets dans cette classe. Ceux-ci sont “publics par défaut” comme c’est le cas avec n’importe quelle classe Groovy, mais Groovy crée les champs en tant que références privées et externes à ceux-ci (en utilisant les getters et les setters fournis par Groovy). J’expliquerai cela plus en détail dans les exemples de scripts ci-dessous.

    Les lignes 14 à 16 déclarent les champs privés – le séparateur de champs, un indicateur pour indiquer si la première ligne d’un fichier est un en-tête et une liste pour le nom du fichier.

    Les lignes 17 à 31 définissent trois constructeurs. Le premier reçoit les arguments de la ligne de commande. Le second reçoit le caractère séparateur de champs. Le troisième reçoit le drapeau indiquant si la première ligne est un en-tête ou non.

    Les lignes 31 à 67 définissent le moteur lui-même, car le go() méthode.

    La ligne 33 appelle le onBegin() fermeture (équivalent à awk BEGIN {} déclaration).

    La ligne 34 initialise le recordNumber pour le flux (équivalent au awk NR variable) à 0 (notez que je fais ici l’origine 0 plutôt que l’origine awk 1).

    Les lignes 35 à 65 utilisent chacune {} pour boucler sur la liste des fichiers à traiter.

    La ligne 36 initialise le fileRecordNumber pour le fichier (équivalent à awk FNR variable) à 0 (origine 0, pas origine 1).

    Les lignes 37 à 64 obtiennent un Reader instance pour le fichier et le traiter.

    Les lignes 38-39 obtiennent un CSVReader exemple.

    La ligne 40 vérifie si la première ligne est traitée comme un en-tête.

    Si la première ligne est traitée comme un en-tête, les lignes 41-42 obtiennent la liste des noms d’en-tête de champ du premier enregistrement.

    Les lignes 43 à 54 traitent le reste des enregistrements.

    Les lignes 44 à 48 copient les valeurs de champ dans la carte de name:value.

    Les lignes 49 à 51 appellent le onEachLine() fermeture (équivalent à ce qui apparaît dans un programme awk entre BEGIN {} et END {}bien qu’aucun motif ne puisse être attaché pour rendre l’exécution conditionnelle), en passant dans la carte de name:valuele numéro d’enregistrement de flux, le nom de fichier et le numéro d’enregistrement de fichier.

    Les lignes 52-53 incrémentent le numéro d’enregistrement de flux et le numéro d’enregistrement de fichier.

    Autrement:

    Les lignes 56 à 62 traitent les enregistrements.

    Les lignes 57 à 59 appellent le onEachLine() fermeture, en passant dans le tableau des valeurs de champ, le numéro d’enregistrement de flux, le nom de fichier et le numéro d’enregistrement de fichier.

    Les lignes 60-61 incrémentent le numéro d’enregistrement de flux et le numéro d’enregistrement de fichier.

    La ligne 66 appelle le onEnd() fermeture (équivalent à awk END {}).

    Voilà pour le cadre. Vous pouvez maintenant le compiler :

    $ groovyc AwkEngine.groovy

    Quelques commentaires :

    Si un argument est passé qui n’est pas un fichier, le code échoue avec une trace de pile Groovy standard, qui ressemble à ceci :

    Caught: java.io.FileNotFoundException: not-a-file (No such file or directory)
    java.io.FileNotFoundException: not-a-file (No such file or directory)
    at AwkEngine$_go_closure1.doCall(AwkEngine.groovy:46)

    OpenCSV a tendance à revenir String[] valeurs, qui ne sont pas aussi pratiques que List valeurs dans Groovy (par exemple, il n’y a pas each {} défini pour un tableau). Les lignes 41-42 convertissent le tableau de valeurs du champ d’en-tête en une liste, alors peut-être fieldsByNumber à la ligne 57 doit également être converti en liste.

    Utilisation du framework dans les scripts

    Voici un script très simple utilisant AwkEngine examiner un dossier comme /etc/groupqui est délimité par deux-points et n’a pas d’en-tête :

    1 def ae = new AwkEngine(args, ‘:')
    2 int lineCount = 0

    3 ae.onBegin = {
    4    println “in begin”
    5 }

    6 ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->
    7    if (lineCount < 10)
    8       println “fileName $fileName fields $fields”
    9       lineCount++
    10 }

    11 ae.onEnd = {
    12    println “in end”
    13    println “$lineCount line(s) read”
    14 }

    15 ae.go()

    La ligne 1 appelle le constructeur à deux arguments, en passant la liste d’arguments et les deux-points comme délimiteur.

    La ligne 2 définit une variable de niveau supérieur du script, lineCountutilisé pour enregistrer le nombre de lignes lues (notez que les fermetures Groovy ne nécessitent pas de variables définies en dehors de la fermeture pour être finales).

    Les lignes 3 à 5 définissent le onBegin() fermeture, qui imprime simplement la chaîne “in begin” sur la sortie standard.

    Les lignes 6 à 10 définissent le onEachLine() fermeture, qui imprime le nom du fichier et les champs pour les 10 premières lignes et dans tous les cas incrémente le nombre de lignes.

    Les lignes 11-14 définissent le onEnd() fermeture, qui imprime la chaîne “in end” et le nombre de lignes lues.

    La ligne 15 exécute le script en utilisant le AwkEngine.

    Exécutez ce script comme suit :

    $ groovy Test1Awk.groovy /etc/group
    in begin
    fileName /etc/group fields [root, x, 0, ]
    fileName /etc/group fields [daemon, x, 1, ]
    fileName /etc/group fields [bin, x, 2, ]
    fileName /etc/group fields [sys, x, 3, ]
    fileName /etc/group fields [adm, x, 4, syslog,clh]
    fileName /etc/group fields [tty, x, 5, ]
    fileName /etc/group fields [disk, x, 6, ]
    fileName /etc/group fields [lp, x, 7, ]
    fileName /etc/group fields [mail, x, 8, ]
    fileName /etc/group fields [news, x, 9, ]
    in end
    78 line(s) read
    $

    Bien sûr le .class les fichiers créés en compilant la classe framework doivent être sur le classpath pour que cela fonctionne. Naturellement, vous pouvez utiliser jar pour empaqueter ces fichiers de classe.

    J’aime beaucoup le support de Groovy pour la délégation de comportement, qui nécessite diverses manigances dans d’autres langues. Pendant de nombreuses années, Java a nécessité des classes anonymes et pas mal de code supplémentaire. Les lambdas ont fait beaucoup pour résoudre ce problème, mais ils ne peuvent toujours pas faire référence à des variables non finales en dehors de leur portée.

    Voici un autre script plus intéressant qui rappelle beaucoup mon utilisation typique de awk :

    1 def ae = new AwkEngine(args, ‘;', true)
    2 ae.onBegin = {
    3    // nothing to do here
    4 }

    5 def regionCount = [:]
    6    ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->
    7    regionCount[fields.REGION] =
    8    (regionCount.containsKey(fields.REGION) ?
    9    regionCount[fields.REGION] : 0) +
    10   (fields.PERSONAS as Integer)
    11 }

    12 ae.onEnd = {
    13    regionCount.each { region, population ->
    14    println “Region $region population $population”
    15    }
    16 }

    17 ae.go()

    La ligne 1 appelle le constructeur à trois arguments, reconnaissant qu’il s’agit d’un “vrai fichier CSV” avec l’en-tête sur la première ligne. Comme il s’agit d’un fichier espagnol, où la virgule est utilisée comme “point” décimal, le délimiteur standard est le point-virgule.

    Les lignes 2 à 4 définissent le onBegin() fermeture qui dans ce cas ne fait rien.

    La ligne 5 définit un (vide) LinkedHashMap, que vous remplirez avec des clés String et des valeurs entières. Le fichier de données provient du recensement le plus récent du Chili et vous calculez le nombre de personnes dans chaque région du Chili dans ce script.

    Les lignes 6 à 11 traitent les lignes du fichier (il y en a 180 500, y compris l’en-tête) – notez que dans ce cas, parce que vous définissez la ligne 1 comme en-têtes de colonne CSV, le paramètre fields va être une instance de LinkedHashMap<String,String>.

    Les lignes 7 à 10 incrémentent le regionCount map, en utilisant la valeur dans le champ REGION comme clé et la valeur dans le champ PERSONAS comme valeur—notez que, contrairement à awk, dans Groovy, vous ne pouvez pas faire référence à une entrée de carte inexistante sur le côté droit et attendez-vous à ce qu’une valeur vide ou nulle se matérialise.

    Les lignes 12 à 16 impriment la population par région.

    La ligne 17 exécute le script sur le AwkEngine exemple.

    Exécutez ce script comme suit :

    $ groovy Test2Awk.groovy ~/Downloads/Censo2017/ManzanaEntidad_CSV/Censo*csv
    Region 1 population 330558
    Region 2 population 607534
    Region 3 population 286168
    Region 4 population 757586
    Region 5 population 1815902
    Region 6 population 914555
    Region 7 population 1044950
    Region 8 population 1556805
    Region 16 population 480609
    Region 9 population 957224
    Region 10 population 828708
    Region 11 population 103158
    Region 12 population 166533
    Region 13 population 7112808
    Region 14 population 384837
    Region 15 population 226068
    $

    C’est ça. Pour ceux d’entre vous qui aiment awk et qui aimeraient encore un peu plus, j’espère que vous apprécierez cette approche Groovy.

    Source

    Houssen Moshinaly

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code