2017-07-31 55 views
14

这个问题的目的是提供一个规范的答案。使用awk高效解析CSV的最健壮的方法是什么?

给出一个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或可由MS-Excel生成。

该解决方案必须容许记录的结尾为LF(\n),这是UNIX文件的典型值,而不是CRLF(\r\n),因为该标准需要Excel和其他Windows工具生成。它也会容忍未加引号的字段与引用字段混合在一起。它将特别不需要容忍使用前面的反斜杠(即\"而不是"")转义" s,因为其他一些CSV格式允许 - 如果您有这样的设置,那么先前添加一个gsub(/\\"/,"\"\"")将处理它并尝试自动处理两个转义机制一个脚本会使脚本不必要地变得脆弱和复杂。

回答

13

如果您的CSV不能包含换行符或转义双引号,那么所有你需要的是(与GNU AWK为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是:

$ 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")   # <"[email protected]""bar"> -> <"[email protected]@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) # <"[email protected]@Bbar"> -> <"[email protected]"bar"> 
      gsub(/^"|"$/,"",$i)     # <"[email protected]"bar"> -> <[email protected]"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行结尾更简单,因为每个字段中的“换行符”实际上只是换行符(即\n s),所以您可以设置RS="\r\n",然后在字段内设置\n不会被视为行结束符。

它通过简单地计算有多少" s为存在于当前纪录至今每当遇到RS - 如果它是一个奇数,则RS(大概\n但不必须)是中场所以我们继续构建当前记录,但是如果它甚至是当前记录的结尾,所以我们可以继续处理现在完整记录的脚本的其余部分。

gsub(/@/,"@A"); gsub(/""/,"@B")转换每对双引号axcross全程实录(记住这些""对只能援引领域内应用)将字符串@B不包含双引号,这样,当我们记录拆分成田match()不会被出现在字段内的引号绊倒。 gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)单独恢复每个字段内的引号,并将"" s转换为它们实际表示的"

相关问题