如果您的 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>
如果您真正想要做的只是将您的 CSV 转换为单独的行,例如,在带引号的字段中用空格替换换行符,用分号替换逗号,那么您所需要的就是这个,再次使用 GNU awk 进行多字符 RS和RT:
$ awk -v RS='"([^"]|"")*"' -v ORS= '{gsub(/\n/," ",RT); gsub(/,/,";",RT); print $0 RT}' 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
"""""","""rec3;fld2""",
不过,识别适用于任何现代 awk* 的字段的通用、稳健、便携的解决方案是:
$ cat decsv.awk
function buildRec( fpat,fldNr,fldStr,done) {
CurrRec = CurrRec $0
if ( gsub(/"/,"&",CurrRec) % 2 ) {
# The string built so far in CurrRec has an odd number
# of "s and so is not yet a complete record.
CurrRec = CurrRec RS
done = 0
}
else {
# If CurrRec ended with a null field we would exit the
# loop below before handling it so ensure that cannot happen.
# We use a regexp comparison using a bracket expression here
# and in fpat so it will work even if FS is a regexp metachar
# or a multi-char string like "\\\\" for \-separated fields.
CurrRec = CurrRec ( CurrRec ~ ("[" FS "]$") ? "\"\"" : "" )
$0 = ""
fpat = "([^" FS "]*)|(\"([^\"]|\"\")+\")"
while ( (CurrRec != "") && match(CurrRec,fpat) ) {
fldStr = substr(CurrRec,RSTART,RLENGTH)
# Convert <"foo"> to <foo> and <"foo""bar"> to <foo"bar>
if ( gsub(/^"|"$/,"",fldStr) ) {
gsub(/""/, "\"", fldStr)
}
$(++fldNr) = fldStr
CurrRec = substr(CurrRec,RSTART+RLENGTH+1)
}
CurrRec = ""
done = 1
}
return done
}
# If your input has \-separated fields, use FS="\\\\"; OFS="\\"
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>
----
Record 3:
$1=<"">
$2=<"rec3,fld2">
$3=<>
----
以上假设 UNIX 行结尾为 \n。使用 Windows \r\n 换行符更简单,因为每个字段中的“换行符”实际上只是换行符(即\ns),因此您可以设置RS="\r\n"(使用 GNU awk 进行多字符 RS)和那么字段中的\ns 将不会被视为行尾。
只要遇到RS,它就通过简单地计算当前记录中存在多少"来工作 - 如果它是一个奇数,那么RS(大概是\n,但没有to be) 是中场,因此我们会继续构建当前记录,但如果是这样,那么它就是当前记录的结尾,因此我们可以继续脚本的其余部分处理现在完整的记录。
*我在上面说“现代 awk”是因为 tawk 和 mawk1 显然仍然非常旧(即大约 2000 年)版本在其gsub() 实现中存在错误,因此gsub(/^"|"$/,"",fldStr) 不会删除开始/结束@ 987654340@s 来自fldStr。如果您正在使用其中之一,那么获得一个新的 awk,最好是 gawk, 因为它们也可能存在其他问题,但如果这不是一个选项,那么我希望您可以通过以下方式解决该特定错误改变这个:
if ( gsub(/^"|"$/,"",fldStr) ) {
到这里:
if ( sub(/^"/,"",fldStr) && sub(/"$/,"",fldStr) ) {
感谢以下人员使用此答案的原始版本确定并建议解决所述问题:
-
@mosvy 用于字段中的转义双引号。
-
@datatraveller1 用于字段中的多对连续转义引号和记录末尾的空字段。
相关:另请参阅How do I use awk under cygwin to print fields from an excel spreadsheet?,了解如何从 Excel 电子表格生成 CSV。