Remover vírgula entre aspas apenas em um arquivo delimitado por vírgula

23

Eu tenho um arquivo de entrada delimitado por vírgulas ( ,). Existem alguns campos entre aspas duplas que possuem vírgula. Aqui está a linha de amostra

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

Eu preciso remover todas as vírgulas que ocorrem dentro das aspas duplas e aspas duplas também. Portanto, a linha acima deve ser analisada conforme mostrado abaixo

123,ABC DEV 23,345,534.202,NAME

Eu tentei o seguinte usando, sedmas não dando os resultados esperados.

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

Algum truque rápido com sed, awkou qualquer outro utilitário unix, por favor?

mtk
fonte
Não sei ao certo o que você está tentando fazer, mas o utilitário "csvtool" é muito melhor para analisar o csv do que ferramentas genéricas como sed ou awk. Está em quase todas as distros do linux.
figtrap

Respostas:

32

Se as aspas estiverem equilibradas, você deverá remover vírgulas entre todas as outras aspas, isso pode ser expresso da awkseguinte maneira:

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

Saída:

123,ABC DEV 23,345,534.202,NAME

Explicação

O -F"makes awk separa a linha nos sinais de aspas duplas, o que significa que todos os outros campos serão o texto entre aspas. O loop for é executado gsub, abreviação de substituto global, em todos os outros campos, substituindo vírgula ( ",") por nada ( ""). A 1no final invoca o código-padrão do bloco: { print $0 }.

Thor
fonte
1
Por favor, você pode elaborar gsube explicar resumidamente, como esse liner funciona? por favor.
mtk
Obrigado! Esse script funciona muito bem, mas você poderia explicar o 1 solitário no final do script? -} 1 '-
CocoaEv
@CocoaEv: Executa { print $0 }. Eu adicionei isso à explicação também.
Thor
2
essa abordagem tem um problema: às vezes o csv possui linhas que abrangem várias linhas, como: prefix,"something,otherthing[newline]something , else[newline]3rdline,and,things",suffix (ou seja: várias linhas e aninhado "," em qualquer lugar dentro de aspas duplas de várias linhas: a "...."parte inteira deve ser reconectada e a parte interna ,deve ser substituído / removido ...): seu script não verá pares de aspas duplas nesse caso, e não é realmente fácil de resolver (é necessário "juntar novamente" as linhas que estão em um "aberto" (ou seja, com números ímpares) aspas duplas ... + tomar cuidado extra se houver também um escapou \" dentro da string)
Olivier Dulac
1
Adorei essa solução, mas a aprimorei, já que muitas vezes gosto de manter as vírgulas, mas ainda quero delimitar. Em vez disso, eu troquei as vírgulas fora as cotações para tubos, convertendo a CSV para um arquivo psv:awk -F'"' -v OFS='"' '{ for (I=1; i<=NF; i+=2) gsub(",", "|", $i) } 1' infile
Danton Noriega
7

Existe uma boa resposta, usando sed simplesmente uma vez com um loop :

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

Explicação:

  • :a; é um rótulo para ramo mais furter
  • s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 / pode conter 3 partes fechadas
    • primeiro, o 2º: [^"]*,\?\|"[^",]*",\?corresponde a uma sequência que não contém aspas duplas, talvez seguida por um coma ou por uma delimitada por duas aspas duplas, sem coma e talvez seguida por um coma.
    • que a primeira parte do ER é composta por tantas repetições da parte 2 descrita anteriormente, seguidas por 1 aspas duplas e alguns caracteres, mas sem aspas duplas, nem vírgulas.
    • A primeira parte da ER a ser seguida por um coma.
    • Nota, o resto da linha não precisa ser tocado
  • tafará um loop para :ase o s/comando anterior fez alguma alteração.
F. Hauri
fonte
Funciona também com aspas aninhadas. Incrível, obrigado!
tricasse 27/05
5

Uma solução geral que também pode manipular várias vírgulas entre aspas balanceadas precisa de uma substituição aninhada. Eu implementei uma solução em perl, que processa todas as linhas de uma determinada entrada e substitui vírgulas em todos os outros pares de aspas:

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 em suma

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

Você pode canalizar o texto que deseja processar para o comando ou especificar o arquivo de texto a ser processado como o último argumento da linha de comandos.

user1146332
fonte
1
O [^\\]efeito indesejável de combinar o último caractere entre aspas e removê-lo (não \ caractere), ou seja, você não deve consumir esse caractere. Tente em (?<!\\)vez disso.
tojrobinson
Obrigado pela sua objeção, eu corrigi isso. No entanto, acho que não precisamos olhar por trás da afirmação aqui, ou precisamos !?
user1146332
1
Incluir o não \ no seu grupo de captura produz um resultado equivalente. +1
tojrobinson 20/09/12
1
+1. depois de tentar algumas coisas com o sed, verifiquei os documentos do sed e confirmei que ele não pode aplicar uma substituição apenas à parte correspondente de uma linha ... então desisti e tentei o perl. Terminou com uma abordagem muito semelhante, mas esta versão usa [^"]*para fazer a correspondência não-gananciosos (ou seja combina com tudo a partir de um "para o seguinte " ): perl -pe 's/"([^"]+)"/($match = $1) =~ (s:,::g);$match;/ge;'. Ele não reconhece a ideia bizarra que uma citação pode ser precedidos por uma barra invertida :-)
cas
Obrigado por seu comentário. Seria interessante se a [^"]*abordagem ou a abordagem explícita não gananciosa consumisse menos tempo de CPU.
user1146332
3

Eu usaria um idioma com um analisador CSV adequado. Por exemplo:

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
fonte
enquanto eu gostei esta solução, inicialmente, ele acabou por ser incrível lento para grandes arquivos ...
KIC
3

Suas segundas citações estão fora de lugar:

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

Além disso, o uso de expressões regulares tende a corresponder à parte mais longa possível do texto, o que significa que isso não funcionará se você tiver mais de um campo entre aspas na sequência.

Uma maneira de lidar com vários campos citados no sed

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

Essa também é uma maneira de resolver isso, no entanto, com entradas que podem conter mais de uma vírgula por campo citado, a primeira expressão no sed precisaria ser repetida tantas vezes quanto o conteúdo máximo de vírgula em um único campo ou até que não altera a saída.

A execução de sed com mais de uma expressão deve ser mais eficiente do que vários processos sed em execução e um "tr" em execução com tubos abertos.

No entanto, isso pode ter consequências indesejadas se a entrada não estiver formatada corretamente. ou seja, aspas aninhadas, aspas não terminadas.

Usando o exemplo em execução:

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'

Saída:

123,ABC  DEV 23,345,534,some more  comma-separated  words,202,NAME
Didi Kohen
fonte
Você pode torná-lo mais geral, com ramificação condicional e mais legível com ERE, por exemplo, com GNU sed: sed -r ':r; s/("[^",]+),([^",]*)/\1 \2/g; tr; s/"//g'.
25413 Thor
2

No perl - você pode usar Text::CSVpara analisar isso e fazê-lo trivialmente:

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

Você pode imprimir com Text::CSVmas tende a preservar aspas, se o fizer. (Embora, eu sugiro - em vez de retirar aspas para sua saída, você pode simplesmente analisar usando Text::CSVem primeiro lugar).

Sobrique
fonte
0

Eu criei uma função para percorrer todos os caracteres da string.
Se o caractere for uma cotação, a verificação (b_in_qt) será marcada como verdadeira.
Enquanto b_in_qt for verdadeiro, todas as vírgulas são substituídas por um espaço.
b_in_qt é definido como false quando a próxima vírgula é encontrada.

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
fonte