Удалить запятую между кавычками только в файле с разделителями-запятыми


23

У меня есть входной файл, разделенный запятыми ( ,). Есть некоторые поля, заключенные в двойные кавычки, в которых есть запятая. Вот пример строки

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

Мне нужно удалить все запятые внутри двойных кавычек, а также двойные кавычки. Таким образом, приведенная выше строка должна быть проанализирована, как показано ниже

123,ABC DEV 23,345,534.202,NAME

Я попробовал следующее, используя, sedно не давая ожидаемых результатов.

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

Любые быстрые трюки sed, awkили любая другая утилита Unix, пожалуйста?


Я не уверен, что вы пытаетесь сделать, но утилита "csvtool" гораздо лучше разбирает CSV, чем универсальные инструменты, такие как sed или awk. Это почти в каждом дистрибутиве Linux.
figtrap

Ответы:


32

Если кавычки сбалансированы, вы захотите удалить запятые между каждой другой кавычкой, это можно выразить awkтак:

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

Выход:

123,ABC DEV 23,345,534.202,NAME

объяснение

Команда -F"make awk отделяет строку от двойных кавычек, что означает, что любое другое поле будет текстом, заключенным в кавычки. Цикл for запускается gsub, за исключением глобально заменителя, в любом другом поле, заменяя запятую ( ",") ничем ( ""). 1В конце вызывает кодовый блок по умолчанию: { print $0 }.


1
Пожалуйста, не могли бы вы уточнить gsubи объяснить вкратце, как работает этот лайнер? Пожалуйста.
MTK

Спасибо! Этот сценарий работает очень хорошо, но не могли бы вы объяснить одинокий 1 в конце сценария? -} 1 '-
CocoaEv

@CocoaEv: это выполняется { print $0 }. Я добавил это к объяснению также.
Тор

2
у этого подхода есть проблема: иногда в csv есть строки, которые занимают несколько строк, например: prefix,"something,otherthing[newline]something , else[newline]3rdline,and,things",suffix (то есть: несколько строк и вложенные символы "," в любом месте в многострочном двойном кавычке: вся "...."часть должна быть объединена, а внутренняя часть ,должна быть заменен / удален ...): ваш сценарий не будет видеть пары двойных кавычек в этом случае, и это не очень легко решить (нужно «воссоединиться» со строками, которые находятся в «открытом» (то есть нечетном) двойная кавычка ... + будьте особенно осторожны, если \" внутри строки также есть экранированный символ)
Оливье Дюлак

1
Мне понравилось это решение, но я его подправил, потому что мне часто нравятся запятые, но я все еще хочу разделять их. Вместо этого я переключил запятые вне кавычек на каналы, преобразовав csv в файл psv:awk -F'"' -v OFS='"' '{ for (I=1; i<=NF; i+=2) gsub(",", "|", $i) } 1' infile
Danton Noriega

7

Есть хороший ответ, используя sed просто один раз с циклом :

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

Объяснение:

  • :a; это этикетка для дальней ветви
  • s/^\(\([^"]*,\?\|"[^",]*",\?\)*"[^",]*\),/\1 / может содержать 3 закрытые части
    • первый 2-й: [^"]*,\?\|"[^",]*",\?соответствует строке, не содержащей двойных кавычек, может сопровождаться запятой или строкой, заключенной в две двойных кавычки, без запятой и может сопровождаться запятой.
    • чем первая часть RE состоит из множества повторений ранее описанной части 2, за которыми следуют 1 двойная кавычка и несколько символов, но ни двойной кавычки, ни запятых.
    • Первая часть RE, за которой следует кома.
    • Nota, остальная часть линии не должна быть затронута
  • taзацикливается, :aесли предыдущая s/команда внесла некоторые изменения.

Работает также с вложенными кавычками. Круто, спасибо!
Трикасс

5

Общее решение, которое также может обрабатывать несколько запятых между сбалансированными кавычками, требует вложенной замены. Я реализую решение в Perl, которое обрабатывает каждую строку данного ввода и заменяет запятые только в каждой другой паре кавычек:

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'

или короче

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

Вы можете либо передать текст, который вы хотите обработать, в команду, либо указать текстовый файл для обработки в качестве последнего аргумента командной строки.


1
Это [^\\]приведет к нежелательному эффекту совпадения последнего символа внутри кавычек и удаления его (не \ символ), т. Е. Вы не должны использовать этот символ. Попробуй (?<!\\)вместо этого.
Тойробинсон

Спасибо за ваше возражение, я исправил это. Тем не менее, я думаю, что нам не нужно смотреть за утверждением здесь, или мы !?
user1146332

1
Включение non \ в вашу группу захвата дает эквивалентный результат. +1
тойробинсон

1
+1. после нескольких попыток с sed я проверил документы sed и подтвердил, что он не может применить замену только к соответствующей части строки ... так что сдался и попробовал perl. Законченный с очень похожим подходом , но данная версия используется , [^"]*чтобы сделать матч не жадным (т.е. соответствует всем от одного "до следующего " ): perl -pe 's/"([^"]+)"/($match = $1) =~ (s:,::g);$match;/ge;'. Он не признает диковинную идею , что цитата может быть экранированы с обратной косой черты :-)
саз

Спасибо за ваш комментарий. Было бы интересно, если бы этот [^"]*подход или явный не жадный подход потреблял меньше времени процессора.
user1146332

3

Я бы использовал язык с правильным парсером CSV. Например:

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

хотя изначально мне нравилось это решение, оно оказалось невероятно медленным для больших файлов ...
KIC

3

Ваши вторые цитаты неуместны:

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

Кроме того, использование регулярных выражений, как правило, соответствует самой длинной части текста, что означает, что это не сработает, если в строке более одного поля в кавычках.

Способ, который обрабатывает несколько полей в кавычках в sed

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

Это также способ решить эту проблему, однако, с помощью ввода, которое может содержать более одной запятой на поле в кавычках, первое выражение в sed должно будет повторяться столько раз, сколько максимальное запятое в одном поле, или до не меняет выход вообще.

Выполнение sed с более чем одним выражением должно быть более эффективным, чем выполнение нескольких процессов sed, и "tr", выполняемый с открытыми каналами.

Однако это может иметь нежелательные последствия, если ввод не отформатирован должным образом. то есть вложенные кавычки, неопределенные кавычки.

Используя работающий пример:

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'

Выход:

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

Вы можете сделать это более общее с условным ветвлением и более удобным для чтения с ERE, например , с GNU СЭД: sed -r ':r; s/("[^",]+),([^",]*)/\1 \2/g; tr; s/"//g'.
Тор

2

В Perl - вы можете использовать это Text::CSVдля анализа, и сделать это тривиально:

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

Вы можете печатать с помощью, Text::CSVно он сохраняет кавычки, если вы делаете. (Хотя, я бы предложил - вместо зачистки кавычки для вывода, вы можете просто разобрать , используя Text::CSVв первую очередь).


0

Я создал функцию для циклического прохождения каждого символа в строке.
Если символ является кавычкой, то проверка (b_in_qt) помечается как истинная.
Хотя b_in_qt имеет значение true, все запятые заменяются пробелом.
b_in_qt устанавливается в false при обнаружении следующей запятой.

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"
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.