Eliminar comas entre comillas solo en un archivo delimitado por comas

23

Tengo un archivo de entrada delimitado con comas ( ,). Hay algunos campos encerrados entre comillas dobles que tienen una coma en ellos. Aquí está la fila de muestra

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

Necesito eliminar todas las comas que aparecen dentro de las comillas dobles y las comillas dobles también. Entonces, la línea anterior debe analizarse como se muestra a continuación

123,ABC DEV 23,345,534.202,NAME

Intenté lo siguiente usando sedpero sin dar los resultados esperados.

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

¿Algún truco rápido con sed, awko cualquier otra utilidad de Unix por favor?

mtk
fuente
No estoy seguro de lo que está intentando hacer, pero la utilidad "csvtool" es mucho mejor para analizar csv que las herramientas genéricas como sed o awk. Está en casi todas las distribuciones de Linux.
figtrap

Respuestas:

32

Si las comillas están equilibradas, querrás eliminar las comas entre las otras comillas, esto puede expresarse awkasí:

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

Salida:

123,ABC DEV 23,345,534.202,NAME

Explicación

Esto -F"hace que awk separe la línea en los signos de comillas dobles, lo que significa que cualquier otro campo será el texto entre comillas. La ejecución del ciclo for gsub, abreviatura de sustituto global, en cualquier otro campo, reemplazando coma ( ",") con nada ( ""). El 1al final invoca el código de bloque por defecto: { print $0 }.

Thor
fuente
1
Por favor, ¿puede gsubexplicar y explicar brevemente cómo funciona este revestimiento? Por favor.
mtk
¡Gracias! Este guión funciona muy bien, pero ¿podría explicar el solitario 1 al final del guión? -} 1 '-
CocoaEv
@CocoaEv: se ejecuta { print $0 }. Agregué eso a la explicación también.
Thor
2
este enfoque tiene un problema: a veces el csv tiene filas que abarcan varias líneas, tales como: prefix,"something,otherthing[newline]something , else[newline]3rdline,and,things",suffix (es decir: varias líneas y anidado "," en cualquier lugar dentro de una comilla doble de varias líneas: toda la "...."parte debe volver a unirse y el interior ,debe estar unido reemplazado / eliminado ...): su script no verá pares de comillas dobles en ese caso, y no es realmente fácil de resolver (necesita "volver a unir" las líneas que están en un "abierto" (es decir, números impares) comilla doble ... + tenga mucho cuidado si también hay un escape \" dentro de la cadena)
Olivier Dulac
1
Me encantó esta solución, pero la modifiqué dado que a menudo me gusta mantener las comas pero aún quiero delimitar. En cambio, cambié las comas fuera de las comillas a tuberías, convirtiendo el csv en un archivo psv:awk -F'"' -v OFS='"' '{ for (I=1; i<=NF; i+=2) gsub(",", "|", $i) } 1' infile
Danton Noriega
7

Hay una buena respuesta, usando sed simplemente una vez con un bucle :

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

Explicación:

  • :a; es una etiqueta para la rama adicional
  • s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 / podría contener 3 partes cerradas
    • primero el 2do: [^"]*,\?\|"[^",]*",\?coincide con una cadena que no contiene comillas dobles, tal vez seguida de un coma o una cadena encerrada por dos comillas dobles, sin coma y tal vez seguida de un coma.
    • que la primera parte RE está compuesta por tantas repeticiones de la parte 2 descrita anteriormente, seguida de 1 comilla doble y algunos caracteres, pero no comillas dobles ni comas.
    • La primera parte RE como seguida de un coma.
    • Nota, el resto de la línea no necesita ser tocado
  • tase repetirá :asi el s/comando anterior hizo algún cambio.
F. Hauri
fuente
Funciona también con comillas anidadas. ¡Genial gracias!
tricasse
5

Una solución general que también puede manejar varias comas entre comillas balanceadas necesita una sustitución anidada. Implemento una solución en perl, que procesa cada línea de una entrada dada y solo sustituye comas en cada otro par de comillas:

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 en resumen

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

Puede canalizar el texto que desea procesar al comando o especificar el archivo de texto que se procesará como último argumento de línea de comando.

usuario1146332
fuente
1
El [^\\]va a tener el efecto no deseado de hacer coincidir el último carácter dentro de las comillas y retirarlo (\ carácter no), es decir, no se debe consumir ese carácter. Intenta en su (?<!\\)lugar.
tojrobinson
Gracias por su objeción, lo he corregido. Sin embargo, creo que no necesitamos mirar detrás de la afirmación aquí, ¿o sí?
user1146332
1
Incluir el non \ en su grupo de captura produce un resultado equivalente. +1
tojrobinson
1
+1. Después de probar algunas cosas con sed, revisé los documentos de sed y confirmó que no puede aplicar un reemplazo solo a la parte correspondiente de una línea ... así que me di por vencido y probé con Perl. Terminamos con un enfoque muy similar, pero esta versión utiliza [^"]*para hacer que el partido no expansivo (es decir, coincide con todo, desde uno "a la siguiente " ): perl -pe 's/"([^"]+)"/($match = $1) =~ (s:,::g);$match;/ge;'. No reconoce la extravagante idea de que una cita podría escapar con una barra invertida :-)
cas
Gracias por tu comentario. Sería interesante si el [^"]*enfoque o el enfoque explícito no codicioso consume menos tiempo de CPU.
user1146332
3

Usaría un lenguaje con un analizador CSV adecuado. Por ejemplo:

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
fuente
aunque inicialmente me gustó esta solución, resultó ser increíblemente lenta para archivos grandes ...
KIC
3

Tus segundas citas están fuera de lugar:

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

Además, el uso de expresiones regulares tiende a coincidir con la parte más larga posible del texto, lo que significa que esto no funcionará si tiene más de un campo entre comillas en la cadena.

Una forma que maneja múltiples campos cotizados en sed

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

Esta es también una forma de resolver esto, sin embargo, con una entrada que puede contener más de una coma por campo citado, la primera expresión en el sed tendría que repetirse tantas veces como el contenido máximo de coma en un solo campo, o hasta que no cambia la salida en absoluto.

La ejecución de sed con más de una expresión debería ser más eficiente que la ejecución de varios procesos sed y un "tr" que se ejecuta con tuberías abiertas.

Sin embargo, esto puede tener consecuencias no deseadas si la entrada no está formateada correctamente. es decir, comillas anidadas, comillas sin terminar.

Usando el ejemplo en ejecución:

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'

Salida:

123,ABC  DEV 23,345,534,some more  comma-separated  words,202,NAME
Didi Kohen
fuente
Puede que sea más general, con ramificación condicional y más fácil de leer con ERE, por ejemplo, con sed de GNU: sed -r ':r; s/("[^",]+),([^",]*)/\1 \2/g; tr; s/"//g'.
Thor
2

En perl, puede usar Text::CSVpara analizar esto y hacerlo 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";
}

Puede imprimir con Text::CSVpero tiende a conservar las comillas si lo hace. (Aunque, sugeriría que, en lugar de eliminar las comillas para su salida, podría analizar usando Text::CSVen primer lugar).

Sobrique
fuente
0

Creé una función para recorrer todos los caracteres de la cadena.
Si el carácter es una cita, entonces el cheque (b_in_qt) se marca como verdadero.
Si bien b_in_qt es verdadero, todas las comas se reemplazan con un espacio.
b_in_qt se establece en falso cuando se encuentra la siguiente coma.

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"
usuario143598
fuente