Извлекать текст из предсказуемых файлов TOML и выводить их в формате CSV - PullRequest
1 голос
/ 21 мая 2019

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

key1 = "someID"
key2 = "someVersionNumber"
key3 = "someTag"
key4 = "someOtherTag"
key5 = [] #empty array, sometimes contains strings
key6 = "long text"
key7 = "more text"
key8 = """
- text
- more text
- so much text
"""

Я хочу преобразовать его в CSV следующим образом:

"key1","key2","key3","key4","key5","key6","key7","key8"
"someID","someVersionNumber","someTag","someOtherTag","","long text","more text", "- text- more text- so much text"

Могу ли я сделать это снесколько строк команд bash?

А что, если я хочу объединить все строки CSV в одну, например,

"key1","key2","key3","key4","key5","key6","key7","key8"
"someID","someVersionNumber","someTag","someOtherTag","","long text","more text", "- text- more text- so much text"
"someID","someVersionNumber","someTag","someOtherTag","","long text","more text", "- text- more text- so much text"
"someID","someVersionNumber","someTag","someOtherTag","","long text","more text", "- text- more text- so much text"

... т. е. на выходе будет одна строка CSV на.toml файл плюс заголовок вверху (всегда один и тот же заголовок CSV и количество столбцов, поскольку файлы .toml предсказуемы).

Я смотрю на sed, awk или что-то еще проще?Я посмотрел на некоторые связанные вопросы, но чувствую, что что-то упустил, так как получаю слишком много функциональности:

Извлечение данных между двумя точками в текстовом файле

Разбор json с awk / sed в bash для получения пары ключ-значение

Ответы [ 2 ]

1 голос
/ 22 мая 2019

Если бы был только один входной файл, я бы использовал однострочник Perl.К сожалению, это получается довольно сложно:

perl -pe 'if(/"""/&&s/"""/"/.../"""/&&s/"""/"\n/){s/[\n\r]//;};if(/ = \[([^]]*)]/){$r=$1eq""?"\"\"":$1=~s/"\s*,\s*"/ /gr;s/ = \[([^]]*)]/ = $r/};s/"\s*#[^"\n]*$/"/' one.toml | perl -ne 'if(/^([^"]+) = "(.*)"/){push@k,$1;push@v,"\"$2\""}END{print((join",",@k),"\n",join",",@v)}'

Все становится только хуже, если нам нужно работать с несколькими (*) файлами одновременно:

perl -ne 'if(/"""/&&s/"""/"/.../"""/&&s/"""/"\n/){s/[\n\r]//;};if(/ = \[([^]]*)]/){$r=$1eq""?"\"\"":$1=~s/"\s*,\s*"/ /gr;s/ = \[([^]]*)]/ = $r/};s/"\s*#[^"\n]*$/"/;print;print"-\n"if eof' *.toml | perl -ne 'if(/^-$/){push@o,join",",@k if scalar@o==0;push@o,join",",@v;@k=@v=()};if(/^([^"]+) = "(.*)"/){push@k,$1;push@v,"\"$2\""}END{print join"\n",@o}'

Эти два фактора вызываютдля структурированного скрипта.Здесь это на Perl, но то же самое можно сделать на Python или любом другом языке, который вам удобен:

#!/usr/bin/env perl
use strict; use warnings; my @output;

foreach my $filename (@ARGV) {
    my $content, my @lines, my $replace, my @keys, my @values;
    open my $fh, "<:encoding(utf8)", $filename or die "Could not open $filename: $!";
    {local $/; $content = <$fh>;}
    $content =~ s/"""([^"]*)"""/'"' . $1=~s#[\r\n]##rg . '"'/ge;
    @lines = split (/[\r\n]/, $content);
    foreach my $line (@lines) {
        if ($line =~ m/ = \[([^]]*)]/) {
            $replace = $1 eq "" ? '""' : $1 =~ s/"\s*,\s*"/ /gr;
            $line =~ s/ = \[([^]]*)]/ = $replace/
        }
        $line =~ s/"\s*#[^"]*$/"/;
        $line =~ m/^([^"]+) = "(.*)"/;
        push @keys, $1;
        push @values, '"' . $2 . '"'
    }
    push @output, join ",", @keys if scalar @output == 0;
    push @output, join ",", @values
}
print join "\n", @output

Примечания:

Многое из сложностииз-за необходимости иметь дело с массивами (!), комментариями и многострочными строками.Некоторая предварительная обработка необходима для каждого, и именно это занимает большую часть длины решения.Кроме того, потребуется дополнительная информация о возможных угловых случаях и о том, как с ними бороться (например, как разместить массив строк в CSV).Все это только подчеркивает важность качества и согласованности входных данных.Предложенное решение ни в коем случае не является полным или надежным, поскольку оно делает несколько предположений о входных данных и желаемом формате вывода.Вот как я решил упомянутые проблемы:

  • значения должны быть только строками, так как они находятся в опубликованном файле примера.Скрипт не обрабатывает числа, даты и логические значения.
  • массивы могут быть либо пустыми [], либо массивами строк ["my", "array"].В отсутствие четкой спецификации OP они переводят в одну строку, которая является объединением всех строк элементов.В массиве не допускаются разрывы строк, и массив не может содержать другие массивы.
  • комментарии обрабатываются только в том случае, если они вставляются после строкового значения.Нет строк только для комментариев.
  • отступ , пустые строки и заголовки разделов не обрабатываются

Тестовый прогон:

$ perl toml-to-csv.pl *.toml
"someID1","someVersionNumber1","someTag1","someOtherTag1","","long text1","more text1","- text- more text- so much text"
"someID2","someVersionNumber2","someTag2","someOtherTag2","Array","long text2","more text2","- text- more text- so much text"
"someID3","someVersionNumber3","someTag3","someOtherTag3","My array","long text3","more text3","- text- more text- so much text"
0 голосов
/ 21 мая 2019
$ cat tst.awk
BEGIN { OFS="," }
{
    sub(/[[:space:]]*#[^"]*$/,"")
    key = val = $0
}

sub(/^[[:alnum:]]+[[:space:]]+=[[:space:]]+/,"",val) {
    sub(/[[:space:]]+.*/,"",key)
    keys[++numKeys] = key
    gsub(/^("""|\[])$|^"|"$/,"",val)
    vals[numKeys] = val
}

/^-[[:space:]]+/ {
    vals[numKeys] = vals[numKeys] val
}

/^"""$/ {
    if ( !doneHdr++ ) {
        for (keyNr=1; keyNr<=numKeys; keyNr++) {
            printf "\"%s\"%s", keys[keyNr], (keyNr<numKeys ? OFS : ORS)
        }
    }
    for (keyNr=1; keyNr<=numKeys; keyNr++) {
        printf "\"%s\"%s", vals[keyNr], (keyNr<numKeys ? OFS : ORS)
    }
}

.

$ awk -f tst.awk file
"key1","key2","key3","key4","key5","key6","key7","key8"
"someID","someVersionNumber","someTag","someOtherTag","","long text","more text","- text- more text- so much text"

Замените file списком ваших входных файлов.

Регулярное выражение, которое я использую в sub(/[[:space:]]*#[^"]*$/,""), чтобы удалить комментарии, начинающиеся с# означает, что в комментарии не может быть двойной кавычки.Я сделал это для защиты от изменения #, появляющегося в строках данных.Не стесняйтесь придумать лучшее регулярное выражение или другой подход для обработки ваших комментариев.

...