这个问题的目的是提供一个规范的答案.
给定可能由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
Run Code Online (Sandbox Code Playgroud)
使用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>
----
Run Code Online (Sandbox Code Playgroud)
所以它可以在awk脚本的其余部分内部用作那些记录和字段.
有效的CSV将符合RFC 4180或可由MS-Excel生成.
解决方案必须允许记录结束只是LF(\n
),这是UNIX文件的典型情况,而不是CRLF(\r\n
),正如标准所要求的那样,Excel或其他Windows工具会生成.它还可以容忍与引用字段混合的未加引号的字段.它特别不需要容忍"
使用前面的反斜杠转义s(即\"
代替""
),因为其他一些CSV格式允许 - 如果你有,那么添加一个gsub(/\\"/,"\"\"")
前端将处理它并尝试在一个脚本中自动处理两个转义机制将使脚本不必要地脆弱和复杂.
Ed *_*ton 32
如果您的CSV不能包含换行符或转义双引号,那么您只需要(使用GNU awk \n
):
$ 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>
Run Code Online (Sandbox Code Playgroud)
否则,可以与任何现代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 "----"
}
Run Code Online (Sandbox Code Playgroud)
.
$ 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>
----
Run Code Online (Sandbox Code Playgroud)
以上假定UNIX行结尾\r\n
.对于Windows \n
行结尾,它更简单,因为每个字段中的"换行符"实际上只是换行符(即RS="\r\n"
s),因此您可以设置\n
然后"
字段内的s不会被视为行结尾.
它通过简单地计算RS
当前记录中当前记录中存在的s 数量来工作RS
- 如果它是奇数,则\n
(可能gsub(/@/,"@A"); gsub(/""/,"@B")
但不一定是)是中场,因此我们继续构建当前记录但如果它甚至那么它就是当前记录的结束,所以我们可以继续处理现在完整记录的脚本的其余部分.
该""
转换每对双引号axcross全程实录(记住这些@B
对只能援引领域内应用)为一个字符串gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)
不包含一个双引号,这样,当我们分裂成几个域匹配()不被字段内出现的引号绊倒了.在""
恢复单独的每个领域内的报价,也是转换"
S到\n
他们真正代表秒.
对@EdMortonFPAT
解决方案的改进,它应该能够处理"
通过加倍转义的双引号()(""
- CSV标准允许)。
gawk -v FPAT='[^,]*|("[^"]*")+' ...
Run Code Online (Sandbox Code Playgroud)
这仍然
无法处理引用字段内的换行符,这在标准 CSV 文件中是完全合法的。
假设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>
Run Code Online (Sandbox Code Playgroud)