当前位置: 首页 > 知识库问答 >
问题:

使用awk高效解析CSV最可靠的方法是什么?

查学文
2023-03-14

这个问题的目的是提供一个规范的答案。

给定一个可能由Excel或其他工具生成的CSV,其中在字段中嵌入了换行符和/或双引号和/或逗号,以及空字段,例如:

$ cat file.csv
"rec1, fld1",,"rec1"",""fld3.1
"",
fld3.2","rec1
fld4"
"rec2, fld1.1

fld1.2","rec2 fld2.1""fld2.2""fld2.3","",rec2 fld4
"""""","""rec3,fld2""",

使用awk有效识别单独记录和字段的最健壮方法是什么:

Record 1:
    $1=<rec1, fld1>
    $2=<>
    $3=<rec1","fld3.1
",
fld3.2>
    $4=<rec1
fld4>
----
Record 2:
    $1=<rec2, fld1.1

fld1.2>
    $2=<rec2 fld2.1"fld2.2"fld2.3>
    $3=<>
    $4=<rec2 fld4>
----
Record 3:
    $1=<"">
    $2=<"rec3,fld2">
    $3=<>
----

因此,它可以在awk脚本的其余部分内部用作这些记录和字段。

有效的CSV应符合RFC 4180或可由MS Excel生成。

解决方案必须允许记录结尾仅为LF(\n),这是UNIX文件的典型情况,而不是该标准要求的CRLF(\r\n),以及Excel或其他Windows工具将生成的结果。它还允许未加引号的字段与加引号的字段混合。它特别不需要像其他一些CSV格式所允许的那样,允许用前面的反斜杠转义””,而不是”——如果你有这种格式,那么就添加一个gsub(/\\”/,“\\”)up-front会处理它,如果试图在一个脚本中自动处理两种转义机制,会使脚本变得不必要的脆弱和复杂。

共有3个答案

龙枫
2023-03-14

这正是csvquote的用途——它使awk和其他命令行数据处理工具变得简单。

有些事情很难用awk表达。与运行单个awk命令并尝试让awk使用嵌入的逗号和换行符处理带引号的字段不同,csvquote为awk准备数据,这样awk就可以始终将它找到的逗号和换行符解释为字段分隔符和记录分隔符。这使得管道中的awk部分更简单。一旦awk处理完数据,它将通过csvquote-u返回,以恢复引号字段中嵌入的逗号和换行符。

csvquote file.csv | awk -f my_awk_script | csvquote -u
薛经纶
2023-03-14

对@EdMorton的FPAT解决方案的改进,该解决方案应该能够处理通过加倍(”)转义的双引号(——CSV标准允许)。

gawk -v FPAT='[^,]*|("[^"]*")+' ...

这还是

>

假设GNU awk(gawk),标准的awk就不行了。

示例:

$ echo 'a,,"","y""ck","""x,y,z"," ",12' |
gawk -v OFS='|' -v FPAT='[^,]*|("[^"]*")+' '{$1=$1}1'
a||""|"y""ck"|"""x,y,z"|" "|12

$ echo 'a,,"","y""ck","""x,y,z"," ",12' |
gawk -v FPAT='[^,]*|("[^"]*")+' '{
  for(i=1; i<=NF;i++){
    if($i~/"/){ $i = substr($i, 2, length($i)-2); gsub(/""/,"\"", $i) }
    print "<"$i">"
  }
}'
<a>
<>
<>
<y"ck>
<"x,y,z>
< >
<12>
臧令
2023-03-14

如果您的CSV不能包含换行符,那么您只需要(使用GNU awk for FPAT):

$ echo 'foo,"field,""with"",commas",bar' |
    awk -v FPAT='[^,]*|("([^"]|"")*")' '{for (i=1; i<=NF;i++) print i " <" $i ">"}'
1 <foo>
2 <"field,""with"",commas">
3 <bar>

或使用任何awk的同等产品:

$ echo 'foo,"field,""with"",commas",bar' |
    awk -v fpat='[^,]*|("([^"]|"")*")' -v OFS=',' '{
        rec = $0
        $0 = ""
        i = 0
        while ( (rec!="") && match(rec,fpat) ) {
            $(++i) = substr(rec,RSTART,RLENGTH)
            rec = substr(rec,RSTART+RLENGTH+1)
        }
        for (i=1; i<=NF;i++) print i " <" $i ">"
    }'
1 <foo>
2 <"field,""with"",commas">
3 <bar>

看见https://www.gnu.org/software/gawk/manual/gawk.html#More-CSV获取我在上面使用的特定FPAT设置的信息。

如果您实际上想做的只是将CSV转换为单独的行,例如,在引用字段中用空格替换换行符,并用分号替换逗号,那么您所需要的就是这样,再次使用GNU awk进行多字符RS和RT:

$ awk -v RS='"([^"]|"")*"' -v ORS= '{gsub(/\n/," ",RT); gsub(/,/,";",RT); print $0 RT}' file.csv
"rec1; fld1",,"rec1"";""fld3.1 ""; fld3.2","rec1 fld4"
"rec2; fld1.1  fld1.2","rec2 fld2.1""fld2.2""fld2.3","",rec2 fld4
"""""","""rec3;fld2""",

然而,除此之外,通用的、健壮的、可移植的解决方案,用于识别将与任何现代awk*一起使用的字段,是:

$ cat decsv.awk
function buildRec(      fpat,fldNr,fldStr,done) {
    CurrRec = CurrRec $0
    if ( gsub(/"/,"&",CurrRec) % 2 ) {
        # The string built so far in CurrRec has an odd number
        # of "s and so is not yet a complete record.
        CurrRec = CurrRec RS
        done = 0
    }
    else {
        # If CurrRec ended with a null field we would exit the
        # loop below before handling it so ensure that cannot happen.
        # We use a regexp comparison using a bracket expression here
        # and in fpat so it will work even if FS is a regexp metachar
        # or a multi-char string like "\\\\" for \-separated fields.
        CurrRec = CurrRec ( CurrRec ~ ("[" FS "]$") ? "\"\"" : "" )
        $0 = ""
        fpat = "([^" FS "]*)|(\"([^\"]|\"\")+\")"
        while ( (CurrRec != "") && match(CurrRec,fpat) ) {
            fldStr = substr(CurrRec,RSTART,RLENGTH)
            # Convert <"foo"> to <foo> and <"foo""bar"> to <foo"bar>
            if ( gsub(/^"|"$/,"",fldStr) ) {
                gsub(/""/, "\"", fldStr)
            }
            $(++fldNr) = fldStr
            CurrRec = substr(CurrRec,RSTART+RLENGTH+1)
        }
        CurrRec = ""
        done = 1
    }
    return done
}

# If your input has \-separated fields, use FS="\\\\"; OFS="\\"
BEGIN { FS=OFS="," }
!buildRec() { next }
{
    printf "Record %d:\n", ++recNr
    for (i=1;i<=NF;i++) {
        # To replace newlines with blanks add gsub(/\n/," ",$i) here
        printf "    $%d=<%s>\n", i, $i
    }
    print "----"
}

.

$ awk -f decsv.awk file.csv
Record 1:
    $1=<rec1, fld1>
    $2=<>
    $3=<rec1","fld3.1
",
fld3.2>
    $4=<rec1
fld4>
----
Record 2:
    $1=<rec2, fld1.1

fld1.2>
    $2=<rec2 fld2.1"fld2.2"fld2.3>
    $3=<>
    $4=<rec2 fld4>
----
Record 3:
    $1=<"">
    $2=<"rec3,fld2">
    $3=<>
----

以上假设UNIX行结尾为\n。使用Windows\r\n行结束符更简单,因为每个字段中的“换行符”实际上只是换行符(即\ns),因此您可以设置RS=“\r\n”(使用GNU awk处理多字符),然后字段中的\ns将不被视为行结束符。

它的工作原理是,只要计算当前记录中到目前为止出现的个数,只要它遇到RS——如果是奇数,那么RS(可能是\n,但不一定是)是中间区域,所以我们继续构建当前记录,但如果是偶数,那么它就是当前记录的结尾,所以我们可以继续脚本的其余部分处理现在完整的记录。

*我在上面说“现代awk”,因为显然还有非常古老(即大约2000年)的tawk和mawk1版本,它们的gsub()实现中存在错误,因此gsub(/^"|"$/,"",fldStr)不会从fldStr中删除start/end"s。如果您正在使用其中一个,那么请获得一个新的awk,最好是gawk,因为它们也可能存在其他问题,但如果这不是一个选项,那么我希望您可以通过更改此选项来解决该特定错误:

        if ( gsub(/^"|"$/,"",fldStr) ) {

为此:

        if ( sub(/^"/,"",fldStr) && sub(/"$/,"",fldStr) ) {

感谢以下人员使用本答案的原始版本识别并提出所述问题的解决方案:

  1. @mosvy表示字段中的转义双引号

相关:另请参见如何在cygwin下使用awk打印excel电子表格中的字段?了解如何从Excel电子表格生成CSV。

 类似资料:
  • 问题内容: 我尝试寻找其他答案,但仍不确定正确的方法。我有许多个非常大的.csv文件(每个文件可以是一个千兆字节),我想首先获取它们的列标签,因为它们并不完全相同,然后根据用户的喜好使用某些条件提取其中的一些列。在开始提取部分之前,我做了一个简单的测试,以了解解析此文件的最快方法,这是我的代码: 我的结果是: 因此,似乎大多数人使用的csv库确实比其他人慢很多。也许以后证明当我开始从csv文件中提

  • 我必须解析一个csv文件,并将其内容转储到mysql表中。 第一输出 在第二个输出中,我需要自定义标头水平对齐。例如 对于第二个输出,它可以是我选择的任何一组标题。然后,我可以使用load data infile将这两个输出数据加载到mysql表中。正在寻找awk脚本来实现这一点。如果你还需要什么,请告诉我。德克萨斯州。

  • 问题内容: 给定一个有效的CSS颜色值的字符串: ffffff 白色 rgb(255,255,255) 需要获取以下格式的数字数组:[R,G,B] 用JavaScript(假设使用主要的浏览器)最有效的方法是什么? 问题答案: 显然,数值比名称更容易解析。所以我们先做那些。 那是一个 现在获取完整的六位数格式: 现在是格式: 另外,您还可以添加支持的格式,甚至/ 如果添加HSL2RGB转换功能。

  • 通过UDP发送大量的小数据包会占用更多的资源(cpu、zlib压缩等)。我在这里读到,通过UDP发送一个大的~65kBYTEs数据包可能会失败,所以我认为发送许多较小的数据包会更频繁地成功,但是随之而来的是使用更多处理能力的计算开销(或者至少这是我假设的)。问题基本上是这样的:发送最大成功数据包并将计算保持在最低限度的最佳方案是什么?有没有一个特定的尺寸在大部分时间都有效?我使用Erlang作为服

  • 问题内容: 假设您有一个存储有序树层次结构的平面表: 这是一个图,我们在这里。根节点0是虚构的。 您将使用哪种简约方法将其作为正确排序,正确缩进的树输出到HTML(就此而言,还是文本)? 进一步假设您只有基本的数据结构(数组和哈希图),没有带有父/子引用的奇特对象,没有ORM,没有框架,只有两只手。该表表示为结果集,可以随机访问。 可以使用伪代码或简单的英语,这纯粹是一个概念性问题。 额外的问题:

  • 问题内容: 我正在用Java替代传统应用程序。要求之一是必须将旧应用程序使用的ini文件原样读取到新的Java应用程序中。此ini文件的格式是常见的Windows样式,带有标头部分和键=值对,使用#作为注释字符。 我尝试使用Java中的Properties类,但是如果不同的标头之间存在名称冲突,那当然是行不通的。 因此,问题是,读取此INI文件和访问密钥的最简单方法是什么? 问题答案: 我正在用J