Удалить ложные запятые

Клиент-идиот генерирует файлы csv, но в одном поле иногда добавляются дополнительные запятые (поле описания).

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

Пример:

A,B,C,This is a description,D,E
F,G,H,This is a description with a comma (,) in it,D,E

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

У нас нет роскоши говорить глупому клиенту об изменении своего кода.

Добавлен

Я бы не стал возражать против решения, которое просто удаляет ложную запятую one, которую я должен запускать несколько раз.

Ответ 1

Решение 1: однострочное, удаление ,

Здесь вы можете использовать однострочный переключатель SED:

sed -r 's/([^,],[^,],[^,],)(.*)(,.+,.+)/\1'"$(sed -r 's/([^,],[^,],[^,],)(.*)(,.+,.+)/\2/' <<< $myInput | sed 's/,//g')"'\3/' <<< $myInput

Вам нужно заменить <<< $myInput на любой ваш текущий вход.
Когда вы работаете с CSV, вам может потребоваться настроить (оба входа) регулярное выражение для соответствия каждой строке вашего листа CSV.
Если ваши первые три и последние два поля больше одного char заменить [^,] на [^,]*.

Объяснение:
Мы используем это регулярное выражение

/([^,],[^,],[^,],)(.*)(,.+,.+)/

который захватывает первую (F,G,H,), вторую (.*) и последнюю часть (,D,E) строки для нас.
Первая и третья группы захвата будут неизменными, а вторая будет заменена.
Для подстановки будем называть sed вторым (и фактически третьим) временем. Сначала мы фиксируем только вторую группу, второй заменяем каждый , ничем (только в группе захвата!).

Доказательство: enter image description here

Конечно, если нет нежелательной запятой, ничего не заменяется: enter image description here


Решение 2: весь файл, строка за строкой, удаление ,

Если вы хотите указать только файл, и замена должна произойти для каждой строки файла, которую вы можете использовать

while read line; do sed -r 's/([^,],[^,],[^,],)(.*)(,.+,.+)/\1'"$(sed -r 's/([^,],[^,],[^,],)(.*)(,.+,.+)/\2/' <<< $line | sed 's/,//g')"'\3/' <<< $line; done < input.txt

где input.txt в конце - очевидно - ваш файл.
Я просто использую команду SED сверху в while -loop, который читает каждую строку текста. Это необходимо, потому что вам нужно отслеживать строку, которую вы читаете, так как вы вызываете sed два раза на одном и том же входе.

enter image description here


Решение 3: весь файл, заключить поле в "

Как @Łukasz L. указал в комментариях к OP, согласно RFC1480, в котором описывается формат CSV файлов, было бы лучше заключить поля, содержащие запятую в ".
Это проще, чем другие решения:

sed -r 's/([^,],[^,],[^,],)(.*)(,.*,.*)/\1"\2"\3/' input.txt

Снова у нас есть три группы захвата. Это позволяет просто обернуть вторую группу в "!

enter image description here

Ответ 2

Если количество столбцов фиксировано, мы можем попытаться вырезать первые три и последние два столбца с помощью lookaheads ?: и сопоставить запятые внутри остальной части строки (это описание). У меня есть что-то вроде этого:

(?:^(?:[^,]*,){3})(?:(?:[^,]*(,))*[^,]*)(?:(?:,[^,]*){2}$)

[^,]* - это поле (без запятой), поэтому (?:^(?:[^,]*,){3}) сократит первые 3 столбца (включая следующую запятую). (?:(?:,[^,]*){2}$) удалит последние 2 столбца, включая запятую. (?:(?:[^,]*(,))*[^,]*) соответствует внутреннему.

В JavaScript все выражение возвращает полное описание (с запятыми) в качестве первого совпадения, а в нем запятые в качестве второго. Это дает возможность, в зависимости от двигателя Regex, либо espace, либо заменять описание (если движок дает диапазоны согласованного выражения) или нацеливать на выражение (,), соответствующее запятой с синтаксисом замены.

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

Ответ 3

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

cat your_file | ruby -ne '$_.scan(/^(\w+,\w+,\w+,)([^$]+)(,\w,\w)$/).each{|m|puts m[0]+m[1].gsub(",","")+m[2]}'

Это предполагает, что всегда есть 6 столбцов, а четвертый - это та, которая может содержать запятые.

Код был протестирован с ruby ​​1.8.7, 1.9.1 и 2.1.0.

Ответ 4

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

Далее script делает некоторые расчеты. Когда существуют дополнительные запятые, будут дополнительные столбцы, поэтому они получат место, где расположены дополнительные столбцы и присоединяются к ним.

#!/usr/bin/env perl 

use warnings;
use strict;
use Text::CSV_XS;

my (@columns);

open my $fh, '<', shift or die;
my ($total_columns, $weird_column) = (shift, shift);

my $csv = Text::CSV_XS->new or die;
while ( my $row = $csv->getline( $fh ) ) { 
    undef @columns;
    if ( @$row == $total_columns ) { 
        @columns = @$row;
        next;
    }   

    my $extra_columns = @$row - $total_columns;
    my $post_columns_index = $weird_column + $extra_columns;
    @columns = ( 
        @$row[0..($weird_column-2)], 
        join( '', @$row[($weird_column-1)..($post_columns_index-1)]),  
        @$row[$post_columns_index..$#$row] 
    );  
}
continue {
    $csv->print( \*STDOUT, \@columns );
    printf "\n";
}

Предполагая входной файл как:

A,B,C,This is a description,D,E
F,G,H,This is a description with a comma (,) in it,D,E
F,G,H,This is, a description with two commas (,) in it,D,E
F,G,H,This is, a description with, three commas (,) in it,D,E

Запустите его так:

perl script.pl infile 6 4

Это дает:

A,B,C,"This is a description",D,E
F,G,H,"This is a description with a comma () in it",D,E
F,G,H,"This is a description with two commas () in it",D,E
F,G,H,"This is a description with three commas () in it",D,E       

Возможно, это может завершиться ошибкой с крайними случаями (первое и последнее поле). Я не тестировал его подробно, но надеюсь, что вы поняли это. Я попытался сделать это как можно более общим.