Analyser un fichier CSV avec gawk
Question
Comment analyser un fichier CSV avec gawk? Il ne suffit pas de définir FS = ","
, un champ entre guillemets contenant une virgule sera traité comme un champ multiple.
Exemple d'utilisation de FS = ","
qui ne fonctionne pas:
:
one,two,"three, four",five
"six, seven",eight,"nine"
script gawk:
BEGIN { FS="," }
{
for (i=1; i<=NF; i++) printf "field #%d: %s\n", i, $(i)
printf "---------------------------\n"
}
mauvaise sortie:
field #1: one
field #2: two
field #3: "three
field #4: four"
field #5: five
---------------------------
field #1: "six
field #2: seven"
field #3: eight
field #4: "nine"
---------------------------
sortie souhaitée:
field #1: one
field #2: two
field #3: "three, four"
field #4: five
---------------------------
field #1: "six, seven"
field #2: eight
field #3: "nine"
---------------------------
La solution
La réponse courte est "Je n’utiliserais pas gawk pour analyser CSV si le CSV contient des données difficiles", où "maladroit" signifie, par exemple, des virgules dans les données de champ CSV.
La question suivante est "Quel autre traitement allez-vous effectuer", car cela influera sur les alternatives que vous utilisez.
J'utiliserais probablement Perl et les modules Text :: CSV ou Text :: CSV_XS pour lire et traiter les données. Rappelez-vous, Perl était à l'origine écrit en partie comme un tueur awk
et sed
- d'où les programmes a2p
et s2p
distribué avec Perl qui convertit les scripts awk
et sed
(respectivement) en Perl.
Autres conseils
Le manuel de gawk version 4 dit d'utiliser FPAT = "([^,] *) | (" "[^ \"] + + "") "
Lorsque FPAT
est défini, il désactive FS
et spécifie les champs par contenu plutôt que par séparateur.
Vous pouvez utiliser une fonction d'encapsuleur simple appelée csvquote pour nettoyer l'entrée et la restaurer après le traitement de awk. Transférez vos données au début et à la fin, et tout devrait bien se passer:
avant:
gawk -f mypgoram.awk input.csv
après:
csvquote input.csv | gawk -f mypgoram.awk | csvquote -u
Voir https://github.com/dbro/csvquote pour le code et la documentation.
Si cela est possible, j'utiliserais le module csv de Python, en effectuant un paiement spécial. attention au dialecte utilisé et paramètres de formatage requis , pour analyser le fichier CSV que vous avez.
csv2delim.awk
# csv2delim.awk converts comma delimited files with optional quotes to delim separated file
# delim can be any character, defaults to tab
# assumes no repl characters in text, any delim in line converts to repl
# repl can be any character, defaults to ~
# changes two consecutive quotes within quotes to '
# usage: gawk -f csv2delim.awk [-v delim=d] [-v repl=`"] input-file > output-file
# -v delim delimiter, defaults to tab
# -v repl replacement char, defaults to ~
# e.g. gawk -v delim=; -v repl=` -f csv2delim.awk test.csv > test.txt
# abe 2-28-7
# abe 8-8-8 1.0 fixed empty fields, added replacement option
# abe 8-27-8 1.1 used split
# abe 8-27-8 1.2 inline rpl and "" = '
# abe 8-27-8 1.3 revert to 1.0 as it is much faster, split most of the time
# abe 8-29-8 1.4 better message if delim present
BEGIN {
if (delim == "") delim = "\t"
if (repl == "") repl = "~"
print "csv2delim.awk v.m 1.4 run at " strftime() > "/dev/stderr" ###########################################
}
{
#if ("first","second","third"
"fir,st","second","third"
"first","sec""ond","third"
" first ",sec ond,"third"
"first" , "second","th ird"
"first","sec;ond","third"
"first","second","th;ird"
1,2,3
,2,3
1,2,
,2,
1,,2
1,"2",3
"1",2,"3"
"1",,"3"
1,"",3
"","",""
"","""aiyn","oh"""
"""","""",""""
11,2~2,3
~ repl) {
# print "Replacement character " repl " is on line " FNR ":" lineIn ";" > "/dev/stderr"
#}
if (rem test csv2delim
rem default is: -v delim={tab} -v repl=~
gawk -f csv2delim.awk test.csv > test.txt
gawk -v delim=; -f csv2delim.awk test.csv > testd.txt
gawk -v delim=; -v repl=` -f csv2delim.awk test.csv > testdr.txt
gawk -v repl=` -f csv2delim.awk test.csv > testr.txt
~ delim) {
print "Temp delimiter character " delim " is on line " FNR ":" lineIn ";" > "/dev/stderr"
print " replaced by " repl > "/dev/stderr"
}
gsub(delim, repl)
<*> = gensub(/([^,])\"\"/, "\\1'", "g")
# <*> = gensub(/\"\"([^,])/, "'\\1", "g") # not needed above covers all cases
out = ""
#for (i = 1; i <= length(<*>); i++)
n = length(<*>)
for (i = 1; i <= n; i++)
if ((ch = substr(<*>, i, 1)) == "\"")
inString = (inString) ? 0 : 1 # toggle inString
else
out = out ((ch == "," && ! inString) ? delim : ch)
print out
}
END {
print NR " records processed from " FILENAME " at " strftime() > "/dev/stderr"
}
test.csv
<*>test.bat
<*>Je ne sais pas si c'est la bonne façon de faire les choses. Je préférerais travailler sur un fichier csv dans lequel toutes les valeurs sont à citer ou aucune. Btw, awk permet aux expressions rationnelles d'être des séparateurs de champs. Vérifiez si cela est utile.
{
ColumnCount = 0
<*> = <*> "," # Assures all fields end with comma
while(<*>) # Get fields by pattern, not by delimiter
{
match(<*>, / *"[^"]*" *,|[^,]*,/) # Find a field with its delimiter suffix
Field = substr(<*>, RSTART, RLENGTH) # Get the located field with its delimiter
gsub(/^ *"?|"? *,$/, "", Field) # Strip delimiter text: comma/space/quote
Column[++ColumnCount] = Field # Save field without delimiter in an array
<*> = substr(<*>, RLENGTH + 1) # Remove processed text from the raw data
}
}
Les modèles qui suivent celui-ci peuvent accéder aux champs de la colonne []. ColumnCount indique le nombre d'éléments trouvés dans Column []. Si toutes les lignes ne contiennent pas le même nombre de colonnes, Column [] contient des données supplémentaires après Column [ColumnCount] lors du traitement des lignes les plus courtes.
Cette implémentation est lente, mais elle semble émuler la fonctionnalité FPAT
/ patsplit ()
trouvée dans gawk > = 4.0.0 mentionné dans une réponse précédente.
Voici ce que j'ai proposé. Tous les commentaires et / ou les meilleures solutions seraient les bienvenus.
BEGIN { FS="," }
{
for (i=1; i<=NF; i++) {
f[++n] = $i
if (substr(f[n],1,1)=="\"") {
while (substr(f[n], length(f[n]))!="\"" || substr(f[n], length(f[n])-1, 1)=="\\") {
f[n] = sprintf("%s,%s", f[n], $(++i))
}
}
}
for (i=1; i<=n; i++) printf "field #%d: %s\n", i, f[i]
print "----------------------------------\n"
}
L'idée de base est que je parcourt les champs. Tout champ commençant par une citation mais ne se terminant pas par une citation se voit attribuer le champ suivant qui y est ajouté.
Perl a le module Text :: CSV_XS spécialement conçu pour gérer l'étrangeté entre virgules et guillemets.
Sinon, essayez le module Text :: CSV.
perl -MText :: CSV_XS -ne 'BEGIN {$ csv = Texte :: CSV_XS- & new ()} if ($ csv- > parse ($ _)) {@ f = $ csv - "gt; fields (); for $ n (0 .. $ # f) {print" champ # $ n: $ f [$ n] \ n "quot;}; fichier" --- \ n "quot}} .csv
Produit cette sortie:
field #0: one
field #1: two
field #2: three, four
field #3: five
---
field #0: six, seven
field #1: eight
field #2: nine
---
Voici une version lisible par l'homme.
Enregistrez-le en tant que parsecsv, chmod + x et exécutez-le en tant que "parsecsv file.csv"
#!/usr/bin/perl
use warnings;
use strict;
use Text::CSV_XS;
my $csv = Text::CSV_XS->new();
open(my $data, '<', $ARGV[0]) or die "Could not open '$ARGV[0]' $!\n";
while (my $line = <$data>) {
if ($csv->parse($line)) {
my @f = $csv->fields();
for my $n (0..$#f) {
print "field #$n: $f[$n]\n";
}
print "---\n";
}
}
Vous devrez peut-être indiquer une version différente de perl sur votre ordinateur, car le module Text :: CSV_XS n'est peut-être pas installé sur votre version par défaut de perl.
Can't locate Text/CSV_XS.pm in @INC (@INC contains: /home/gnu/lib/perl5/5.6.1/i686-linux /home/gnu/lib/perl5/5.6.1 /home/gnu/lib/perl5/site_perl/5.6.1/i686-linux /home/gnu/lib/perl5/site_perl/5.6.1 /home/gnu/lib/perl5/site_perl .).
BEGIN failed--compilation aborted.
Si Text :: CSV_XS n'est installé dans aucune de vos versions de Perl, vous devez:
sudo apt-get install cpanminus
sudo cpanm Text :: CSV_XS