这个问题的目的是提供一个规范的答案。
给定一个CSV,它可能是由Excel或其他工具生成的,包含嵌入的换行符、嵌入的双引号和空字段,如:

$ 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

使用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>
----

所以它可以在awk脚本的其余部分内部用作那些记录和字段。
有效的CSV应该是符合RFC 4180的CSV,或者可以由MS-Excel生成。
该解决方案必须允许记录结尾仅为LF(\n),这是UNIX文件的典型情况,而不是该标准要求的CRLF(\r\n)以及Excel或其他Windows工具将生成的结果。它还将允许未加引号的字段与加引号的字段混合。它特别不需要像某些其他CSV格式所允许的那样,允许用前面的反斜杠(即"而不是\")转义""s-如果有,那么在前面添加一个gsub(/\\"/,"\"\"")将处理它,并且尝试在一个脚本中自动处理这两种转义机制将使脚本变得不必要的脆弱和复杂。

最佳答案

如果CSV不能包含换行符或转义双引号,那么您所需要的就是(使用GNU awk forFPAT):

$ 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:
$ cat decsv.awk
function buildRec(      i,orig,fpat,done) {
    $0 = PrevSeg $0
    if ( gsub(/"/,"&") % 2 ) {
        PrevSeg = $0 RS
        done = 0
    }
    else {
        PrevSeg = ""
        gsub(/@/,"@A"); gsub(/""/,"@B")            # <"x@foo""bar"> -> <"x@Afoo@Bbar">
        orig = $0; $0 = ""                         # Save $0 and empty it
        fpat = "([^" FS "]*)|(\"[^\"]+\")"         # Mimic GNU awk FPAT meaning
        while ( (orig!="") && match(orig,fpat) ) { # Find the next string matching fpat
            $(++i) = substr(orig,RSTART,RLENGTH)   # Create a field in new $0
            gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)  # <"x@Afoo@Bbar"> -> <"x@foo"bar">
            gsub(/^"|"$/,"",$i)                    # <"x@foo"bar">   -> <x@foo"bar>
            orig = substr(orig,RSTART+RLENGTH+1)   # Move past fpat+sep in orig $0
        }
        done = 1
    }
    return done
}

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

上面假设UNIX行的结尾是\n。有了Windows\r\n行结束符,它就简单多了,因为每个字段中的“换行符”实际上只是换行符(即\ns),所以您可以设置RS="\r\n",然后字段中的\ns将不会被视为行结束符。
它的工作原理是,只要计算当前记录中目前存在的"s的数量,只要它遇到RS-如果它是奇数,那么RS(可能是\n,但不一定是)是中间字段,因此我们继续构建当前记录,但如果它是偶数,那么它是当前记录的结尾,因此我们可以继续处理现在完成的记录的脚本。
gsub(/@/,"@A"); gsub(/""/,"@B")将跨越整个记录的每对双引号ax转换为不包含双引号的字符串""(请记住,这些@B对只能应用于带引号的字段中),这样当我们将记录拆分为字段时,match()不会被字段中出现的引号绊倒。gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)分别恢复每个字段中的引号,并将""s转换为它们真正表示的"s。
另请参见How do I use awk under cygwin to print fields from an excel spreadsheet?了解如何从Excel电子表格生成csv。

09-11 05:14
查看更多