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