【问题标题】:Can awk deal with CSV file that contains comma inside a quoted field?awk 可以处理在引用字段中包含逗号的 CSV 文件吗?
【发布时间】:2011-03-09 11:27:39
【问题描述】:

我正在使用 awk 来计算 csv 文件中一列的总和。数据格式类似于:

id, name, value
1, foo, 17
2, bar, 76
3, "I am the, question", 99

我正在使用这个 awk 脚本来计算总和:

awk -F, '{sum+=$3} END {print sum}'

名称字段中的某些值包含逗号,这会破坏我的 awk 脚本。 我的问题是:awk 能解决这个问题吗?如果是,我该怎么做?

谢谢。

【问题讨论】:

    标签: csv awk field text-parsing quoting


    【解决方案1】:

    使用GNU awkFPAT 的一种方式

    awk 'BEGIN { FPAT = "([^, ]+)|(\"[^\"]+\")" } { sum+=$3 } END { print sum }' file.txt
    

    结果:

    192
    

    【讨论】:

    • FPAT 方法很棒,但它仅在 FS 为单个字符时才有效,因为您无法否定 RE。当 FS 是一个字符串时它不起作用,因为在这种情况下,AFAIK 它是“,”,所以尽管它适用于发布的特定示例输入数据,但它会识别出太多字段给定一个输入行,其中一个字段包含空格但不包含在引号中。
    • 好吧,CSV 没有一个标准,所以 YMMV 但通常在您需要在字段中包含字段分隔符时使用引号,而不是当您只有空格时。例如,如果单元格包含空格,MS-Excel 在保存为 CSV 格式时不会使用引号,除非它包含逗号。
    • 这很好,只是您需要能够匹配完全空白的字段:FPAT = "([^, ]*)|(\"[^\"]+\")" }。否则,它会与 22,,,"some string" 等行中的字段不匹配
    • 由于双引号中包含的字段本身可以包含双引号(然后被额外的双引号转义),我采用这种模式:FPAT = "([^,]*)|(\"([^\"]|(\"\"))*\")"
    • @EdMorton 是的,您可以通过在相应自动机中切换最终状态和非最终状态来否定正则表达式。但这通常会导致非常难看的正则表达式。
    【解决方案2】:

    我正在使用

    `FPAT="([^,]+)|(\"[^\"]+\")" `
    

    用 gawk 定义字段。我发现当该字段为空时,这无法识别正确数量的字段。因为“+”要求字段中至少有 1 个字符。 我把它改成:

    `FPAT="([^,]*)|(\"[^\"]*\")"`
    

    并将"+" 替换为"*"。它工作正常。

    我也发现 GNU Awk User Guide 也有这个问题。 https://www.gnu.org/software/gawk/manual/html_node/Splitting-By-Content.html

    【讨论】:

    • 这个解决方案确实需要固定在某个地方。您会期望默认行为也会计算空字段!
    【解决方案3】:

    在 perl 中使用 Text::CSV 可能会更好,因为这是一个快速而强大的解决方案。

    【讨论】:

    • 是的,我同意你的观点,我只是想知道 awk 如何处理这个问题。 :)
    • 查看我发布的关于如何识别一般字段的答案,但对于您的具体问题,@HaiVu 给出的答案是正确的。
    【解决方案4】:

    对于像这样简单的输入文件,您只需编写一个小函数将引号之外的所有真实 FS 转换为其他值(我选择 RS,因为记录分隔符不能是记录的一部分),然后将其用作 FS,例如:

    $ cat decsv.awk
    BEGIN{ fs=FS; FS=RS }
    
    {
       decsv()
    
       for (i=1;i<=NF;i++) {
           printf "Record %d, Field %d is <%s>\n" ,NR,i,$i
       }
       print ""
    }
    
    function decsv(         curr,head,tail)
    {
       tail = $0
       while ( match(tail,/"[^"]+"/) ) {
           head = substr(tail, 1, RSTART-1);
           gsub(fs,RS,head)
           curr = curr head substr(tail, RSTART, RLENGTH)
           tail = substr(tail, RSTART + RLENGTH)
       }
       gsub(fs,RS,tail)
       $0 = curr tail
    }
    
    $ cat file
    id, name, value
    1, foo, 17
    2, bar, 76
    3, "I am the, question", 99
    
    $ awk -F", " -f decsv.awk file
    Record 1, Field 1 is <id>
    Record 1, Field 2 is <name>
    Record 1, Field 3 is <value>
    
    Record 2, Field 1 is <1>
    Record 2, Field 2 is <foo>
    Record 2, Field 3 is <17>
    
    Record 3, Field 1 is <2>
    Record 3, Field 2 is <bar>
    Record 3, Field 3 is <76>
    
    Record 4, Field 1 is <3>
    Record 4, Field 2 is <"I am the, question">
    Record 4, Field 3 is <99>
    

    只有当您必须处理嵌入的换行符和嵌入的引号中的转义引号时,它才会变得复杂,即使这样也不会太难,而且之前都已经完成了......

    更多信息请参见What's the most robust way to efficiently parse CSV using awk?

    【讨论】:

      【解决方案5】:

      您可以使用我编写的名为 csvquote 的小脚本帮助 awk 处理包含逗号(或换行符)的数据字段。它用非打印字符替换引用字段中的冒犯逗号。如果需要,您可以稍后恢复这些逗号 - 但在这种情况下,您不需要。

      命令如下:

      csvquote inputfile.csv | awk -F, '{sum+=$3} END {print sum}'
      

      代码见https://github.com/dbro/csvquote

      【讨论】:

        【解决方案6】:

        您总是可以从源头上解决问题。在名称字段周围加上引号,就像“我是问题”字段一样。这比花时间编写解决方法要容易得多。

        更新(按照丹尼斯的要求)。一个简单的例子

        $ s='id, "name1,name2", value 1, foo, 17 2, bar, 76 3, "I am the, question", 99'
        
        $ echo $s|awk -F'"' '{ for(i=1;i<=NF;i+=2) print $i}'
        id,
        , value 1, foo, 17 2, bar, 76 3,
        , 99
        
        $ echo $s|awk -F'"' '{ for(i=2;i<=NF;i+=2) print $i}'
        name1,name2
        I am the, question
        

        如您所见,通过将分隔符设置为双引号,属于“引号”的字段总是在偶数上。由于 OP 没有修改源数据的奢侈,所以这种方法不适合他。

        【讨论】:

        • 如果您展示了如何处理带引号的字段,也许会有所帮助。
        • 谢谢,Dennis 但是 csv 文件是由客户端生成的,所以我不能对文件格式做任何事情。 :(
        【解决方案7】:

        这篇文章确实帮助我解决了同样的数据字段问题。大多数 CSV 会在包含空格或逗号的字段周围加上引号。这会弄乱 awk 的字段计数,除非您将它们过滤掉。

        如果您需要那些包含垃圾的字段中的数据,那么这不适合您。 ghostdog74 提供了答案,它清空了该字段,但最终保持了总字段数,这是保持数据输出一致的关键。我不喜欢这个解决方案如何引入新行。这是我使用的这个解决方案的版本。前三个字段在数据中从来没有出现过这个问题。包含客户姓名的第四个字段经常这样做,但我需要该数据。显示问题的其余字段我可以毫无问题地丢弃,因为我的报告输出中不需要它。所以我首先非常明确地清除了第四个字段的垃圾并删除了前两个引号实例。然后我应用 ghostdog74gave 来清空其中包含逗号的剩余字段 - 这也会删除引号,但我使用 printf 将数据保存在单个记录中。从我的 8000 多行杂乱数据中,我从 85 个字段开始,在所有情况下都以 85 个字段结束。满分!

        grep -i $1 $dbfile | sed 's/\, Inc.//;s/, LLC.//;s/, LLC//;s/, Ltd.//;s/\"//;s/\"//' | awk -F'"' '{ for(i=1;i<=NF;i+=2) printf ($i);printf ("\n")}' > $tmpfile
        

        清空其中包含逗号的字段但同时保留记录的解决方案当然是:

        awk -F'"' '{ for(i=1;i<=NF;i+=2) printf ($i);printf ("\n")}
        

        非常感谢 ghostdog74 提供的出色解决方案!

        NetsGuy256/

        【讨论】:

        • printf 是一个内置函数而不是一个函数,所以“(”s 不会做你认为不合适的事情。此外,printf 的概要是“printf fmt, values” - 做“printf values " 带有用户输入是危险的,应该避免。最后,不要通过 printf "\n" 硬编码 ORS,只需使用 print "" 并让 ORS 自然扩展。
        【解决方案8】:

        FPAT 是一个优雅的解决方案,因为它可以处理可怕的逗号内引号问题,但是要对最后一列中的一列数字求和,而不管前面的分隔符有多少,$NF 效果很好:

        awk -F"," '{sum+=$NF} END {print sum}'

        要访问倒数第二列,您可以使用:

        awk -F"," '{sum+=$(NF-1)} END {print sum}'

        【讨论】:

          【解决方案9】:

          如果您确定“值”列始终是最后一列:

          awk -F, '{sum+=$NF} END {print sum}'
          

          NF代表字段数,所以$NF是最后一列

          【讨论】:

            【解决方案10】:

            完全成熟的 CSV 解析器(例如 Perl 的 Text::CSV_XS)是专门为处理这种怪异而构建的。

            perl -MText::CSV_XS -lne 'BEGIN{$csv=Text::CSV_XS-&gt;new({allow_whitespace =&gt; 1})} if($csv-&gt;parse($_)){@f=$csv-&gt;fields();$sum+=$f[2]} END{print $sum}' file

            allow_whitespace 是必需的,因为输入数据在逗号分隔符周围有空格。 Text::CSV_XS 的旧版本可能不支持此选项。

            我在这里的回答中提供了对Text::CSV_XS 的更多解释:parse csv file using gawk

            【讨论】:

              【解决方案11】:

              您可以尝试通过 perl 正则表达式管道文件以将引用的 , 转换为类似 | 的其他内容。

              cat test.csv | perl -p -e "s/(\".+?)(,)(.+?\")/\1\|\3/g" | awk -F, '{...
              

              上面的正则表达式假定双引号内总是有一个逗号。所以需要更多的工作来使逗号成为可选的

              【讨论】:

                【解决方案12】:

                您在 awk 中编写如下函数:

                $ awk 'func isnum(x){return(x==x+0)}BEGIN{print isnum("hello"),isnum("-42")}'
                0 1
                

                你可以在你的脚本中加入这个函数并检查第三个字段是否是数字。如果不是数字,那么去第四个字段,如果第四个字段反过来不是数字,去第五个......直到你到达数值。可能循环在这里会有所帮助,并将其添加到总和中。

                【讨论】:

                • 这真的很笨拙,如果字段不是数字,它就会失败。 @Steve 的回答要好得多。
                • 不仅如此,如果字符串包含数字,它似乎会成功。几乎从未读过如此糟糕的接受答案。
                • 这个答案的另一个问题是,如果一行中缺少“值”,它将假定“id”是值,除非在 awk 程序中放入更多逻辑来表示“如果行中的项目是数字而不是第一个元素..."
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-05-20
                • 2017-03-17
                • 2017-09-14
                • 1970-01-01
                • 2016-12-14
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多