Rimuovere la virgola tra virgolette solo in un file delimitato da virgole


23

Ho un file di input delimitato da virgole ( ,). Ci sono alcuni campi racchiusi tra virgolette doppie che contengono una virgola. Ecco la riga di esempio

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

Devo rimuovere tutte le virgole che si trovano all'interno delle virgolette doppie e anche delle virgolette doppie. Quindi la riga sopra dovrebbe essere analizzata come mostrato di seguito

123,ABC DEV 23,345,534.202,NAME

Ho provato a utilizzare il seguente sedma non a dare i risultati previsti.

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

Eventuali trucchi rapidi con sed, awko qualsiasi altra utilità unix per favore?


Non sono sicuro di quello che stai cercando di fare, ma l'utilità "csvtool" è molto meglio per l'analisi del csv rispetto a strumenti generici come sed o awk. È presente in quasi ogni distribuzione di Linux.
figtrap,

Risposte:


32

Se le virgolette sono bilanciate, ti consigliamo di rimuovere le virgole tra ogni altra citazione, questo può essere espresso in awkquesto modo:

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

Produzione:

123,ABC DEV 23,345,534.202,NAME

Spiegazione

Le -F"marche awk separare la linea ai segni doppio virgolette, il che significa che ogni altro campo sarà il testo inter-preventivo. Il ciclo for viene eseguito gsub, abbreviazione di sostituto globale, su ogni altro campo, sostituendo la virgola ( ",") con niente ( ""). La 1alla fine invoca il codice blocco predefinito: { print $0 }.


1
Per favore, puoi approfondire gsube spiegare in breve, come funziona questa copertina ?? per favore.
mtk,

Grazie! Questo script funziona davvero bene, ma potresti spiegare l'1 solitario alla fine dello script? -} 1 '-
CocoaEv

@CocoaEv: esegue { print $0 }. L'ho aggiunto anche alla spiegazione.
Thor,

2
questo approccio ha un problema: a volte il CSV ha righe che si estendono su più righe, come ad esempio: prefix,"something,otherthing[newline]something , else[newline]3rdline,and,things",suffix (cioè: diverse righe e nidificate "," ovunque all'interno di una doppia virgoletta multi-riga: l'intera "...."parte dovrebbe essere unita e all'interno ,dovrebbe essere sostituito / rimosso ...): il tuo script non vedrà coppie di virgolette doppie in quel caso, e non è davvero facile da risolvere (è necessario "ricongiungere" le righe che sono in un "aperto" (cioè, dispari-numerate) virgoletta doppia ... + fai molta attenzione se c'è anche una fuga \" all'interno della corda)
Olivier Dulac

1
Ho adorato questa soluzione ma l'ho modificata dato che spesso mi piace mantenere le virgole ma voglio ancora delimitare. Invece, ho convertito le virgole al di fuori delle virgolette in pipe, convertendo il CSV in un file psv:awk -F'"' -v OFS='"' '{ for (I=1; i<=NF; i+=2) gsub(",", "|", $i) } 1' infile
Danton Noriega,

7

C'è una buona risposta, usando sed semplicemente una volta con un ciclo :

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

Spiegazione:

  • :a; è un'etichetta per il ramo furter
  • s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 / potrebbe contenere 3 parti allegate
    • prima la seconda: [^"]*,\?\|"[^",]*",\?corrispondenza per una stringa che non contiene virgolette doppie, forse seguita da un coma o una stringa racchiusa tra due virgolette doppie, senza coma e forse seguita da un coma.
    • la prima parte di RE è composta da altrettante ripetizioni della parte 2 precedentemente descritta, seguita da 1 virgoletta doppia e alcuni caratteri, ma nessuna virgoletta doppia, né virgole.
    • La prima parte RE deve essere seguita da un coma.
    • Nota, il resto della linea non deve essere toccato
  • taeseguirà il ciclo :ase il s/comando precedente ha apportato delle modifiche.

Funziona anche con virgolette nidificate. Fantastico, grazie!
tricasse

5

Una soluzione generale che può anche gestire diverse virgole tra virgolette bilanciate richiede una sostituzione nidificata. Implemento una soluzione in perl, che elabora ogni riga di un dato input e sostituisce solo virgole in ogni altra coppia di virgolette:

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'

o in breve

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

È possibile reindirizzare il testo che si desidera elaborare al comando o specificare il file di testo da elaborare come ultimo argomento della riga di comando.


1
La [^\\]sta per avere l'effetto indesiderato di corrispondenza dell'ultimo carattere dentro le citazioni e la rimozione (\ carattere non), vale a dire, non si dovrebbe consumare quel personaggio. Prova (?<!\\)invece.
Tojrobinson,

Grazie per la tua obiezione, l'ho corretto. Tuttavia, penso che non abbiamo bisogno di guardare dietro l'affermazione qui, o no !?
user1146332

1
Includere il non \ nel gruppo di acquisizione produce un risultato equivalente. +1
tojrobinson,

1
+1. dopo aver provato alcune cose con sed, ho controllato i documenti di sed e ho confermato che non può applicare un rimpiazzo solo alla porzione corrispondente di una riga ... quindi ho rinunciato e ho provato perl. Finito con un approccio molto simile, ma questa versione utilizza [^"]*per rendere il match non avido (cioè le partite tutto da uno "al successivo " ): perl -pe 's/"([^"]+)"/($match = $1) =~ (s:,::g);$match;/ge;'. Non riconosce l'idea stravagante che una citazione potrebbe essere sfuggita a una barra rovesciata :-)
cas

Grazie per il tuo commento. Sarebbe interessante se l' [^"]*approccio o l' approccio esplicito non avido consumino meno tempo della CPU.
user1146332

3

Vorrei usare una lingua con un parser CSV adeguato. Per esempio:

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

sebbene inizialmente mi piacesse questa soluzione, si è rivelata incredibilmente lenta per file di grandi dimensioni ...
KIC

3

Le tue seconde citazioni sono fuori posto:

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

Inoltre, l'uso delle espressioni regolari tende a corrispondere alla parte più lunga possibile del testo, il che significa che non funzionerà se nella stringa sono presenti più campi tra virgolette.

Un modo che gestisce più campi tra virgolette in sed

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

Questo è anche un modo per risolverlo, tuttavia, con input che possono contenere più di una virgola per campo tra virgolette, la prima espressione nella sed dovrebbe essere ripetuta tante volte quanto il massimo contenuto di virgola in un singolo campo, o fino a quando non modifica affatto l'output.

L'esecuzione di sed con più di un'espressione dovrebbe essere più efficiente di diversi processi sed in esecuzione e un "tr" tutto in esecuzione con pipe aperte.

Tuttavia, ciò potrebbe avere conseguenze indesiderate se l'input non è formattato correttamente. cioè virgolette nidificate, virgolette non terminate.

Utilizzando l'esempio corrente:

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'

Produzione:

123,ABC  DEV 23,345,534,some more  comma-separated  words,202,NAME

Si può rendere più generale con il branching condizionale e più leggibile con ERE, ad esempio con GNU sed: sed -r ':r; s/("[^",]+),([^",]*)/\1 \2/g; tr; s/"//g'.
Thor,

2

In perl - puoi usarlo Text::CSVper analizzare questo e farlo banalmente:

#!/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";
}

Puoi stampare con Text::CSVma tende a conservare le virgolette se lo fai. (Anche se, suggerirei - piuttosto che mettere a nudo le virgolette per il tuo output, potresti semplicemente analizzare usando Text::CSVin primo luogo).


0

Ho creato una funzione per passare in rassegna tutti i caratteri della stringa.
Se il carattere è tra virgolette, il segno di spunta (b_in_qt) è contrassegnato come vero.
Mentre b_in_qt è vero, tutte le virgole vengono sostituite con uno spazio.
b_in_qt è impostato su false quando viene trovata la virgola successiva.

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"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.