Usuń przecinek między cudzysłowami tylko w pliku rozdzielanym przecinkami

23

Mam plik wejściowy rozdzielany przecinkami ( ,). Niektóre pola są ujęte w podwójny cudzysłów i zawierają przecinek. Oto przykładowy wiersz

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

Muszę również usunąć wszystkie przecinki występujące wewnątrz podwójnych cudzysłowów i podwójnych cudzysłowów. Tak więc powyższa linia powinna zostać przeanalizowana, jak pokazano poniżej

123,ABC DEV 23,345,534.202,NAME

Próbowałem następujących przy użyciu, sedale nie dając oczekiwanych rezultatów.

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

Wszelkie szybkich sztuczki z sed, awklub jakiekolwiek inne narzędzie UNIX proszę?

Mt.
źródło
Nie jestem pewien, co próbujesz zrobić, ale narzędzie „csvtool” jest znacznie lepsze do analizowania plików csv niż ogólne narzędzia, takie jak sed lub awk. Jest w prawie każdej dystrybucji Linuksa.
figtrap

Odpowiedzi:

32

Jeśli cytaty są zrównoważone, będziesz chciał usunąć przecinki między każdym innym cytatem, można to wyrazić w awknastępujący sposób:

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

Wydajność:

123,ABC DEV 23,345,534.202,NAME

Wyjaśnienie

Te -F"marki awk oddzielić linię na oznak dwukrotnie środki, co oznacza, że każda inna dziedzina będzie tekst między cytat. Przebiegi for-loop gsub, skrót od globalnie zastępują, na każdym innym polu, zastępując przecinek ( ",") nic ( ""). 1Na koniec wywołuje blok kodu: { print $0 }.

Thor
źródło
1
Czy możesz rozwinąć gsubi wyjaśnić w skrócie, jak działa ten jeden liner? Proszę.
mtk
Dziękuję Ci! Te skrypty działają naprawdę dobrze, ale czy możesz wyjaśnić samotną 1 na końcu skryptu? -} 1 '-
CocoaEv
@CocoaEv: Wykonuje się { print $0 }. Dodałem to również do wyjaśnienia.
Thor
2
takie podejście ma problem: czasami csv ma ​​wiersze obejmujące kilka linii, na przykład: prefix,"something,otherthing[newline]something , else[newline]3rdline,and,things",suffix (tj.: kilka linii i zagnieżdżone „,” w dowolnym miejscu w podwójnym cudzysłowiu wielu linii: cała "...."część powinna być ponownie połączona, a wewnątrz ,powinna być zastąpiony / usunięty ...): twój skrypt nie zobaczy w tym przypadku par podwójnych cudzysłowów i nie jest to tak naprawdę łatwe do rozwiązania (trzeba „ponownie dołączyć” wiersze, które są w „otwartych” (tzn. nieparzystych) podwójny cytat ... + zachowaj szczególną ostrożność, jeśli \" wewnątrz łańcucha ucieknie również ucieczka )
Olivier Dulac
1
Podobało mi się to rozwiązanie, ale poprawiłem je, ponieważ często lubię przecinki, ale nadal chcę je rozgraniczać. Zamiast tego zmieniłem przecinki poza cudzysłowami na rury, konwertując csv do pliku psv:awk -F'"' -v OFS='"' '{ for (I=1; i<=NF; i+=2) gsub(",", "|", $i) } 1' infile
Danton Noriega
7

Jest dobra reakcja, używając sed po prostu raz z pętlą :

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

Wyjaśnienie:

  • :a; to etykieta dla branży rolniczej
  • s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 / może zawierać 3 zamknięte części
    • pierwszy drugi: [^"]*,\?\|"[^",]*",\?dopasuj ciąg bez podwójnego cudzysłowu, może po nim śpiączka lub ciąg zamknięty dwoma podwójnymi cudzysłowami, bez śpiączki i być może po śpiączce.
    • niż pierwsza część RE składa się z tylu powtórzeń poprzednio opisanej części 2, po której następuje 1 podwójny cytat i niektóre karactry, ale bez podwójnego cytatu ani śpiączki.
    • Pierwsza część RE, po której następuje śpiączka.
    • Uwaga, reszty linii nie trzeba dotykać
  • tazapętli się, :ajeśli poprzednie s/polecenie coś zmieniło.
F. Hauri
źródło
Działa również z zagnieżdżonymi cudzysłowami. Wielkie dzieki!
tricasse
5

Ogólne rozwiązanie, które może również obsługiwać kilka przecinków między zrównoważonymi cudzysłowami, wymaga zagnieżdżonego podstawienia. Implementuję rozwiązanie w perlu, które przetwarza każdy wiersz danego wejścia i zastępuje przecinki tylko w każdej innej parze cudzysłowów:

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'

lub w skrócie

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

Możesz potokować tekst, który chcesz przetworzyć, do polecenia lub określić plik tekstowy do przetworzenia jako argument ostatniego wiersza polecenia.

użytkownik1146332
źródło
1
[^\\]Będzie mieć niepożądany efekt dopasowania ostatni znak wewnątrz cudzysłowów i usunięcie go (non \ znaków), to znaczy, że nie należy spożywać ten znak. Spróbuj (?<!\\)zamiast tego.
tojrobinson
Dzięki za sprzeciw, poprawiłem to. Niemniej jednak uważam, że nie musimy tu szukać twierdzeń, czyż nie !?
user1146332
1
Włączenie non \ do grupy przechwytywania daje równoważny wynik. +1
tojrobinson
1
+1. po wypróbowaniu kilku rzeczy z sedem, sprawdziłem dokumenty seda i potwierdziłem, że nie można zastosować zamiany tylko na pasującą część linii ... więc poddałem się i spróbowałem perla. Skończyło się z bardzo podobne podejście, ale ta wersja używa [^"]*do sprawiają, że mecz nie chciwy (tj pasuje wszystko od jednego "do następnego " ) perl -pe 's/"([^"]+)"/($match = $1) =~ (s:,::g);$match;/ge;'. To nie potwierdza dziwacznego pomysłu, że cytat można uniknąć odwrotnego ukośnika :-)
cas
Dzięki za komentarz. Byłoby interesujące, gdyby albo [^"]*podejście, albo jawne niechciwe podejście, zużywa mniej czasu procesora.
user1146332,
3

Użyłbym języka z odpowiednim parserem CSV. Na przykład:

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
źródło
chociaż początkowo podobało mi się to rozwiązanie, okazało się, że jest niesamowicie wolne dla dużych plików ...
KIC
3

Twoje drugie cytaty są niewłaściwe:

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

Ponadto używanie wyrażeń regularnych zwykle pasuje do najdłuższej możliwej części tekstu, co oznacza, że ​​to nie zadziała, jeśli w łańcuchu będzie więcej niż jedno pole cytowane.

Sposób, który obsługuje wiele cytowanych pól w sed

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

Jest to również sposób na rozwiązanie tego problemu, jednak w przypadku danych wejściowych, które mogą zawierać więcej niż jeden przecinek na cytowane pole, pierwsze wyrażenie w sed musiałoby być powtarzane tyle razy, ile maksymalna zawartość przecinka w jednym polu, lub dopóki w ogóle nie zmienia wyjścia.

Uruchamianie sed z więcej niż jednym wyrażeniem powinno być bardziej wydajne niż kilka uruchomionych procesów sed i „tr” wszystkie z otwartymi potokami.

Może to jednak mieć niepożądane konsekwencje, jeśli dane wejściowe nie zostaną poprawnie sformatowane. tzn. cytaty zagnieżdżone, cytaty niezakończone.

Korzystając z działającego przykładu:

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'

Wydajność:

123,ABC  DEV 23,345,534,some more  comma-separated  words,202,NAME
Didi Kohen
źródło
Można uczynić go bardziej ogólnie z rozgałęzienia warunkowe i bardziej czytelne z ERE, np GNU sed: sed -r ':r; s/("[^",]+),([^",]*)/\1 \2/g; tr; s/"//g'.
Thor
2

W perlu - możesz go użyć Text::CSVdo parsowania tego i rób to trywialnie:

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

Możesz drukować za pomocą, Text::CSVale zazwyczaj zachowuje cytaty. (Chociaż sugerowałbym - zamiast usuwania cytatów z wyników, możesz po prostu parsować używając Text::CSVw pierwszej kolejności).

Sobrique
źródło
0

Stworzyłem funkcję umożliwiającą zapętlanie każdego znaku w ciągu.
Jeśli znak jest cytatem, wówczas czek (b_in_qt) jest oznaczony jako prawda.
Podczas gdy b_in_qt jest prawdziwe, wszystkie przecinki są zastępowane spacją.
b_in_qt jest ustawione na false, gdy zostanie znaleziony następny przecinek.

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"
użytkownik143598
źródło