如何在Bash脚本读取CSV文件时处理逗号

10

我正在创建一个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]

1
请参见https://dev59.com/Imsz5IYBdhLWcg3wlYwn。 - Tom Whittock
谢谢@TomWhittock,我会调查答案中提供的链接,我以前从未使用过awk,所以可能需要学习一下(为了其他人的利益,链接是:http://backreference.org/2010/04/17/csv-parsing-with-awk/)。 - chrisbunney
你能否重新使用'|'、制表符或其他在输入中不存在的字符导出数据呢?祝好运。 - shellter
还可以在Google Groups上搜索comp.lang.awk。10年前有一个为期3个月的CSV处理讨论,提出了一些非常复杂的解决方案。祝你好运。 - shellter
Similar - Dennis Williamson
显示剩余4条评论
6个回答

9

如果您想全部使用 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 

根据这个c.u.s.的帖子(也删除了嵌入的逗号),这应该与您的awk版本兼容。

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

1
谢谢,看起来我的Debian 6安装没有使用awk 4,我以为这个软件包会有更新版本的awk。 - chrisbunney
1
你可以尝试我刚添加的 Perl 解决方案。 - Dimitre Radoulov
接受并+1,因为我认为这是最佳解决方案,即使在这种情况下我不能使用它。 - chrisbunney
嗨@chrisbunney,我已经添加了一个版本,应该可以与您的awk版本一起使用。 - Dimitre Radoulov

5

在查看这里@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] 

谢谢,不确定为什么这是社区维基,但我会去看看 :) - chrisbunney
@chrisbunney,因为我参考了dimitre的解决方案,所以认为把这个答案归功于自己是不合适的。 :) - jaypal singh
我刚测试了一下,它没有像你那样输出相同的结果。事实上,它产生了与我在问题中描述的相同的“坏”输出。 - chrisbunney
@chrisbunney 看起来是 awk 版本问题。我在 gnu-awk v 4.0.0 上测试过了。 - jaypal singh
是的,在@Dimitre的帮助下,结果发现我的机器上有一个较旧版本的awk。 - chrisbunney

2

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

为此,我想出了一个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 中匹配,如 .* 是贪婪的。 - Dennis Williamson
感谢反馈。我相信我的输入可以正常工作,但我有兴趣了解如何改进通用解决方案。这会是一个改进吗?(")(^,*)(,)(^"*)(") 显然 sed 不支持惰性匹配,但否定字符类可能有效。转义引号也可能会导致问题,我期望。 - chrisbunney

1
如果你可以容忍输出中包含周围引号,你可以使用我编写的一个名为csvquote的小脚本来使awk和cut(以及其他UNIX文本工具)正确处理包含逗号的带引号字段。你可以像这样包装命令:
csvquote inputfile.csv | awk -F, '{print "["$1"] ["$2"] ["$3"] ["$4"] ["$5"]"}' | csvquote -u

请查看https://github.com/dbro/csvquote获取代码和文档。


0

使用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

0
由于我的系统上的awk版本略有过时,而且我个人更喜欢使用Bash脚本,因此我想出了一个稍微不同的解决方案。
我基于这篇博客文章编写了一个实用程序脚本,它解析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

我只是把它发布出来,以防其他人发现它有用。


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