使用awk高效parsingCSV的最健壮的方法是什么?

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

给定一个CSV可能由Excel或其他embedded换行符的工具生成,embedded的双引号和空字段,如:

$ 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来确定单独的logging和字段,最有效的方法是什么?

 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脚本的其他部分在内部使用这些logging和字段。

一个有效的CSV将是一个符合RFC 4180或可以由MS-Excel生成的。

该解决scheme必须容许logging的结尾只是LF( \n ),而不像UNIX标准所要求的和CRLF( \r\n )那样需要使用Excel或其他Windows工具才能生成的UNIX文件。 它也将容忍与引用字段混合的未加引号的字段。 它会特别的不需要容忍转义与前面的反斜杠(即\"而不是"" ),因为一些其他CSV格式允许 – 如果你有,然后添加一个gsub(/\\"/,"\"\"")前端将处理它,并试图在一个脚本中自动处理这两个转义机制将使脚本不必要的脆弱和复杂。

如果你的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一起使用的更一般,更强大,更便携的解决scheme是:

 $ 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 s在字段内不会被视为行结尾。

它的工作方式是简单地计算当前logging中到目前为止存在多less个logging – 如果它是一个奇数,那么RS (假定\n但不必是)是中场,所以我们继续构build当前logging,但是如果它甚至是当前logging的结尾,那么我们可以继续处理现在完整logging的脚本的其余部分。

gsub(/@/,"@A"); gsub(/""/,"@B") gsub(/@/,"@A"); gsub(/""/,"@B")将整个logging中的每一对双引号转换(注意这些""对只能在引用字段中应用)到不包含双引号的string@B当我们将logging拆分成字段时,match()不会被字段中出现的引号绊倒。 gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)恢复每个字段内的引号,并将"" s转换为"" s "真正代表。