如何使awk忽略双引号内的字段分隔符?

44

我需要在逗号分隔值文件中删除2列内容。考虑以下csv文件中的一行:

"abc@xyz.com,www.example.com",field2,field3,field4
"def@xyz.com",field2,field3,field4

现在,我希望最终得到的结果是:

"abc@xyz.com,www.example.com",field4
"def@xyz.com",field4

我使用了以下命令:

awk 'BEGIN{FS=OFS=","}{print $1,$4}'

但是引号内部的逗号会产生问题,以下是我得到的结果:

"abc@xyz.com,field3
"def@xyz.com",field4
现在我的问题是如何让 awk 忽略双引号内的逗号?
4个回答

66

1
我很好奇内部会发生什么?这是在Perl中使用正则表达式产生非常不同匹配的用法:perl -lnE 'while(/([^,]*)|("[^"]+")/g){say "#$1#"}' <<<'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' - Johannes Riecken
2
@rubystallion,你需要向gawk的开发人员询问内部发生了什么,但是正则表达式通常匹配最左边最长的字符串,因此perl在"1234 A Pretty Street和NE"上作为2个单独的字符串进行匹配似乎是错误的,因为上面的awk和grep -Eo '([^,]*)|("[^"]+")' <<<'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA'都将"1234 A Pretty Street, NE"识别为一个单一的字符串。当然,那个perl脚本可能包含一些神奇的咒语,意味着“不要匹配最左边最长的”,我不知道,因为我不懂perl语法。 - Ed Morton
1
感谢提供grep示例,它指引我找到了答案:POSIX规范指出:“如果模式允许匹配多个字符,并且在该点上有多个这样的序列,则匹配最长的序列。例如,BRE“bb*”匹配字符串“abbbc”的第二到第四个字符,ERE“(wee|week)(knights|night)”匹配字符串“weeknights”的所有十个字符。” - Johannes Riecken
1
虽然 perl 规范 表示“备选项从左到右尝试,因此选择整个表达式匹配的第一个备选项。这意味着备选项不一定是贪婪的。例如:当将 "foo|foot" 与 "barefoot" 匹配时,只有 "foo" 部分会匹配,因为它是尝试的第一个备选项,并且成功地匹配了目标字符串。” - Johannes Riecken
2
@RalphCallaway 对的,FPAT是GNU awk的扩展。安装gawk或查看我的答案中的链接,以获取适用于任何awk的解决方案。 - Ed Morton
显示剩余5条评论

17
我建议使用CSVKit,它是一个可以通过pip install csvkit安装的命令行工具集合,专门用于处理CSV文件。其中包括csvcut,它可以满足你所需的功能。这不是一个bash/awk解决方案。请注意保留HTML标签。
csvcut --columns=1,4 <<EOF
"abc@xyz.com,www.example.com",field2,field3,field4
"def@xyz.com",field2,field3,field4
EOF

输出:

"abc@xyz.com,www.example.com",field4
def@xyz.com,field4

它会剥离掉不必要的引号,我想这应该不是问题。
阅读CSVKit的文档在RTD上这里。ThoughtBot有一篇很好的博客文章介绍了这个工具,这也是我了解CSVKit的地方。

2
CSVKit 真是太棒了!感谢你向我介绍它 :) - John Oxley
在 MacOS 上,我成功地使用了 brew 进行安装,而不是 pip。 - wytten

4
在您的样本输入文件中,只有第一列被引用,并且仅引用第一列。如果这通常是正确的,请考虑以下方法来删除第二列和第三列:
$ awk -F, '{for (i=1;i<=NF;i++){printf "%s%s",(i>1)?",":"",$i; if ($i ~ /"$/)i=i+2};print""}' file
"abc@xyz.com,www.example.com",field4
"def@xyz.com",field4

如评论中所述,awk 不能原生地理解带引号的分隔符。该解决方案通过查找以引号结尾的第一个字段来解决这个问题。然后跳过接下来的两个字段。

详细信息

  • for (i=1;i<=NF;i++)

    这开始了一个针对每个字段for循环。

  • printf "%s%s",(i>1)?",":"",$i

    这会打印字段。如果它不是第一个字段,则在字段之前加上逗号。

  • if ($i ~ /"$/)i=i+2

    如果当前字段以双引号结尾,则将字段计数器增加2。这是我们跳过第2和第3个字段的方法。

  • print""

    完成for循环后,这会打印一个换行符。


2
这个 awk 应该能够处理带引号的字段,并且也适用于转义引号,无论这些字段出现在哪里。
awk '{while(match($0,/"[^"]+",|([^,]+(,|$))/,a)){
      $0=substr($0,RSTART+RLENGTH);b[++x]=a[0]}
      print b[1] b[4];x=0}' file

Input

"abc@xyz.com,www.example.com",field2,field3,field4  
"def@xyz.com",field2,field3,field4  
field1,"abc@xyz.com,www.example.com",field3,field4  

输出

"abc@xyz.com,www.example.com",field4
"def@xyz.com",field4
field1,field4

它甚至可以在

field1,"field,2","but this field has ""escaped"\" quotes",field4

强大的FPAT变量在某些情况下会失效!


解释

 while(match($0,/"[^"]+",|([^,]+(,|$))/,a))

开始一个while循环,只要匹配成功(即存在一个字段),就会继续。
该匹配将匹配正则表达式的第一个出现,并恰好匹配字段并将其存储在数组a中。

 $0=substr($0,RSTART+RLENGTH);b[++x]=a[0]

$0设置为从匹配字段的结尾开始,并将匹配字段添加到b对应数组位置。
  print b[1] b[4];x=0}

打印你想要的b字段,并将x设置为零以便下一行使用。


缺陷

如果字段同时包含转义引号和逗号,则会失败。


编辑

更新以支持空字段。

awk '{while(match($0,/("[^"]+",|[^,]*,|([^,]+$))/,a)){
     $0=substr($0,RSTART+RLENGTH);b[++x]=a[0]}
     print b[1] b[4];x=0}' file

如果一个字段为空,例如 foo,,bar,它也会失败。 - Ed Morton
@EdMorton 修复了,我想是吧? - user4453924
1
看起来不错。现在当你设置x=0时,需要添加delete b,否则当当前记录的字段较少时,b将保留上一条记录末尾的内容,例如对于输入行a,b,c后跟着只有d,e的情况下,print b[3]会输出两次c - Ed Morton
@EdMorton 是的,我也考虑过这个问题,但鉴于OP的问题,我假设总会有第四个字段。 - user4453924
这在bash中使用awk会导致"非法语句"的语法错误。 - Ben Wheeler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接