从可预测的 toml 文件中检索文本并输出为 CSV

Retrieve text from predictable toml files and output as CSV

我有一些可预测的 .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"

...即输出将是每个 .toml 文件的一行 CSV 加上顶部的 header(始终相同的 CSV header 和列数,因为 .toml 文件是可预测的) .

我是在看 sed、awk 还是更简单的东西?我已经查看了一些相关问题,但感觉我必须缺少一些东西,因为我获得了太多的功能:

Extract data between two points in a text file

Parsing json with awk/sed in bash to get key value pair

$ cat tst.awk
BEGIN { OFS="," }
{
    sub(/[[:space:]]*#[^"]*$/,"")
    key = val = [=10=]
}

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:]]*#[^"]*$/,"") 中用于删除以 # 开头的评论的正则表达式意味着您不能在评论中使用双引号。我这样做是为了防止更改 # 出现在数据字符串中。请随意找出更好的正则表达式或其他方法来处理您的评论。

如果只有一个输入文件,我会选择 Perl one-liner。不幸的是,结果相当复杂:

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

如果我们需要同时操作多个 (*) 文件,事情只会变得更糟:

perl -ne 'if(/"""/&&s/"""/"/.../"""/&&s/"""/"\n/){s/[\n\r]//;};if(/ = \[([^]]*)]/){$r=eq""?"\"\"":=~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,;push@v,"\"\""}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/"""([^"]*)"""/'"' . =~s#[\r\n]##rg . '"'/ge;
    @lines = split (/[\r\n]/, $content);
    foreach my $line (@lines) {
        if ($line =~ m/ = \[([^]]*)]/) {
            $replace =  eq "" ? '""' :  =~ s/"\s*,\s*"/ /gr;
            $line =~ s/ = \[([^]]*)]/ = $replace/
        }
        $line =~ s/"\s*#[^"]*$/"/;
        $line =~ m/^([^"]+) = "(.*)"/;
        push @keys, ;
        push @values, '"' .  . '"'
    }
    push @output, join ",", @keys if scalar @output == 0;
    push @output, join ",", @values
}
print join "\n", @output

备注:

大部分复杂性是由于必须处理数组 (!)、注释和多行字符串。每个都需要一些预处理,这就是解决方案长度的大部分内容。此外,还需要有关可能的极端情况以及如何处理它们的其他信息(例如,如何在 CSV 中拟合字符串数组)。所有这些都强调了输入数据质量和一致性的重要性。所提出的解决方案绝不是完整或稳健的,因为它确实对输入数据和所需的输出格式做出了一些假设。以下是我解决上述问题的方法:

  • values 应该只是字符串,因为它们在发布的示例文件中。该脚本不处理数字、日期和布尔值。
  • arrays 可以是空 [] 或字符串数​​组 ["my", "array"]。在 OP 没有明确规范的情况下,它们转换为单个字符串,该字符串是所有元素字符串的串联。一个数组内不允许换行,一个数组也不能包含其他数组。
  • 评论 只有当它们在字符串值之后内联时才会被处理。没有 comment-only 行。
  • 缩进空行部分headers未处理

测试运行:

$ 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"

“备案”(因为这个问题现在已经快三年了)和未来的读者:通过使用 command-line 工具 tomlq,它是 [=29= 的一部分],这个任务就变得简单

tomlq -r 'map(arrays |= join(" ") | gsub("\n"; "")) | @csv' *.toml

tomlq 基本上将其输入转换为 JSON,然后对其应用 jq 过滤器。上面使用的一个作用如下:

  • map(…) 将其子过滤器应用于其输入结构的每个元素(在本例中为每个“键”)
  • arrays |= join(" ") 通过使用 space 作为胶水连接它们的元素来替换数组(点击“key5”)
  • gsub("\n"; "") 将任何换行符替换为空(有效地删除它们)
  • @csv 将结果转换为 CSV-formatted 行(使用引号,必要时转义)

此解决方案遵循其他答案的解释,即通过连接由 space 字符(定义为 join 函数的参数)分隔的元素将数组转换为字符串,以及在数组和字符串中无替换地消除换行符(定义为 gsub 函数的第二个参数),因为此函数在从数组转换为字符串后应用。

就是说,此过滤器要求值是字符串或数组。但是,由于 @csv 内置函数也可以正确处理数字和布尔值,因此可以通过在 strings |= (就像使用 join 将数组转换为字符串仅限于使用 arrays |= 的数组一样)。只有日期的处理会更难实现,因为在 JSON 和 CSV 中没有直接表示它们(必须事先将它们转换为字符串)。

这些值按照它们在源文件中出现的顺序进行处理,而不考虑实际的键名。因此,两个文件之间的任何不同顺序都将按原样传递,并且不会与键顺序匹配,但是,也可以通过在实现中明确引用键名来轻松处理(例如 [.key1, .key7, .key4 ] | …).

从键名生成 CSV header 行也可以通过在整个过滤器前添加 keys_unsorted, 轻松完成。其中的逗号将使它成为最后应用 @csv 的另一条记录。然而,由于总体目的是转换多个 .toml 文件的内容,但据推测只生成一个 CSV header,最简单的方法是将其外包到自己的过滤器中,然后将其应用于只有一个输入文件(不过,也可以使用一个过滤器的另一种方式)。

tomlq -r 'keys_unsorted | @csv' first.toml