【问题标题】:How to make awk ignore the field delimiter inside double quotes? [duplicate]如何让 awk 忽略双引号内的字段分隔符? [复制]
【发布时间】:2015-06-20 22:25:28
【问题描述】:

我需要删除逗号分隔值文件中的 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 忽略双引号内的“,”?

【问题讨论】:

    标签: bash shell awk


    【解决方案1】:

    来自 GNU awk 手册 (http://www.gnu.org/software/gawk/manual/gawk.html#Splitting-By-Content):

    $ awk -vFPAT='([^,]*)|("[^"]+")' -vOFS=, '{print $1,$4}' file
    "abc@xyz.com,www.example.com",field4
    "def@xyz.com",field4
    

    请参阅What's the most robust way to efficiently parse CSV using awk? 以更一般地解析字段中包含换行符等的 CSV。

    【讨论】:

    • 我很好奇内部发生了什么?这是 perl 中正则表达式的用法,它产生了非常不同的匹配:perl -lnE 'while(/([^,]*)|("[^"]+")/g){say "#$1#"}' <<<'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA'
    • @rubystallion 您必须向 gawk 开发人员询问内部发生的情况,但正则表达式通常匹配最左边最长的字符串,因此 perl 在 "1234 A Pretty StreetNE" 上匹配为 2 单独的字符串 似乎是错误的,因为加上上面的 awk 和 grep -Eo '([^,]*)|("[^"]+")' <<<'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA'"1234 A Pretty Street, NE" 识别为单个字符串。当然,perl 脚本可能包含一些神奇的咒语,意思是“不匹配最左边最长的”,idk 因为我没有得到 perl 语法。
    • 感谢 grep 示例,它指出了在哪里可以找到答案:POSIX spec 说:If the pattern permits a variable number of matching characters and thus there is more than one such sequence starting at that point, the longest such sequence is matched. For example, the BRE "bb*" matches the second to fourth characters of the string "abbbc", and the ERE "(wee|week)(knights|night)" matches all ten characters of the string "weeknights".
    • perl specAlternatives are tried from left to right, so the first alternative found for which the entire expression matches, is the one that is chosen. This means that alternatives are not necessarily greedy. For example: when matching "foo|foot" against "barefoot", only the "foo" part will match, as that is the first alternative tried, and it successfully matches the target string.
    • @RalphCallaway 对,FPAT 是 GNU awk 扩展。安装 gawk 或查看我的答案中的链接,了解适用于任何 awk 的解决方案。
    【解决方案2】:

    这不是 bash/awk 解决方案,但我推荐CSVKit,可以通过pip install csvkit 安装。它提供了一组专门用于 CSV 的命令行工具,包括 csvcut,它完全符合您的要求:

    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 here on RTD 的文档。 ThoughtBot 有一个nice little blog post 介绍这个工具,这是我了解 CSVKit 的地方。

    【讨论】:

    • CSVKit 太棒了!感谢您向我介绍它:)
    • 在 MacOS 上我成功使用 brew 安装,而不是 pip
    【解决方案3】:

    在您的示例输入文件中,引用的是第一个字段并且只有第一个字段。如果一般情况下是这样,那么考虑以下作为删除第二列和第三列的方法:

    $ 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
    

    正如 cmets 中所述,awk 本身并不理解带引号的分隔符。该解决方案通过查找以引号结尾的第一个字段来解决此问题。然后它会跳过后面的两个字段。

    细节

    • for (i=1;i&lt;=NF;i++)

      这会在每个字段 i 上开始一个 for

    • printf "%s%s",(i&gt;1)?",":"",$i

      这将打印字段i。如果不是第一个字段,则该字段前面有一个逗号。

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

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

    • print""

      在我们完成for 循环之后,这将打印一个换行符。

    【讨论】:

      【解决方案4】:

      无论引用的字段在哪里,这个 awk 都应该可以工作,并且也可以在转义的引号上工作。

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

      输入

      "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.
      • @EdMorton 我认为已修复?
      • 看起来更好。现在您需要在设置x=0 时添加delete bb 将在当前记录的字段较少时保留上一条记录末尾的内容,例如print b[3] 用于输入行a,b,c 后跟 d,e 将输出 c 两次。
      • @EdMorton 是的,我想到了这一点,但鉴于 OP 的问题,我认为总会有第四个字段。
      猜你喜欢
      • 2017-06-14
      • 2018-06-30
      • 1970-01-01
      • 2020-05-23
      • 1970-01-01
      • 2014-03-04
      • 2020-04-13
      • 1970-01-01
      相关资源
      最近更新 更多