【问题标题】:CVS file parser on UNIX command line with sed(1), can it be done?使用 sed(1) 的 UNIX 命令行上的 CVS 文件解析器,可以完成吗?
【发布时间】:2020-03-24 06:37:18
【问题描述】:

在 UNIX 命令行上,我们可以使用简单的字段分隔符(或字段分隔符)来执行简单的面向记录的文件工作。常见的分隔符是空格、制表符或竖线,但任何字符都可以作为分隔符。命令sortjoincut等都将字段分隔符作为选项-t或-d,并且shell(再次bourne或bourne)接受read -a命令的IFS环境变量将一行解析为一个数组或set -- 命令将一行解析为特殊的命令行参数变量$0, $1, ....

简单的字段分隔符方法很简单,唯一需要注意的是分隔符字符不会出现在数据本身中。理想情况下根本没有。这可以适用于特定的数据集,但不能普遍适用。这就是为什么在 UNIX shell 和 C 语言(以及从那里开始的 C++、Java)上,反斜杠转义序列有时用于将此类分隔符标记为数据的一部分(典型的 \_,例如,当您有一个带有空格的文件名时. 但是,记录和面向字段的命令(例如排序、剪切和连接)不支持任何方式。

现在,我们通常会下载“逗号分隔值”(CSV) 文件,这种格式显然源自 Windows 世界。其中逗号用作分隔符(通常是一个不好的选择,因为逗号很可能出现在实际数据值中),并且如果数据字段可能包含逗号(甚至空格)。然后在这样的引用值中,如果引用是值的一部分,则通过将其加倍 "" 将其“转义”。

现在我正在寻找将 CSV 文件转换为简单分隔文件的最简单方法。可以选择数据中未出现的任何分隔符。

难点在于 CSV 引用规则需要一个非常简单的有状态解析器。你要么在引用值之内,要么在引用值之外。如果在里面,你需要阅读重复的引用""作为引用。

我在这里找不到最佳答案,在一般的互联网搜索中我发现了一些东西,但它们不正确或使用了太多工具。

让我们把它变成一场比赛。在 bourne shell 或 bash 上仅使用 sed(可能还有 grep 和 tr)运行的最简单和优雅的单行程序赢得了公认的答案。如果结果更优雅并且不依赖于 AWK 的一个特殊版本,则允许使用 AWK。不允许使用 Perl,也不允许使用 C 程序。

我当然会尝试自己的答案。

更新: 那些甚至不用 sed 并直接转向 awk 的人显然具有优势。如果有人可以在 sed 中优雅地做到这一点,他们将是赢家。我自己在 sed 中的尝试并不优雅。

我发现 CSV 文件可能在带引号的字段中包含换行符。这是需要考虑的。由于我们正在尝试为 UNIX shell 处理创建简单的记录和字段格式,因此这些嵌入的换行符应转换为 \n。

PS:有人问:为什么是“单线”。它不一定是严格意义上的单行,重点在于您可以在命令行上创建它。为什么不是 Perl?因为大多数 UNIX 系统都带有 shell 和 sed 和 awk,但是需要安装 Perl(并且有所有这些不同的版本),对于 Python 来说相同或更糟。在我使用 Perl 或 Python 之前,我只会用 C 编写它。不,我们不想要任何语言,它应该在基本的 UNIX 设置上运行,而不需要安装一堆东西。

【问题讨论】:

  • CSV 是非正式的,即。不是标准化的格式。例如,引号可能因此被转义:"value \"quoted\""。另一个例子:一些解析器可能要求逗号之间为空值(value,,value),但其他解析器完全省略它们(value,value)。您的问题格式可能更适合 CodeGolf,他们经常参加此类比赛。
  • 这与 stackoverflow 上已经存在的许多其他“使用 sed 解析 csv”问题有何不同?如果 CSV 文件已经包含所有可能的字符,您建议如何选择分隔符?
  • @rath CSV 可能是非正式的,但许多公共数据发布都包含在 CSV 文件中。我从来没有在它们中看到反斜杠转义。,省略空字段显然被破坏了。我同意周围有许多损坏的 CSV 文件方法(因此是我的问题),但是有一种方法可以区分好坏,我们不必担心坏的损坏的 CSV 文件或解析器。如果你愿意的话,“黄金标准”是“你能在 Excel 中打开它吗”?
  • @jhnc 我以前在这里搜索过。如果您在 stackoverflow 上有一个“使用 sed 解析 csv”,欢迎您指出。我找不到任何东西。如果需要,请将此问题标记为重复。
  • 为了公平起见,将所有awk-ers 放在同一基线上:gnu.org/software/gawk/manual/html_node/…

标签: shell csv parsing unix sed


【解决方案1】:

替代解决方案,逐字符处理,保持状态(z-inside 引号字符串)。不用说,它假设输入遵循上述规则。

不确定这是否符合单线的条件。约 200 个字符。

#! /usr/bin/awk -f
BEGIN {
        Q="\""
        FS=","
        OFS="|"
}

{
        n=split($0,a,"")
        r=""
        for (i=1;i<=n;i++ ) {
                c=a[i]
                if (c==Q) if(a[i+1]==Q) i++ ; else { z=!z ; c="" } ; if (!z&&c==FS) { c=OFS }
                r = r c
        }
        print r
}

【讨论】:

  • 这是最好的,因为它似乎可以工作,即使额外发现 CSV 文件可以在引用的字段中包含换行符。意思是,在这种情况下它比我的效果更好。但是我们所有人都需要考虑这一点,并用换行符的一些转义序列替换多行值,为此我将使用\n
  • @GuntherSchadow 删除了新行报价统计的重置。这将允许多重留置权常数。代码适用于“foo\nbar\nzoo”,但不适用于“foo\bar,zoo”。
【解决方案2】:

我的(第一个?)方法是根据以下大纲:

  1. 确定最佳字段分隔符(定界符);
  2. 用数据中不存在的一些(序列)其他字符A替换(少数)出现的选定分隔符;
  3. 将引号内的任何嵌套换行符替换为\n
  4. 将重复的引号"" 替换为数据中任何位置都不存在的一些其他字符(序列)B
  5. 将嵌套在引用字段中的逗号替换为数据中不存在的一些其他字符(序列)C
  6. 删除引用字段周围的引号(即,删除所有剩余的引号,因为不应有任何剩余);
  7. 用选定的分隔符替换剩余的逗号;
  8. 用单双引号替换重复双引号的替换(序列)字符B
  9. 用逗号替换引号值内逗号的替换(序列)字符C

就是这样。步骤 2、3 和 4 是取决于确定不会出现在文件中任何位置的字符序列的步骤。那可能是~~^^$$ 或任何东西。所以这是通过一系列测试来确定的。例如:

fgrep '|' data.csv

发现只有少数命中,我现在替换 |使用$$,因为我确定$$ 根本不会发生:

fgrep '$$' data.csv

以同样的方式,我确定重复双引号"" 的替换,例如^^ 和嵌套在引号内的逗号我将替换为##

现在我有了我需要的数据。至此,上面的计划就差不多完成了:

sed <data.csv \
 -e 's/|/$$/g' \
 -e ???????????????? \
 -e 's/""/^^/g' \
 -e 's/???????/???????/g' \
 -e 's/"//g' \
 -e 's/,/|/g' \
 -e 's/^^/"/g' \
 -e 's/##/,/g'

您可以在每个 sed 命令的一行中看到编号为 2 到 9 的每个步骤。所以一切都很清楚。除了步骤 3 和 5 分别使用 ????????````, the hardest of them all, to replace line breaks and commas nested inside the quotes with the chosen replacement\nand$$```。

我该怎么做?我需要一个正则表达式(sed 实际上可以这样做),它将带引号的字符串中的逗号替换为其他内容,并且不会混淆引号。

如果我们想要做的只是完全删除我们可以说的带引号的字符串

 -e 's/,"[^"]*",/,REMOVED,/g' \

我会这样做:

 -e 's/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g'

这将替换它一次。我现在可以多次重复相同的 sed 命令步骤来捕获包含多个嵌套逗号的情况:

 -e 's/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g'
     -e 's/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g'
     -e 's/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g'
     -e 's/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g'
     ...

问题是我不知道我需要多久更换一次。但是我们可以使用 sed 的一个更高级的功能:定义一个标签,然后在进行替换时跳转回该标签:

:c
s/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g
tc

定义一个标签“a”,当替换完成后,跳转到标签。或者简而言之,一行:

:c;s/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g;tc

最后,在引号内用换行符分隔的行的连接是通过类似的技巧完成的:

-e ':n;$!N;s/,"\([^"]*\)\n/,"\1\\n/g;tn'

这里唯一的附加技巧是$!N 这是$ 最后一行,$! 除了最后一行,N 将下一行附加到模式空间,以便正则表达式可以搜索该行打破 \n 并将其替换为文字 \n

LANG=C sed <data.csv \
 -e 's/|/$$/g' \
 -e ':n;$!N;s/,"\([^"]*\)\n/,"\1\\n/g;tn' \
 -e 's/""/^^/g' \
 -e ':c;s/,"\([^,"]*\),\([^"]*\)"/,"\1##\2",/g;tc' \
 -e 's/"//g' \
 -e 's/,/|/g' \
 -e 's/\^\^/"/g' \
 -e 's/##/,/g'

因此,与我在此答案的第一版中所采用的方法相比,这现在是一种非常简洁的方法(请参阅以前的版本以了解它现在有多好)。

PS 可能仍然存在错误。特别是我目前不允许我引用的值作为第一个字段出现,现在开始引用 " 只能在逗号​​后识别。

【讨论】:

  • 为自己节省大量输入并将所有 sed 代码,即 s/,"\([^,"]*\),\([^"]*\)",/,"\1##\2",/g 一个接一个地放在一个纯文本文件中,然后像 sed -f myfixer.sed input &gt; output 一样调用它。 (不需要-e 或单引号或延续字符)。祝你好运。
  • 我注意到我不认为 CSV 文件可以在引号中包含换行符,这会导致我的算法失效。我还需要替换引号内的换行符。
【解决方案3】:

从原版 awk CSV 标记器开始:https://www.gnu.org/software/gawk/manual/html_node/Splitting-By-Content.html

小修改,将带引号的字符串中的双引号替换为单引号。

#! /usr/bin/awk -f
BEGIN {
    FPAT = "([^,]+)|(\"[^\"]+\")"
    OFS = "|"
    Q = "\""
}

{
    for (i = 1; i <= NF; i++) {
        v = $i
        if ( $i ~ Q ) v = gensub(Q Q, Q, "g", substr(v, 2, length(v)-2))
        printf "%s%s", v, (i<NF?OFS:ORS)
    }
}

我仍在努力将它压缩成一个班轮......这将是一条长线:-)。

???

【讨论】:

  • 当有一系列逗号表示空字段时,此选项似乎无法正常工作。
猜你喜欢
  • 1970-01-01
  • 2018-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
  • 2016-11-30
相关资源
最近更新 更多