使用awk有效解析CSV的最有效方法是什么?

时间:2017-07-31 16:02:39

标签: csv awk

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

鉴于可能由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

使用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或可由MS-Excel生成。

解决方案必须容忍记录结束只是LF(\n),这是UNIX文件的典型情况,而不是CRLF(\r\n),因为该标准需要并且Excel或其他Windows工具会生成。它还可以容忍与引用字段混合的未加引号的字段。它特别不需要容忍使用前面的反斜杠(即"而不是\")转义"",因为其他一些CSV格式允许 - 如果你有,那么添加{{1在前面会处理它并试图在一个脚本中自动处理两个转义机制会使脚本不必要地变得脆弱和复杂。

2 个答案:

答案 0 :(得分:23)

如果您的CSV无法包含换行符或转义双引号,那么您需要的只是(FPAT的GNU awk):

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

以上假设\n的UNIX行结尾。对于Windows \r\n行结尾,它更简单,因为每个字段中的“换行符”实际上只是换行符(即\n s),因此您可以设置RS="\r\n"然后\n字段内的1}}不会被视为行结尾。

通过简单计算当前记录中遇到"时到目前为止有多少RS来工作 - 如果它是奇数,那么RS(大概是{{ 1}}但不一定是)是mid-field所以我们继续构建当前记录但是如果它甚至那么它就是当前记录的结束,所以我们可以继续处理现在完成的脚本的其余部分记录。

\n将每对双引号转换为整个记录(请记住这些gsub(/@/,"@A"); gsub(/""/,"@B")对只能在引用字段中应用)到不包含a的字符串""双引号,这样当我们将记录分成字段时,match()不会被字段内出现的引号绊倒。 @B会单独恢复每个字段中的引号,并将gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)转换为它们真正代表的""

另请参阅How do I use awk under cygwin to print fields from an excel spreadsheet?了解如何从Excel电子表格生成CSV。

答案 1 :(得分:7)

@EdMorton的FPAT解决方案的改进,该解决方案应该能够处理通过加倍("来转义的双引号("")-CSV {{3 }}。

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

此静态

  1. 无法处理带引号的字段中的换行符,而这些换行符在标准CSV文件中是完全合法的。

  2. 假定 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>