Supprimer la virgule entre les guillemets uniquement dans un fichier délimité par des virgules

23

J'ai un fichier d'entrée délimité par des virgules ( ,). Certains champs entourés de guillemets doubles contiennent une virgule. Voici l'exemple de ligne

123,"ABC, DEV 23",345,534.202,NAME

J'ai besoin de supprimer toutes les virgules qui se produisent à l'intérieur des guillemets doubles et des guillemets doubles également. Donc, la ligne ci-dessus doit être analysée comme indiqué ci-dessous

123,ABC DEV 23,345,534.202,NAME

J'ai essayé ce qui suit en utilisant sedmais sans donner les résultats attendus.

sed -e 's/\(".*\),\(".*\)/\1 \2/g'

Des astuces rapides avec sed, awkou tout autre utilitaire Unix s'il vous plaît?

mtk
la source
Je ne sais pas trop ce que vous essayez de faire, mais l'utilitaire "csvtool" est bien meilleur pour analyser csv que des outils génériques comme sed ou awk. C'est à peu près toutes les distributions de Linux.
figtrap

Réponses:

32

Si les citations sont équilibrées, vous souhaiterez supprimer les virgules entre toutes les autres citations, cela peut être exprimé awkcomme ceci:

awk -F'"' -v OFS='' '{ for (i=2; i<=NF; i+=2) gsub(",", "", $i) } 1' infile

Sortie:

123,ABC DEV 23,345,534.202,NAME

Explication

Le -F"fait que awk sépare la ligne au niveau des signes de guillemet double, ce qui signifie que tous les autres champs seront le texte entre guillemets. La boucle for s'exécute gsub, abréviation de globalement substitute, sur tous les autres champs, en remplaçant virgule ( ",") par Nothing ( ""). La 1fin invoque le code-bloc par défaut: { print $0 }.

Thor
la source
1
Pouvez-vous développer gsubet expliquer brièvement comment fonctionne cette doublure? S'il vous plaît.
mtk
Merci! Ce script fonctionne très bien, mais pourriez-vous expliquer le 1 solitaire à la fin du script? -} 1 '-
CocoaEv
@CocoaEv: il s'exécute { print $0 }. J'ai également ajouté cela à l'explication.
Thor
2
cette approche a un problème: parfois le csv a des lignes qui s'étendent sur plusieurs lignes, telles que: prefix,"something,otherthing[newline]something , else[newline]3rdline,and,things",suffix (c'est-à-dire: plusieurs lignes et imbriquées "," n'importe où dans un guillemet double multiligne: la "...."partie entière doit être rejointe et l'intérieur ,doit être remplacé / supprimé ...): votre script ne verra pas de paires de guillemets doubles dans ce cas, et ce n'est pas vraiment facile à résoudre (besoin de "rejoindre" les lignes qui sont dans un "ouvert" (c'est-à-dire, un nombre impair) double citation ... + faites attention s'il y a aussi un échappé \" à l' intérieur de la chaîne)
Olivier Dulac
1
J'ai adoré cette solution mais je l'ai modifiée car j'aime souvent garder les virgules mais je veux toujours les délimiter. Au lieu de cela, j'ai changé les virgules en dehors des guillemets en tuyaux, convertissant le csv en un fichier psv:awk -F'"' -v OFS='"' '{ for (I=1; i<=NF; i+=2) gsub(",", "|", $i) } 1' infile
Danton Noriega
7

Il y a une bonne réponse, en utilisant sed une seule fois avec une boucle :

echo '123,"ABC, DEV 23",345,534,"some more, comma-separated, words",202,NAME'|
  sed ':a;s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 /;ta'
123,"ABC  DEV 23",345,534,"some more  comma-separated  words",202,NAME

Explication:

  • :a; est une étiquette pour une branche plus éloignée
  • s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 / pourrait contenir 3 pièces fermées
    • d'abord le 2ème: [^"]*,\?\|"[^",]*",\?correspond à une chaîne ne contenant pas de guillemet double, peut-être suivie d'un coma ou d' une chaîne entourée de deux guillemets doubles, sans coma et peut-être suivie d'un coma.
    • que la première partie RE est composée par autant de répétition de la partie 2 précédemment décrite, suivie par 1 guillemet double et quelques caractères, mais pas de guillemet double, ni de comas.
    • La première partie RE doit être suivie d'un coma.
    • Nota, le reste de la ligne n'a pas besoin d'être touché
  • tasera mis en boucle :asi la s/commande précédente a changé.
F. Hauri
la source
Fonctionne également avec des guillemets imbriqués. Super merci!
tricasse
5

Une solution générale qui peut également gérer plusieurs virgules entre guillemets équilibrés nécessite une substitution imbriquée. J'implémente une solution en perl, qui traite chaque ligne d'une entrée donnée et ne substitue que des virgules dans toutes les autres paires de guillemets:

perl -pe 's/ "  (.+?  [^\\])  "               # find all non escaped 
                                              # quoting pairs
                                              # in a non-greedy way

           / ($ret = $1) =~ (s#,##g);         # remove all commas within quotes
             $ret                             # substitute the substitution :)
           /gex'

ou en bref

perl -pe 's/"(.+?[^\\])"/($ret = $1) =~ (s#,##g); $ret/ge'

Vous pouvez soit diriger le texte que vous souhaitez traiter vers la commande, soit spécifier le fichier texte à traiter comme dernier argument de ligne de commande.

user1146332
la source
1
Le [^\\]va avoir l'effet indésirable de faire correspondre le dernier caractère à l'intérieur des guillemets et de le supprimer (non \ caractère), c'est-à-dire que vous ne devez pas consommer ce caractère. Essayez (?<!\\)plutôt.
tojrobinson
Merci pour votre objection, j'ai corrigé cela. Néanmoins, je pense que nous n'avons pas besoin de regarder derrière l'assertion ici, ou le faisons-nous!?
user1146332
1
L'inclusion du non \ dans votre groupe de capture produit un résultat équivalent. +1
tojrobinson
1
+1. après avoir essayé quelques trucs avec sed, j'ai vérifié les documents de sed et confirmé qu'il ne pouvait pas appliquer de remplacement à la partie correspondante d'une ligne ... alors j'ai abandonné et essayé perl. Nous avons fini avec une approche très similaire , mais celui - ci utilise la version [^"]*pour rendre le non-gourmand match (c. -à- tout correspond d'une "à la suivante " ): perl -pe 's/"([^"]+)"/($match = $1) =~ (s:,::g);$match;/ge;'. Il ne reconnaît pas l'idée bizarre qu'une citation puisse être échappée avec une barre oblique inverse :-)
cas
Merci pour votre commentaire. Ce serait intéressant si l' [^"]*approche ou l' approche non gourmande explicite consomme moins de temps processeur.
user1146332
3

J'utiliserais une langue avec un analyseur CSV approprié. Par exemple:

ruby -r csv -ne '
  CSV.parse($_) do |row|
    newrow = CSV::Row.new [], []
    row.each {|field| newrow << field.delete(",")}
    puts newrow.to_csv
  end
' < input_file
glenn jackman
la source
alors que j'aimais cette solution au départ, elle s'est avérée être incroyablement lente pour les gros fichiers ...
KIC
3

Vos deuxièmes citations sont déplacées:

sed -e 's/\(".*\),\(.*"\)/\1 \2/g'

De plus, l'utilisation d'expressions régulières a tendance à correspondre à la partie la plus longue possible du texte, ce qui signifie que cela ne fonctionnera pas si vous avez plusieurs champs entre guillemets dans la chaîne.

Un moyen qui gère plusieurs champs entre guillemets dans sed

sed -e 's/\(\"[^",]\+\),\([^",]*\)/\1 \2/g' -e 's/\"//g'

C'est également un moyen de résoudre ce problème, cependant, avec une entrée pouvant contenir plus d'une virgule par champ cité, la première expression dans le sed devra être répétée autant de fois que le contenu maximal de la virgule dans un seul champ, ou jusqu'à ce qu'elle ne change pas du tout la sortie.

L'exécution de sed avec plusieurs expressions doit être plus efficace que plusieurs processus sed exécutés et un "tr" fonctionnant tous avec des tuyaux ouverts.

Cependant, cela peut avoir des conséquences indésirables si l'entrée n'est pas correctement formatée. c'est-à-dire des guillemets imbriqués, des guillemets non terminés.

En utilisant l'exemple en cours d'exécution:

echo '123,"ABC, DEV 23",345,534,"some more, comma-separated, words",202,NAME' \
| sed -e 's/\(\"[^",]\+\),\([^",]*\)/\1 \2/g' \
-e 's/\(\"[^",]\+\),\([^",]*\)/\1 \2/g' -e 's/\"//g'

Sortie:

123,ABC  DEV 23,345,534,some more  comma-separated  words,202,NAME
Didi Kohen
la source
Vous pouvez le rendre plus général avec branchement conditionnel et plus lisible avec ERE, par exemple avec GNU sed: sed -r ':r; s/("[^",]+),([^",]*)/\1 \2/g; tr; s/"//g'.
Thor
2

En perl - vous pouvez utiliser Text::CSVpour analyser cela, et le faire trivialement:

#!/usr/bin/env perl
use strict;
use warnings;

use Text::CSV; 

my $csv = Text::CSV -> new();

while ( my $row = $csv -> getline ( \*STDIN ) ) {
    #remove commas in each field in the row
    $_ =~ s/,//g for @$row;
    #print it - use print and join, rather than csv output because quotes. 
    print join ( ",", @$row ),"\n";
}

Vous pouvez imprimer avec Text::CSVmais cela a tendance à conserver les citations si vous le faites. (Bien que, je suggère - plutôt que de supprimer les guillemets pour votre sortie, vous pouvez simplement analyser en utilisant Text::CSVen premier lieu).

Sobrique
la source
0

J'ai créé une fonction pour boucler à travers chaque caractère de la chaîne.
Si le caractère est une citation, la vérification (b_in_qt) est marquée comme vraie.
Alors que b_in_qt est vrai, toutes les virgules sont remplacées par un espace.
b_in_qt est défini sur false lorsque la prochaine virgule est trouvée.

FUNCTION f_replace_c (str_in  VARCHAR2) RETURN VARCHAR2 IS
str_out     varchar2(1000)  := null;
str_chr     varchar2(1)     := null;
b_in_qt     boolean         := false;

BEGIN
    FOR x IN 1..length(str_in) LOOP
      str_chr := substr(str_in,x,1);
      IF str_chr = '"' THEN
        if b_in_qt then
            b_in_qt := false;
        else
            b_in_qt := true;
        end if;
      END IF;
      IF b_in_qt THEN
        if str_chr = ',' then
            str_chr := ' ';
        end if;
      END IF;
    str_out := str_out || str_chr;
    END LOOP;
RETURN str_out;
END;

str_in := f_replace_c ("blue","cat,dog,horse","",yellow,"green")

RESULTS
  "blue","cat dog horse","",yellow,"green"
user143598
la source