Outils de manipulation de données en ligne de commande

Formats d’échange de données

J’ai acheté un livre dont le titre m’a amusé Data Science at the Command Line où j’ai découvert 2 utilitaires, csvkit et jq, que j’utilise très fréquemment (le livre est en ligne accessible gratuitement).

Le livre utilise une définition de la science des données comme étant composée de cinq étapes :

  1. Obtenir les données
  2. Nettoyer les données
  3. Explorer les données
  4. Mettre en forme les données
  5. Interpréter les données

Dans les projets, on rencontre différents formats de données que l’on peut avoir à manipuler, à analyser. Parmi ces formats, on rencontre souvent : CSV, JSON, XML, … et des déclinaisons particulières de ces formats (XML-TEI, GeoXXX, …)

Ces formats sont faits pour permettre à des programmes d’échanger des données et ces données deviennent rapidement illisibles par un humain lorsqu’elles grossissent.

Il ne faut pas oublier que ces formats sont souvent bien spécifiés par des RFC, des recommandations W3C, etc. Il est important de les connaître pour ne pas partir sur des mises en oeuvre exotiques (typique avec le CSV, headers et séparateurs particuliers).

  • le format CSV (Comma-Separated Values) est décrit par la RFC 4180
  • le format JSON (JavaScript Object Notation) est décrit par la RFC 7159
  • le XML ou (eXtensible Markup Language) est un langage informatique de balisage générique, spécifié par XML 1.1 W3C recommandation

L’avantage des outils en ligne de commande, c’est que l’on peut les chaîner avec |.

Outils CSV

Données manipulées

  • COVID19-web_ITrCX3P.csv : fichier OMS des essais cliniques menés dans le monde pour le COVID-19 (projet COVID INS2I-INSERM)
  • donnees-foncieres-2019-rhone.csv : transactions immobilières et foncières dans le Rhône (data.gouv.fr)
  • corrections_aurehal_19juin2015.xlsx : liste de corrections faites par INRIA sur les structures de recherche dans AUReHAL

csvkit

csvkit est une suite d’outils pour manipuler et convertir des fichiers CSV.

installation

csvkit est codé en Python, il s’installe classiquement dans un environnement virtuel avec pip.

$ pip install csvkit

https://projet.liris.cnrs.fr/edp/cafes-developpeur-liris/2019-01-15-environnements-virtuels-en-python.html

streaming versus buffering

Attention lorsqu’il y a de gros fichiers, il y a des commandes qui chargent toutes les données en mémoire et des commandes qui travaillent en flux.

https://csvkit.readthedocs.io/en/latest/contributing.html#streaming-versus-buffering

csvlook

Visualisation tabulaire des données CSV

$ < COVID19-web_ITrCX3P.csv head -n 100 | csvlook | less -S
| TrialID              | Last Refreshed on | Public title
| -------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------
| NCT00173563          |        2015-02-19 | Induction of Cytokines in Human Monocytes by SARS-CoV in Adults and Children
| NCT00523276          |        2015-02-19 | SARS Survivor Evaluations
| NCT00533741          |        2015-02-19 | SARS Coronavirus Vaccine (SARS-CoV)
| NCT01376765          |        2015-02-19 | Phase I Dose Escalation SARS-CoV Recombinant S Protein, With and Without Adjuvant, Vaccine Study
| IRCT20180223038837N1 |        2019-01-14 | Comparative study of radiofrequency ablation & foam sclerotherapy for treatment patients suffering from varicose veins
| NCT04246242          |        2020-02-03 | A Randomized Multicenter Controlled Clinical Trial of Arbidol in Patients With 2019 Novel Coronavirus (2019-nCoV)
| ChiCTR2000029953     |        2020-02-17 | Construction and Analysis of Prognostic Predictive Model of Novel Coronavirus Pneumonia (COVID-19)
| ChiCTR2000029949     |        2020-02-17 | A Medical Records Based Study for the Effectiveness of Extracorporeal Membrane Oxygenation in Patients with Severe Novel Coronavirus Pneumonia (COVID-19)
| ChiCTR2000029947     |        2020-02-17 | A Randomized Controlled Trial for Qingyi No. 4 Compound in the treatment of Convalescence Patients of Novel Coronavirus Pneumonia (COVID-19)

less -S permet d’afficher les données sans lire l’intégralité des données avant de commencer (mais csvlook charge l’intégralité des données d’où le head -n 100`) et l’option -S permet d’éviter les retours à la ligne lorsque les lignes dépassent la taille de l’écran (no wrap)

Limiter le nombre de colonnes affichées avec --max-columns

$ < donnees-foncieres-2019-rhone.csv head -n 1000 | csvlook --max-columns 6 | less -S
| id_mutation | date_mutation | numero_disposition | nature_mutation                    | valeur_fonciere | adresse_numero | ... |
| ----------- | ------------- | ------------------ | ---------------------------------- | --------------- | -------------- | --- |
|             |    2017-01-10 |                  1 | Vente                              |       21000,00 |              9 | ... |
|             |    2017-01-05 |                  1 | Vente                              |      578800,00 |             31 | ... |
|             |    2017-01-05 |                  1 | Vente                              |      578800,00 |             31 | ... |
|             |    2017-01-03 |                  1 | Vente                              |      569800,00 |             21 | ... |

in2csv

in2csv convertit le fichier Excel en CSV (le premier onglet par défaut).

$ in2csv corrections_aurehal_19juin2015.xlsx >corrections_aurehal_19juin2015.csv
/home/fconil/Progs/python/csvkit/.venv/lib/python3.8/site-packages/agate/utils.py:276: UnnamedColumnWarning: Column 7 has no name. Using "h".

in2csv peut lister les onglets du fichier Excel et exporter un onglet spécifié

$ in2csv -n TrialDetails63.xlsx
TRIAL
SECONDARY ID
HEALTH CONDITION
CONDITION  CODE
INTERVENTION CODE
PRIMARY OUTCOME
SECONDARY OUTCOME
HOSPITAL
POSTCODE
COUNTRY OUTSIDE AUSTRALIA
FUNDING SOURCE
SECONDARY SPONSOR
OTHER COLLABORATOR
ETHICS COMMITTEE
CONTACTS
DATA SHARING STATEMENT
SUMMARY RESULTS

$ in2csv --sheet "COUNTRY OUTSIDE AUSTRALIA" TrialDetails63.xlsx | head | csvlook
| TRIAL ID | COUNTRY        | STATE                              |
| -------- | -------------- | ---------------------------------- |
|  383488 | Turkey         | Izmir                              |
|  383373 | India          | Several districts of Uttar Pradesh |
|  383203 | Turkey         | Izmir                              |
|  382922 | United Kingdom |                                    |
|  383360 | Bangladesh     | Several districts                  |
|  383303 | Indonesia      | West Java                          |

csvcut

Découvrir les colonnes d’un CSV avec csvcut -n

$ < COVID19-web_ITrCX3P.csv csvcut -n
  1: TrialID
  2: Last Refreshed on
  3: Public title
  4: Scientific title
  ...

Sélectionner les colonnes d’un CSV avec csvcut -c, je référence préférentiellement les colonnes par leur numéro (plus rapide et pas de problème de caractères particuliers dans les noms de commande).

$ < COVID19-web_ITrCX3P.csv head -n 100 | csvcut -c 1,2,7,10,12,18 | csvlook | less -S
i TrialID              | Last Refreshed on | Date registration | Source Register    | Recruitment Status | Target size                                                                                    >
| -------------------- | ----------------- | ----------------- | ------------------ | ------------------ | ----------------------------------------------------------------------------------------------->
| NCT00173563          |        2015-02-19 | 12/09/2005        | ClinicalTrials.gov | Recruiting         | 10                                                                                             >
| NCT00523276          |        2015-02-19 | 30/08/2007        | ClinicalTrials.gov | Not recruiting     | 0                                                                                              >
| NCT00533741          |        2015-02-19 | 20/09/2007        | ClinicalTrials.gov | Not recruiting     | 0                                                                                              >
| NCT01376765          |        2015-02-19 | 16/06/2011        | ClinicalTrials.gov | Not recruiting     | 0                                                                                              >
| IRCT20180223038837N1 |        2019-01-14 | 2018-12-08        | IRCT               | Recruiting         | 60                                                                                             >
| NCT04246242          |        2020-02-03 | 27/01/2020        | ClinicalTrials.gov | Not recruiting     | 500                                                                                            >
| ChiCTR2000029953     |        2020-02-17 | 2020-02-17        | ChiCTR             | Not Recruiting     | survival group:200;died:200;                                                                   >
| ChiCTR2000029949     |        2020-02-17 | 2020-02-16        | ChiCTR             | Not Recruiting     | Case series:40;                                                                                >
| ChiCTR2000029947     |        2020-02-17 | 2020-02-16        | ChiCTR             | Not Recruiting     | experimental group :100;control group :100;                                                    >

options générales

  • -d pour spécifier le délimiteur si ce n’est pas une virgule
  • -t pour indiquer qu’il s’agit d’un fichier tabulaire
  • -e pour spécifier l’encodage

Voir https://csvkit.readthedocs.io/en/latest/common_arguments.html

csvgrep

Recherche de chaîne de caractère dans une colonne :

  • -c indique la colonne concernée
  • -m recherche une correspondance exacte avec la chaîne de caractère passée en paramètre
$ csvgrep -c Sigle -m BEAGLE corrections_aurehal_19juin2015.csv | csvlook
| Sigle  | idStructureHal | siidRNSR   | sigleCRI | libelleEN                                      | Id HAL disparus après modification |      OK le | h |
| ------ | -------------- | ---------- | -------- | ---------------------------------------------- | ---------------------------------- | ---------- | - |
| BEAGLE |        160051 | 201120997E | GRA      | Artificial Evolution and Computational Biology |                            397150 | 2015-06-17 |   |

Avec -r on peut faire une recherche avec une expression régulière

$ csvgrep -c Sigle -r "(?i)beagle" corrections_aurehal_19juin2015.csv | csvlook
| Sigle  | idStructureHal | siidRNSR   | sigleCRI | libelleEN                                      | Id HAL disparus après modification |      OK le | h |
| ------ | -------------- | ---------- | -------- | ---------------------------------------------- | ---------------------------------- | ---------- | - |
| BEAGLE |        160051 | 201120997E | GRA      | Artificial Evolution and Computational Biology |                            397150 | 2015-06-17 |   |

Je l’ai utilisé lorsque je recevais périodiquement les fichiers Excel de modifications de structures faites dans AUReHAL, cela me permettait de chercher rapidement si la structure que l’on surveillait était concernée.

csvsort

$ < donnees-foncieres-2019-rhone.csv head -n 1000 | csvsort -c type_local | csvlook --max-columns 6 | less -S
| id_mutation | date_mutation | numero_disposition | nature_mutation                    | valeur_fonciere | adresse_numero | ... |
| ----------- | ------------- | ------------------ | ---------------------------------- | --------------- | -------------- | --- |
|             |    2017-01-05 |                  1 | Vente                              |      578800,00 |             31 | ... |
|             |    2017-01-05 |                  1 | Vente                              |      578800,00 |             31 | ... |
|             |    2017-01-03 |                  1 | Vente                              |      569800,00 |             21 | ... |
|             |    2017-01-05 |                  1 | Vente                              |      240000,00 |             31 | ... |
|             |    2017-01-09 |                  1 | Vente                              |      240000,00 |             50 | ... |

csvstat

Compter le nombre de lignes. Il peut y avoir des sauts de ligne dans les champs donc wc -l n’est pas sûr.

$ csvstat --count TrialDetails63.1.csv
258

$ wc -l TrialDetails63.1.csv
8901 TrialDetails63.1.csv

Affiche des statistiques sur les colonnes du CSV, permet d’avoir une information sur les données contenues et un aperçu de leur qualité.

$ csvstat COVID19-web_ITrCX3P.csv
...
12. "Recruitment Status"

    Type of data:          Text
    Contains null values:  True (excluded from calculations)
    Unique values:         6
    Longest value:         14 characters
    Most common values:    Recruiting (5030x)
                           Not recruiting (3837x)
                           Not Recruiting (3207x)
                           Authorised (442x)
                           None (43x)
...
16. "Inclusion gender"

    Type of data:          Text
    Contains null values:  True (excluded from calculations)
    Unique values:         21
    Longest value:         32 characters
    Most common values:    All (6643x)
                           Both (2237x)
                           None (1789x)
                           <br>Female: yes<br>Male: yes<br> (571x)
                           Both, male and female (410x)

csvsql

ATTENTION : L’utilisation naïve de csvsql peut conduire à des noms de tables et de colonnes difficilement utilisables dans les requêtes.

  • Il faut éviter les tirets dans le nom de la table
  • Il faut éviter les espaces, les tirets et les caractères spéciaux dans les noms de colonnes

Si cela n’est pas fait, on doit mettre le nom de la table et le nom des champs entre guillemets.

utilisation simple

csvsql --db va créer et remplir une base de données SQLite à partir du CSV en inférant le type des colonnes. Sans l’option --insert, les données ne sont pas insérées dans la table.

On précise le nom de la table créée avec --tables pour éviter un nom de table compliqué à manipuler.

$ csvsql --db sqlite:///donnees-foncieres-2019-rhone.db --tables immobilier --insert donnees-foncieres-2019-rhone.csv
sqlite> select count(id), type_local from immobilier group by type_local;
 count(id) = 24988
type_local =

 count(id) = 25171
type_local = Appartement

 count(id) = 20118
type_local = Dépendance

 count(id) = 6013
type_local = Local industriel. commercial ou assimilé

 count(id) = 9708
type_local = Maison

csvsql --query permet d’exécuter une requête SQL sur un fichier csv qui est chargé dans une base SQLite en mémoire.

$ csvsql --query "select count(id), type_local from 'donnees-foncieres-2019-rhone' group by type_local;" donnees-foncieres-2019-rhone.csv | csvlook
| count(id) | type_local                               |
| --------- | ---------------------------------------- |
|    24988 |                                          |
|    25171 | Appartement                              |
|    20118 | Dépendance                               |
|     6013 | Local industriel. commercial ou assimilé |
|     9708 | Maison                                   |

Bien que ce soit intéressant, j’ai abandonné cet usage :

  • dès que le fichier CSV est gros cela prend du temps
  • car on a souvent plusieurs requêtes à exécuter, ou on adapte souvent la requête initiale en fonction des résultats et cela implique de recréer la base à chaque fois
  • on est confronté à plusieurs difficultés liés à la création et au remplissage de la table
difficultés

Les noms de colonnes du header de COVID19-web_ITrCX3P.csv contiennent des espaces.

$ csvsql --db sqlite:///COVID19.db --tables COVID19 --insert COVID19-web_ITrCX3P.csv
sqlite> select distinct("Recruitment Status") from COVID19;
Recruitment Status = Recruiting
Recruitment Status = Not recruiting
Recruitment Status = Not Recruiting
Recruitment Status =
Recruitment Status = Authorised
Recruitment Status = Not Available

csvsql -i va inférer le type des colonnes (comme csvstat) pour générer une instruction CREATE TABLE pour un type de base de données spécifié. Cela permet d’adapter le nom de la table et des colonnes pour créer une base propre.

$ csvsql -i sqlite COVID19-web_ITrCX3P.csv
CREATE TABLE "COVID19-web_ITrCX3P" (
    "TrialID" VARCHAR NOT NULL,
    "Last Refreshed on" DATE NOT NULL,
    "Public title" VARCHAR,
    "Scientific title" VARCHAR,
    "Acronym" VARCHAR,
...
);

Mais je n’ai pas réussi à charger les données avec csvsql et des noms de colonnes modifiées

# KO
$ csvsql --db sqlite:///COVID19_fixed.db --before-insert "$(cat create-table-COVID19-fixed.sql)" --insert COVID19-web_ITrCX3P.csv

# KO
$ < COVID19-web_ITrCX3P.csv sed "1d" > COVID19-web_ITrCX3P.no-header.csv
$ csvsql --db sqlite:///COVID19_fixed.db --no-create --table COVID19 --before-insert "$(cat create-table-COVID19-fixed.sql)" -H --insert COVID19-web_ITrCX3P.no-header.csv

On a une erreur plus parlante en pur SQLite où l’import fonctionne en enlevant les contraintes CHECK générées.

sqlite> .read create-table-COVID19-fixed.sql
sqlite> .mode csv
sqlite> .import COVID19-web_ITrCX3P.no-header.csv COVID19
...
COVID19-web_ITrCX3P.no-header.csv:25792: INSERT failed: CHECK constraint failed: COVID19

sqlite> .read create-table-COVID19-fixed-no-check.sql
sqlite> .mode csv
sqlite> .import COVID19-web_ITrCX3P.no-header.csv COVID19
sqlite> select count(TrialID) from COVID19;
count(TrialID)
12564

Même sans ces contraintes CHECK, cela ne fonctionne toujours pas avec csvsql.

$ csvsql --db sqlite:///COVID19_fixed.db --no-create --table COVID19 --before-insert "$(cat create-table-COVID19-fixed-no-check.sql)" -H --insert COVID19-web_ITrCX3P.no-header.csv

Il faut renommer les colonnes dans le header.

$ < COVID19-web_ITrCX3P.csv sed "1s/ /_/g" > COVID19-web_ITrCX3P.header-fixed.csv
$ csvsql --db sqlite:///COVID19_header_fixed.db --table COVID19 --insert COVID19-web_ITrCX3P.header-fixed.csv

Commandes non utilisées

sql2csv

Je ne m’en suis jamais servie, quand j’ai une base SQL, j’ai tendance à utiliser SQL directement.

$ sql2csv --db sqlite:///donnees-foncieres-2019-rhone.db --query "select count(id), type_local from immobilier group by type_local;" | csvlook
| count(id) | type_local                               |
| --------- | ---------------------------------------- |
|    24988 |                                          |
|    25171 | Appartement                              |
|    20118 | Dépendance                               |
|     6013 | Local industriel. commercial ou assimilé |
|     9708 | Maison                                   |
csvclean

La commande place placer les lignes valides dans un fichier [basename]_out.csv et les lignes erronées dans un fichier [basename]_err.csv.

Avec l’option -n, la commande fonction en dry-run.

https://csvkit.readthedocs.io/en/latest/scripts/csvclean.html

csvjoin

Permet de fusionner 2 fichiers CSV ou plus (avec des colonnes différentes j’imagine).

https://csvkit.readthedocs.io/en/latest/scripts/csvclean.html

csvstack

Permet de fusionner les lignes de plusieurs CSV.

https://csvkit.readthedocs.io/en/latest/scripts/csvstack.html

diff de CSV

csv-diff

https://datasette.io/tools/csv-diff

$ pip install csv-diff

Différence entre 2 fichiers récupérés sur data.gouv.fr et qui semble avoir la même clé pour les député et à peu près les même colonnes.

$ csv-diff --key id --format csv deputes-historique/deputes-historique.csv deputes-actifs/deputes-15.csv  | more
2 columns added, 2 columns removed, 575 rows changed, 1022 rows removed

2 columns added

  placeHemicycle
  premiereElection

2 columns removed

  active
  legislatureLast

575 rows changed

  id: PA605036
    age: "41" => "40"
    twitter: "@damienabad" => ""
    facebook: "damienabad.depute" => ""
    experienceDepute: "10 ans" => "9 ans"
    scoreParticipationSpecialite: "0.21" => "0.30"
    scoreLoyaute: "0.911" => "0.913"
    scoreMajorite: "0.352" => "0.345"
    dateMaj: "2022-01-19" => "2021-02-14"

  id: PA719866
    age: "45" => "44"
    experienceDepute: "5 ans" => "4 ans"
    scoreParticipation: "0.30" => "0.29"
    scoreLoyaute: "0.991" => "0.992"
    scoreMajorite: "0.991" => "0.992"
    dateMaj: "2022-01-19" => "2021-02-14"

...

Voir aussi https://datasette.io/

daff

$ pip install daff

Peut s’utiliser en ligne de commande et programmatiquement.

Indiqué dans la documentation csvkit pour le diff de fichiers CSV : https://csvkit.readthedocs.io/en/latest/cli.html

$ daff --input-format csv --id id  ~/Donnees/deputes/data.gouv.fr/deputes-historique/deputes-historique.csv ~/Donnees/deputes/data.gouv.fr/deputes-actifs/deputes-15.csv

Envoie le résultat dans un navigateur avec --www

$ daff --www --input-format csv --id id  ~/Donnees/deputes/data.gouv.fr/deputes-historique/deputes-historique.csv ~/Donnees/deputes/data.gouv.fr/deputes-actifs/deputes-15.csv

Voir le résultat : tmpz1kk4q6q.html.

Ignorer une ou plusieurs colonnes

$ daff --www --input-format csv --ignore "Export date" --id TrialID COVID19-web_FlhWKml.csv COVID19-web_PXQX3YP.csv

Voir le résultat : tmpk49hjf6k.html.

validation de csv

Je souhaitais trouver une solution de validation de la qualité des fichiers CSV.

frictionless

$ pip install frictionless[sql]

Validation simple d’un fichier CSV

$ frictionless validate COVID19-web_ITrCX3P.csv
# -------
# invalid: COVID19-web_ITrCX3P.csv
# -------

=====  =====  ==========  =============================================================================================================================
row    field  code        message
=====  =====  ==========  =============================================================================================================================
  593     38  type-error  Type error in the cell "True " in row "593" and field "Bridging flag truefalse" at position "38": type is "boolean/default"
  602     38  type-error  Type error in the cell "True " in row "602" and field "Bridging flag truefalse" at position "38": type is "boolean/default"

Génération d’un schéma YAML pour la validation du CSV.

Table Schema (frictionless standard) : https://specs.frictionlessdata.io/table-schema/#language

$ frictionless describe COVID19-web_ITrCX3P.csv --type schema >oms-schema.yaml
# --------
# metadata: COVID19-web_ITrCX3P.csv
# --------

fields:
  - name: TrialID
    type: string
  - name: Last Refreshed on
    type: string
  - name: Public title
    type: string
  - name: Scientific title
    type: string
  - name: Acronym
    type: any
  - name: Primary sponsor
    type: string
  - name: Date registration
    type: string
  - name: Date registration3
    type: integer
  - name: Export date
    type: string
  - name: Source Register
    type: string
  - name: web address
    type: string
  - name: Recruitment Status
    type: string
    ...
  - name: Retrospective flag
    type: string
  - name: Bridging flag truefalse
    type: boolean
  - name: Bridged type
    type: string
  - name: results yes no
    type: any

Les valeurs True contiennent un espace en trop dans le CSV pour le champ « Bridging flag truefalse » : « True « 

$ frictionless validate COVID19-web_ITrCX3P.csv --schema oms-schema.yaml
# invalid: COVID19-web_ITrCX3P.csv
# -------

=====  =====  ==========  =============================================================================================================================
row    field  code        message
=====  =====  ==========  =============================================================================================================================
  593     38  type-error  Type error in the cell "True " in row "593" and field "Bridging flag truefalse" at position "38": type is "boolean/default"
  602     38  type-error  Type error in the cell "True " in row "602" and field "Bridging flag truefalse" at position "38": type is "boolean/default"
  626     38  type-error  Type error in the cell "True " in row "626" and field "Bridging flag truefalse" at position "38": type is "boolean/default"
  639     38  type-error  Type error in the cell "True " in row "639" and field "Bridging flag truefalse" at position "38": type is "boolean/default"

$ < ~/Documents/RstDocs/mespres/data-tools-in-cmde-line/csv/COVID19-web_ITrCX3P.csv csvcut -c 1,13,37,38,40 | sed -n "1p;590,600p"
TrialID,other records,Retrospective flag,Bridging flag truefalse,results yes no
ChiCTR2000030331,No,No,False,
ChiCTR2000030328,No,Yes,False,
EUCTR2020-001301-23-FR,No,Yes,False,
EUCTR2020-001188-96-FR,Yes,Yes,True ,
EUCTR2020-001457-43-FR,No,Yes,False,
EUCTR2020-001442-19-ES,No,No,False,
EUCTR2020-001409-21-ES,No,No,False,
EUCTR2020-001492-33-FR,No,Yes,False,
EUCTR2020-001194-69-ES,No,No,False,
EUCTR2020-001565-37-ES,No,No,False,
EUCTR2020-001421-31-ES,No,No,False,

pandas

Une fois les données explorées, on utilise la bibliothèque pandas pour manipuler les données.

Outils JSON

Données manipulées

jq

S’il s’agit de données que l’on récupère (Assemblée nationale), on a besoin de découvrir progressivement l’architecture des données json que l’on récupère.

$ < votes/VTANR5L15V3732.json jq '.'

$ < votes/VTANR5L15V3732.json jq '.' -C | less -R

Le plus basique consiste à tout afficher, jq applique une coloration et une mise en page qui facilite la lecture de données json qui peuvent être sur une seule ligne.

{
  "scrutin": {
    "@xmlns": "http://schemas.assemblee-nationale.fr/referentiel",
    "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
    "uid": "VTANR5L15V3732",
    "numero": "3732",
    "organeRef": "PO717460",
    "legislature": "15",
    "sessionRef": "SCR5A2021O1",
    "seanceRef": "RUANR5L15S2021IDS24293",
    "dateScrutin": "2021-04-17",
    "quantiemeJourSeance": "2",
    "typeVote": {
      "codeTypeVote": "SPO",
      "libelleTypeVote": "scrutin public ordinaire",
      "typeMajorite": "majorité absolue des suffrages exprimés"
    },
    "sort": {
      "code": "rejeté",
      "libelle": "L'Assemblée nationale n'a pas adopté"
    },
    "titre": "l'amendement n° 4548 de M. Balanant et les amendements identiques suivants après l'article 71 du projet de loi portant lutte contre le dérèglement climatique et renforcement de la résilience face à ses effets (première lecture).",
    "demandeur": {

Ici la structure de base est un dictionnaire avec une seule clé.

$ < votes/VTANR5L15V3732.json jq 'keys'
[
  "scrutin"
]

On va donc extraire la valeur de la clé « scrutin », qui est un dictionnaire et dont on va demander les clés en combinant les filtres avec |

$ < votes/VTANR5L15V3732.json jq '.scrutin | keys'
[
  "@xmlns",
  "@xmlns:xsi",
  "dateScrutin",
  "demandeur",
  "legislature",
  "miseAuPoint",
  "modePublicationDesVotes",
  "numero",
  "objet",
  "organeRef",
  "quantiemeJourSeance",
  "seanceRef",
  "sessionRef",
  "sort",
  "syntheseVote",
  titre",
  "typeVote",
  "uid",
  "ventilationVotes"
]

On peut extraire quelques valeurs du dictionnaire dans un objet avec {} où l’on reprend les clés à extraire.

$ < votes/VTANR5L15V3732.json jq '.scrutin | {titre, uid, demandeur}'
{
  "titre": "l'amendement n° 4548 de M. Balanant et les amendements identiques suivants après l'article 71 du projet de loi portant lutte contre le dérèglement climatique et renforcement de la résilience face à ses effets (première lecture).",
  "uid": "VTANR5L15V3732",
  "demandeur": {
    "texte": "Présidente du groupe \"Socialistes et apparentés\"",
    "referenceLegislative": null
  }
}

Il y a un vote par fichier, comment obtenir une liste des votes et leur fichier ?

Pour exporter en csv (@csv), il faut générer un tableau json avec []. -r permet d’éviter que chaque ligne soit encadrée par des guillemets.

$ echo '"uid","titre"' >liste-votes.csv
$ ls -1 votes/*.json | while read A
> do
> jq -r '.scrutin | [.uid, .titre] | @csv' $A >>liste-votes.csv
> done
< covid/EudraCT_2020-001385-11_ES.json jq -C '.' | less -R
< covid/EudraCT_2020-001385-11_ES.json jq 'keys'

# Ouvrir l'URL de l'essai
< covid/EudraCT_2020-001385-11_ES.json jq '.metadata'

< covid/EudraCT_2020-001385-11_ES.json jq -C '.sections' | less -R
< covid/EudraCT_2020-001385-11_ES.json jq -C '.sections[2] ' | less -R

# Afficher toutes les sous-sections en supprimant le tableau englobant de la sous-section
< covid/EudraCT_2020-001385-11_ES.json jq -C '.sections[] | .subsections | .[] ' | less -R

# Extraire le nom de toutes les sous-sections
< covid/EudraCT_2020-001385-11_ES.json jq -C '.sections[] | .subsections[] | .name'

# Y a-t-il une sous-section qui se répète
# Pas pratique sûrement combinable
< covid/EudraCT_2020-001385-11_ES.json jq -C '.sections[] | .subsections[] | .name | contains(": 2")'

< covid/EudraCT_2020-001385-11_ES.json jq -C '.sections[] | .subsections | .[]' | grep ": 2"

# Récupérer l'élément "A.1" des sections A des fichiers
ls -1 covid/EudraCT_*.json | while read A; do echo $A; cat $A | jq '.sections[] | select (.id == "section-a") | .subsections[] | .data[] | select(.id == "A.1")'; done

Outils XML

Matériel - code

Voir ~/COVID-19/Trial-registries/ISRCTN-UK
~/Progs/xml/xslt
~/PyEnvs-a-trier/PyEnv3.5.2/webscraping/automate-liris-tei-export

1er cas : XML-TEI pour HAL

Format utilisé pour le transfert des publications du Système d’Information LIRIS vers HAL.

Besoin :

  1. Reformatter le fichier reçu du Système d’Information pour le rendre humainement lisible
  2. Vérifier que le fichier est syntaxiquement correct par rapport au schéma XSD du CCSD-HAL

Outil utilisé xmllint, xmllint est un outil associé à libxml2 et qui nécessite le paquet libxml2-utils.

Installation : $ sudo apt install libxml2-utils

$ wget -O xmltei/hal-tei-01529866.xml -nv https://hal.archives-ouvertes.fr/hal-01529866v1/tei

Reformatter le fichier (pas nécessaire pour celui récupéré de HAL)

$ xmllint --format "./xmltei/liris-tei-2853.xml" >"liris-tei-2853.xml"

Vérifier que le fichier est syntaxiquement valide

La vérification ne fonctionne pas avec le fichier que je viens de récupérer de HAL parce que le schéma XSD que j’utilise est un peu ancien et qu’il servait à vérifier les fichiers déposés il y a quelques années. Mais ça montrer l’affichage de Schemas validity error.

$ xmllint --schema ./tei/aofr-sword.xsd "liris-tei-2853.xml" >"./xmltei/liris-tei-2853.xml" 2>&1

$ xmlstarlet val -s tei/aofr-sword.xsd "liris-tei-2853.xml"

2ème cas : Projet COVID INS2I-INSERM

Présentation simple : On récupère des métadonnées des études cliniques de différents pays / continents. On en extrait une partie que l’on transforme pour éviter au CRESS qui analyse ces études de récupérer ces métadonnées manuellement.

On manipule une grande variété de formats : CSV, XML, accès BD, scrapping de site web, …

Dernièrement, j’ai voulu extraire les métadonnées du « registre » anglais puisque les anglais ne déposent plus les métadonnées de leurs études dans le registre européen depuis le Brexit. Une API est fournie qui renvoie du XML.

Il m’a fallu plonger un peu plus sur la façon dont je pouvais analyser puis extraire des données de ce format. J’ai consulté quelques documentations (Structurez vos données avec XML) et regardé XPath (XML Path Language) pour l’extraction.

XMLStarlet

J’avais vu qu’il existait un outil XMLStarlet que j’ai utilisé pour explorer les données récupérées du registre anglais. Voir mémo xmlstarlet et documentation xmlstarlet.

el -v : afficher les éléments de la structure du document.

$ xmlstarlet <commande> --help

$ xmlstarlet el -v liris-tei-2853.xml

sel -l -v "//biblFull/titleStmt/title" : extraire des éléments selon un template XPath.

Malheureusement, l’utilisation d’un namespace par défaut dans le XML pose problème aux 2 outils que j’ai utilisés et la commande ci dessous ne retourne rien.

$ xmlstarlet sel -t -v "//biblFull/titleStmt/title" liris-tei-2853.xml

$ head liris-tei-2853.xml | bat

Le 1er contournement nécessite de modifier tous les éléments du chemin XPath : bof

$ xmlstarlet sel -N x="http://www.tei-c.org/ns/1.0" -t -v "//x:biblFull/x:titleStmt/x:title" liris-tei-2853.xml

# Plus simplement
$ xmlstarlet sel -t -v "//_:biblFull/_:titleStmt/_:title" liris-tei-2853.xml

La 2ème solution consiste à transformer le XML en un format pyx, dont on supprime les namespace et que l’on transforme à nouveau en XML.

C’est « bourrin » mais ça m’a suffit pour mon exploration des données.

$ xmlstarlet pyx liris-tei-2853.xml | grep -v "^Axmlns" | xmlstarlet p2x
  | xmlstarlet sel -t -v "//biblFull/titleStmt/title"

J’ai pris l’habitude de nommer le fichier résultant xxx.no-default-ns.xml

$ xmlstarlet pyx query-covid-and-recruiting-curl_internal.xml | grep -v "^Axmlns"
  | xmlstarlet p2x >query-covid-and-recruiting-curl_internal.no-default-ns.xml

J’ai ensuite pu faire les explorations souhaitées sur le résultat d’une recherche sur les études COVID.

$ xmlstarlet sel -t -v "/allTrials/@totalCount" isrctn/query-covid-and-recruiting-curl_internal.no-default-ns.xml

$ xmlstarlet sel -t -v "//fullTrial/trial/externalRefs/doi" isrctn/query-covid-and-recruiting-curl_internal.no-default-ns.xml

$ xmlstarlet sel -t -v "count(//fullTrial/trial/externalRefs/doi)" isrctn/query-covid-and-recruiting-curl_internal.no-default-ns.xml

tr xsl/transform-search-3.xsl : transformation selon la feuille de style transform-search-3.xsl.

Exemple de quelques transformations XSLT

$ xmlstarlet tr xsl/transform-search-3.xsl isrctn/query-covid-and-recruiting-curl_internal.no-default-ns.xml

$ xmlstarlet tr xsl/transform-search-3.txt.xsl isrctn/query-covid-and-recruiting-curl_internal.no-default-ns.xml

Avertissement

xmlslarlet repose sur la bibliothèque C libxslt qui met en œuvre la version 1.0 de XSLT. Elle ne dispose donc pas de certaines fonctions comme replace().

https://www.oreilly.com/library/view/xslt-2nd-edition/9780596527211/ch05s06s02.html

$ sudo apt-cache depends xmlstarlet
xmlstarlet
  Dépend: libc6
  Dépend: libxml2
  Dépend: libxslt1.1

$ sudo apt show libxslt1.1
Description: traitement XSLT 1.0 - bibliothèque d'exécution

xml2json

Test de l’outil xml2json par curiosité.

$ < isrctn/ISRCTN17072692-internal.no-default-ns.xml xml2json | jq '.'

xmllint

Retour sur l’outil xmllint et son option --shell très intéressante pour explorer un fichier XML.

Dommage que l’on ne puisse pas rappeler les commandes précédentes.

$ xmllint --shell isrctn/query-covid-and-recruiting-curl_internal.no-default-ns.xml
/ > help

> xpath //fullTrial/trial/externalRefs/doi/text()

> xpath count(//fullTrial/trial/externalRefs/doi)

> cd allTrials

allTrials > dir

allTrials > cd fullTrial[1]

fullTrial > ls

fullTrial > cd trial

trial > ls
trial > cat externalRefs
 -------
<externalRefs>
      <doi>10.1186/ISRCTN79815558</doi>
      <eudraCTNumber>2021-000522-97</eudraCTNumber>
      <clinicalTrialsGovNumber>Nil known</clinicalTrialsGovNumber>
      <protocolSerialNumber>VLA2001-301, IRAS 294164</protocolSerialNumber>
    </externalRefs>

trial > du

trial > grep doi

trial > cd trialDescription
trialDescription > ls
trialDescription > pwd

lxml

Une fois l’exploration terminée, codage de l’extraction de données en Python avec lxml.

Outils YAML

yq

yq est utilisé par Emmanuel pour manipuler des fichiers YAML qui peuvent être nombreux dans des outils comme Kubernetes.