【问题标题】:How to handle commas within a CSV file being read by bash script如何处理 bash 脚本读取的 CSV 文件中的逗号
【发布时间】:2012-02-14 22:56:43
【问题描述】:

我正在创建一个 bash 脚本来从 CSV 文件生成一些输出(我有超过 1000 个条目,不喜欢手动执行...)。

CSV 文件的内容类似于:

Australian Capital Territory,AU-ACT,20034,AU,Australia
Piaui,BR-PI,20100,BR,Brazil
"Adygeya, Republic",RU-AD,21250,RU,Russian Federation

我有一些代码可以使用逗号作为分隔符来分隔字段,但有些值实际上包含逗号,例如Adygeya, Republic。这些值用引号括起来,表示其中的字符应被视为字段的一部分,但我不知道如何解析它以考虑到这一点。

目前我有这个循环:

while IFS=, read province provinceCode criteriaId countryCode country
do
    echo "[$province] [$provinceCode] [$criteriaId] [$countryCode] [$country]"
done < $input

它为上面给出的样本数据产生这个输出:

[Australian Capital Territory] [AU-ACT] [20034] [AU] [Australia]
[Piaui] [BR-PI] [20100] [BR] [Brazil]
["Adygeya] [ Republic"] [RU-AD] [21250] [RU,Russian Federation]

如您所见,第三个条目解析不正确。我希望它输出

[Adygeya Republic] [RU-AD] [21250] [RU] [Russian Federation]

【问题讨论】:

  • 谢谢@TomWhittock,我会调查那个答案给出的链接,我以前从未使用过awk,所以可能需要对它进行检查(为了其他人的利益,链接是:backreference.org/2010/04/17/csv-parsing-with-awk)
  • 您不能用“|”、制表符或其他一些未出现在输入中的字符重新导出数据吗?祝你好运。
  • @shellter 不幸的是,我无法控制数据的导出
  • 还可以在 google 群组中搜索 comp.lang.awk。 10 年前有 3 个月的关于处理 CSV 的讨论。一些非常复杂的解决方案。祝你好运。

标签: bash scripting csv


【解决方案1】:

如果您想在 awk 中完成所有操作(此脚本需要 GNU awk 4 才能按预期工作):

awk '{ 
 for (i = 0; ++i <= NF;) {
   substr($i, 1, 1) == "\"" && 
     $i = substr($i, 2, length($i) - 2)
   printf "[%s]%s", $i, (i < NF ? OFS : RS)
    }   
 }' FPAT='([^,]+)|("[^"]+")' infile

示例输出:

% cat infile
Australian Capital Territory,AU-ACT,20034,AU,Australia
Piaui,BR-PI,20100,BR,Brazil
"Adygeya, Republic",RU-AD,21250,RU,Russian Federation
% awk '{    
 for (i = 0; ++i <= NF;) {
   substr($i, 1, 1) == "\"" &&
     $i = substr($i, 2, length($i) - 2)
   printf "[%s]%s", $i, (i < NF ? OFS : RS)
    }
 }' FPAT='([^,]+)|("[^"]+")' infile
[Australian Capital Territory] [AU-ACT] [20034] [AU] [Australia]
[Piaui] [BR-PI] [20100] [BR] [Brazil]
[Adygeya, Republic] [RU-AD] [21250] [RU] [Russian Federation]

使用 Perl

perl -MText::ParseWords -lne'
 print join " ", map "[$_]", 
   parse_line(",",0, $_);
  ' infile 

这应该适用于您的 awk 版本(基于 thisc.u.s. 帖子,也删除了嵌入的逗号)。

awk '{
 n = parse_csv($0, data)
 for (i = 0; ++i <= n;) {
    gsub(/,/, " ", data[i])
    printf "[%s]%s", data[i], (i < n ? OFS : RS)
    }
  }
function parse_csv(str, array,   field, i) { 
  split( "", array )
  str = str ","
  while ( match(str, /[ \t]*("[^"]*(""[^"]*)*"|[^,]*)[ \t]*,/) ) { 
    field = substr(str, 1, RLENGTH)
    gsub(/^[ \t]*"?|"?[ \t]*,$/, "", field)
    gsub(/""/, "\"", field)
    array[++i] = field
    str = substr(str, RLENGTH + 1)
  }
  return i
}' infile

【讨论】:

  • 谢谢,我安装的 Debian 6 似乎没有使用 awk 4,我认为该软件包会有更新版本的 awk
  • 你可以试试我刚刚添加的 Perl 解决方案。
  • 接受并 +1,因为我认为这是最好的解决方案,即使它不是我可以在这种情况下使用的解决方案
  • 嗨@chrisbunney,我添加了应该与您的 awk 版本一起使用的版本。
【解决方案2】:

使用 Dimitre 的解决方案(谢谢)我注意到他的程序忽略了空字段。

这里是修复:

awk '{ 
 for (i = 0; ++i <= NF;) {
   substr($i, 1, 1) == "\"" && 
     $i = substr($i, 2, length($i) - 2)
   printf "[%s]%s", $i, (i < NF ? OFS : RS)
    }   
 }' FPAT='([^,]*)|("[^"]+")' infile

【讨论】:

    【解决方案3】:

    如果您可以容忍在输出中保留周围的引号,您可以使用我编写的一个名为 csvquote 的小脚本来启用 awk 和 cut(以及其他 UNIX 文本工具)来正确处理包含逗号的引用字段。你像这样包装命令:

    csvquote inputfile.csv | awk -F, '{print "["$1"] ["$2"] ["$3"] ["$4"] ["$5"]"}' | csvquote -u
    

    有关代码和文档,请参阅 https://github.com/dbro/csvquote

    【讨论】:

      【解决方案4】:

      由于我的系统上的 awk 版本稍有过时,并且个人偏好坚持使用 Bash 脚本,我得到了一个稍微不同的解决方案。

      我制作了一个基于this blog post 的实用程序脚本,它解析 CSV 文件并用您选择的分隔符替换分隔符,以便捕获输出并用于轻松处理数据。该脚本尊重带引号的字符串和嵌入的逗号,但会删除它找到的双引号,并且不适用于字段中的转义双引号。

      #!/bin/bash
      
      input=$1
      delimiter=$2
      
      if [ -z "$input" ];
      then
          echo "Input file must be passed as an argument!"
          exit 98
      fi
      
      if ! [ -f $input ] || ! [ -e $input ];
      then
          echo "Input file '"$input"' doesn't exist!"
          exit 99
      fi
      
      if [ -z "$delimiter" ];
      then
          echo "Delimiter character must be passed as an argument!"
          exit 98
      fi
      
      gawk '{
          c=0
          $0=$0","                                   # yes, cheating
          while($0) {
              delimiter=""
              if (c++ > 0) # Evaluate and then increment c
              {
                  delimiter="'$delimiter'"
              }
      
              match($0,/ *"[^"]*" *,|[^,]*,/)
              s=substr($0,RSTART,RLENGTH)             # save what matched in f
              gsub(/^ *"?|"? *,$/,"",s)               # remove extra stuff
              printf (delimiter s)
              $0=substr($0,RLENGTH+1)                 # "consume" what matched
          }
          printf ("\n")
      }' $input
      

      只是发布它以防其他人发现它有用。

      【讨论】:

        【解决方案5】:

        在查看了here 上的@Dimitre 解决方案之后。你可以做这样的事情-

        #!/usr/local/bin/gawk -f
        
        BEGIN {
            FS="," 
            FPAT="([^,]+)|(\"[^\"]+\")"
            }
        
              {
            for (i=1;i<=NF;i++) 
                printf ("[%s] ",$i);
            print ""
            } 
        

        测试:

        [jaypal:~/Temp] cat filename
        Australian Capital Territory,AU-ACT,20034,AU,Australia
        Piaui,BR-PI,20100,BR,Brazil
        "Adygeya, Republic",RU-AD,21250,RU,Russian Federation
        
        [jaypal:~/Temp] ./script.awk  filename
        [Australian Capital Territory] [AU-ACT] [20034] [AU] [Australia] 
        [Piaui] [BR-PI] [20100] [BR] [Brazil] 
        ["Adygeya, Republic"] [RU-AD] [21250] [RU] [Russian Federation] 
        

        要删除",您可以将输出通过管道传输到sed

        [jaypal:~/Temp] ./script.awk  filename | sed 's#\"##g'
        [Australian Capital Territory] [AU-ACT] [20034] [AU] [Australia] 
        [Piaui] [BR-PI] [20100] [BR] [Brazil] 
        [Adygeya, Republic] [RU-AD] [21250] [RU] [Russian Federation] 
        

        【讨论】:

        • 谢谢,不知道为什么这是社区 wiki,但会检查一下 :)
        • @chrisbunney 因为我将 dimitre 的解决方案作为参考,所以我认为将这个答案归功于自己是不合适的。 :)
        • 刚刚对此进行了测试,它对我的​​输出与它对您的输出不同。事实上,它会产生与我在问题中描述的相同的“坏”输出
        • @chrisbunney 看起来像 awk 版本问题。我在gnu-awk v 4.0.0 上测试过
        • 是的,在@Dimitre 的帮助下,我的机器上有一个旧版本的 awk
        【解决方案6】:

        在思考了这个问题之后,我意识到由于字符串中的逗号对我来说并不重要,因此在解析之前将其从输入中删除会更容易。

        为此,我编写了一个sed 命令来匹配由包含逗号的双引号包围的字符串。然后该命令会从匹配的字符串中删除您不想要的位。它通过将正则表达式分成记住的部分来做到这一点。

        此解决方案仅适用于字符串在双引号之间包含单个逗号的情况。

        未转义的正则表达式是

        (")(.*)(,)(.*)(")
        

        第一、第三和第五对括号分别捕获开始双引号、逗号和结束双引号。

        第二对和第三对括号捕获我们想要保留的字段的实际内容。

        sed 删除逗号的命令

        echo "$input" | sed 's/\(\"\)\(.*\)\(,\)\(.*\)\(\"\)/\1\2\3\4/' 
        

        sed 删除逗号和双引号的命令

        echo "$input" | sed 's/\(\"\)\(.*\)\(,\)\(.*\)\(\"\)/\2\3/' 
        

        更新代码

        tmpFile=$input"Temp"
        sed 's/\(\"\)\(.*\)\(,\)\(.*\)\(\"\)/\2\4/' < $input > $tmpFile
        while IFS=, read province provinceCode criteriaId countryCode country
        do
            echo "[$province] [$provinceCode] [$criteriaId] [$countryCode] [$country]"
        done < $tmpFile
        rm $tmpFile
        

        输出

        [Australian Capital Territory] [AU-ACT] [20034] [AU] [Australia]
        [Piaui] [BR-PI] [20100] [BR] [Brazil]
        [Adygeya Republic] [RU-AD] [21250] [RU] [Russian Federation]
        [Bío-Bío] [CL-BI] [20154] [CL] [Chile]
        

        【讨论】:

        • 在某些特定情况下这可能有效,但在很多情况下无效。一个重要的问题是在sed 中,诸如.* 之类的匹配是贪婪的。
        • 感谢您的反馈。根据我的意见,我相信这会很好,但我有兴趣找出如何改进通用解决方案。这会是一种改进吗? (")(^,*)(,)(^"*)(") 显然sed 不支持惰性匹配,但否定字符类可能会起作用。我希望转义引号也会引起问题
        猜你喜欢
        • 2017-12-04
        • 2016-07-17
        • 2014-10-05
        • 1970-01-01
        • 1970-01-01
        • 2019-08-03
        • 1970-01-01
        • 1970-01-01
        • 2013-06-18
        相关资源
        最近更新 更多