Использование awk или perl для извлечения определенных столбцов из CSV (разбор) - PullRequest
7 голосов
/ 15 февраля 2012

Фон - Я хочу извлечь определенные столбцы из файла CSV. Файл csv разделен запятыми, использует двойные кавычки в качестве квалификатора текста (необязательно, но когда поле содержит специальные символы, квалификатор будет там - см. Пример) и использует обратную косую черту в качестве escape-символа. Некоторые поля также могут быть пустыми.


Пример ввода и желаемого вывода - Например, я хочу, чтобы только столбцы 1, 3 и 4 были в выходном файле. Окончательное извлечение столбцов из файла CSV должно соответствовать формату исходного файла. Не следует удалять escape-символы или добавлять дополнительные кавычки и т. Д.

Input

"John \"Super\" Doe",25,"123 ABC Street",123-456-7890,"M",A
"Jane, Mary","",132 CBS Street,333-111-5332,"F",B
"Smith \"Jr.\", Jane",35,,555-876-1233,"F",
"Lee, Jack",22,123 Sesame St,"","M",D

Желаемый выход

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,""

Предварительный сценарий (awk) - Ниже приведен предварительный сценарий, который я обнаружил, который работает по большей части, но не работает в одном конкретном случае, который я заметил, и, возможно, больше, что я не видел или думал еще

#!/usr/xpg4/bin/awk -f

BEGIN{  OFS = FS = ","  }

/"/{
    for(i=1;i<=NF;i++){
        if($i ~ /^"[^"]+$/){
            for(x=i+1;x<=NF;x++){
                $i=$i","$x
                if($i ~ /"+$/){
                    z = x - (i + 1) + 1
                    for(y=i+1;y<=NF;y++)
                        $y = $(y + z)
                    break
                }
            }
            NF = NF - z
            i=x
        }
    }
print $1,$3,$4
}

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


Вопрос / Комментарии - Я читал, что awk - не лучший вариант для анализа файлов csv, и рекомендуется perl. Тем не менее, я не знаю, Perl вообще. Я нашел несколько примеров сценариев Perl, но они не дают желаемого результата, который я ищу, и я не знаю, как легко редактировать сценарии для того, что я хочу.

Что касается awk, я знаком с ним и иногда использую его базовые функции, но я не знаю многих расширенных функций, таких как некоторые команды, использованные в приведенном выше сценарии. Возможно ли получить желаемый результат, просто используя awk? Если это так, можно ли будет отредактировать скрипт выше, чтобы исправить проблему, с которой я столкнулся? Может кто-нибудь построчно объяснить, что именно делает скрипт?

Любая помощь будет оценена, спасибо!

Ответы [ 7 ]

10 голосов
/ 15 февраля 2012

Я не собираюсь заново изобретать колесо .

use Text::CSV_XS;

my $csv = Text::CSV_XS->new({
   binary      => 1,
   escape_char => '\\',
   eol         => "\n",
});

my $fh_in  = \*STDIN;
my $fh_out = \*STDOUT;

while (my $row = $csv->getline($fh_in)) {
   $csv->print($fh_out, [ @{$row}[0,2,3] ])
      or die("".$csv->error_diag());
}

$csv->eof()
   or die("".$csv->error_diag());

Вывод:

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary","132 CBS Street",333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack","123 Sesame St",

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


Изобретая колесо:

my $field = qr/"(?:[^"\\]|\\.)*"|[^"\\,]*/s;
while (<>) {
   my @fields = /^($field),$field,($field),($field),/
      or die;
   print(join(',', @fields), "\n");
}

Вывод:

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,""
2 голосов
/ 15 февраля 2012

Я бы предложил Python csv модуль:

#!/usr/bin/env python3
import csv
rdr = csv.reader(open('input.csv'), escapechar='\\')
wtr = csv.writer(open('output.csv', 'w'), escapechar='\\', doublequote=False)
for row in rdr:
    wtr.writerow(row[0:1]+row[2:4])

output.csv

John \"Super\" Doe,123 ABC Street,123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,
0 голосов
/ 08 октября 2017

GNU awk решение.Просто используя колесо как колесо.Вы можете определить, как должны выглядеть поля, используя FPAT , например:

$ awk -vFPAT='[^,]+|"[^"]*"' -vOFS=, '{print $1, $3, $4}' file

, что приводит к:

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\",35,555-876-1233
"Lee, Jack",123 Sesame St,""

Объяснение регулярного выражения:

[^,]+           # 1 or more occurrences of anything that's not a comma, 
|               # OR
"[^"]*"         # 0 or more characters unequal to '"' enclosed by '"'

Прочтите о FPAT в руководстве gawk

Теперь, покажем вам ваш скрипт.По сути, он пытается переписать, как выглядят ваши поля.Сначала вы разделяете на ",", что, очевидно, вызывает некоторые проблемы.Затем он ищет поля, которые не закрываются должным образом символом "".

BEGIN{OFS=FS =","}                        # set field sep (FS) and output field 
                                          #   sep to ,
/"/{                                      # for each line matching '"'
    for(i=1;i<=NF;i++){                   # loop through fields 1 to NF
        if($i ~ /^"[^"]+$/){              # IF field $i start with '"', followed by
                                          #   non-quotes
            for(x=i+1;x<=NF;x++){         # loop through ALL following fields
                $i=$i","$x                # concatenate field $i with ALL following 
                                          #   fields, separated by ","
                if($i ~ /"+$/){           # IF field $i ends with '"'
                    z = x - (i + 1) + 1   # z is index of field we're looking at next
                    for(y=i+1;y<=NF;y++)  
                        $y = $(y + z)     # change contents of following fields to 
                                          #   contents of field, z steps further
                                          #   down the line
                    break                 # break out of for(x) loop
                }
            }
            NF = NF - z                   # reset number of fields
            i=x                           # continue loop for(i) at index x
        }
    }
 print $1,$3,$4
}

Сбой сценария в этой строке ввода:

"Smith \"Jr.\", Jane",35,,555-876-1233,"F",

просто из-за сбоя $i ~ /^"[^"]+$/$ 1.

Надеюсь, вы согласитесь со мной, что переписать поля, как это, может быть сложно. Более того, это как "О, мне нравится awk, но я собираюсь использовать его как C / perl /python. "Использование FPAT - более короткое решение, если не сказать больше.

0 голосов
/ 08 октября 2017

Я сделал несколько ошибок, надеюсь, сейчас исправлюсь.

awk '{sub(/y",""/,"y\42")sub(/,2.|,3./,"")sub(/,".",.*/,"")}1' file

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,""
0 голосов
/ 23 октября 2014

csvkit - это инструмент, который обрабатывает файлы csv и разрешает такие операции (среди других функций).

см. csvcut . Его интерфейс командной строки компактен, и он обрабатывает множество форматов CSV (TSV, другие разделители, кодировки, escape-символы и т. Д.)

То, что вы просили, можно сделать с помощью:

csvcut --columns 0,2,3 input.csv
0 голосов
/ 10 мая 2013

Перед публикацией я вижу, что это старый вопрос, столкнувшийся с уже удаленным ответом, однако я подумал, что все равно воспользуюсь возможностью, чтобы похвастаться Tie :: Array :: CSV , которыйсделать манипулирование CSV-файлами так же просто, как работать с массивами Perl.Полное раскрытие: я автор.

В любом случае, вот сценарий.Данные ОП требовали изменения escape-символа и массивов индексов Perl, начиная с 0, но в остальном это должно быть вполне читабельно.

#!/usr/bin/env perl

use strict;
use warnings;

use Tie::Array::CSV;

my $opts = { text_csv => { escape_char => '\\' } };

tie my @input,  'Tie::Array::CSV', 'data', $opts or die "Cannot open file 'data': $!";
tie my @output, 'Tie::Array::CSV', 'out',  $opts or die "Cannot open file 'out': $!";

for my $row (@input) {
  my @slice = @{ $row }[0,2,3];
  push @output, \@slice;
}

Тем не менее, я думаю, что последний цикл не теряет слишком много читабельностиесли я преобразую его в (ИМО) более впечатляющую форму:

push @output, [ @{$_}[0,2,3] ] for @input;
0 голосов
/ 15 февраля 2012

Следующая команда извлечет обязательные поля (например, первое, третье и четвертое), разделенные разделителем ',' из файла sample.csv, и отобразит вывод в консоли. cut -f1,3,4 -d ',' sample.txt Если вы хотите сохранить вывод в новом CSV-файле, перенаправьте вывод в файл, как показано ниже cut -f1,3,4 -d ',' sample.txt> newSample.csv

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...