如何在Bash中解析CSV文件?

150

我正在编写一长段Bash脚本。我想将CSV文件中的单元格读入Bash变量中。我能够解析行和第一列,但不能解析其他任何列。以下是目前我的代码:


  cat myfile.csv|while read line
  do
    read -d, col1 col2 < <(echo $line)
    echo "I got:$col1|$col2"
  done

它只打印第一列。作为额外的测试,我尝试了以下操作:

read -d, x y < <(echo a,b,)

$y为空。所以我尝试了:

read x y < <(echo a b)

$y是b。为什么呢?


7
你考虑过使用awk来使用$1$2等吗? - BeemerGuy
4
作为旁注:命令 < <(echo "string") ---> 命令 <<< "string" - tokland
1
'cut' 命令行程序就是为此而设计的:http://ss64.com/bash/cut.html - Jay
可能是 https://dev59.com/WVoV5IYBdhLWcg3wauKP 的重复问题。 - tripleee
你想要避免无用的cat使用 - tripleee
如果有帮助的话,我建议使用 awk - Jatin Chauhan
6个回答

259
你需要使用IFS而不是-d
while IFS=, read -r col1 col2
do
    echo "I got:$col1|$col2"
done < myfile.csv

要跳过指定数量的标题行:

skip_headers=3
while IFS=, read -r col1 col2
do
    if ((skip_headers))
    then
        ((skip_headers--))
    else
        echo "I got:$col1|$col2"
    fi
done < myfile.csv

请注意,对于一般目的的CSV解析,您应该使用专门的工具来处理带有内部逗号的引用字段以及Bash本身无法处理的其他问题。 这些工具的示例包括csvtoolcsvkit


9
针对非常简单的CSV文件,即标题和值不包含逗号和嵌入引号,建议的解决方案是可行的。编写通用的CSV解析器实际上非常棘手(特别是因为有几个CSV“标准”)。将CSV文件转换为TSV(制表符分隔值)是使它们更易于使用*nix工具的一种方法,例如使用Excel。 - peak
1
@Zsolt:没有理由会出现这种情况。你一定是打错了字或者有一个杂乱的非打印字符。 - Dennis Williamson
4
当使用 ; 作为分隔符时,你应该将其包含在内,例如:while IFS=";" read col1 col2; do ...。请注意,这句话的意思是在使用 read 命令时,需要将分隔符指定为 ; 并将其包含在引号中。 - thomas.mc.work
2
@thomas.mc.work:对于分号和其他对shell特殊的字符来说,这是正确的。但对于逗号来说,它并不是必需的,我倾向于省略那些不必要的字符。例如,你总是可以使用花括号(例如${var})来指定变量扩展,但当它们不必要时,我会省略它们。对我来说,这看起来更加清晰。 - Dennis Williamson
1
@DennisWilliamson,从一段时间以来,Bash源代码树提供了可加载的内置CSV解析器!请看看我的答案!当然,也有一些限制... - F. Hauri - Give Up GitHub
显示剩余8条评论

16
如何在Bash中解析CSV文件?
对于这个问题来说,由于提供了新功能,并且因为这个问题涉及到,并且已经发布的回答中没有展示这种强大且符合标准的方法,所以我来晚了。
bash下解析CSV文件,使用可加载模块 遵循RFC 4180的规范,像这个样本CSV行一样的字符串:
12,22.45,"Hello, ""man"".","A, b.",42

应该被拆分为

1  12
2  22.45
3  Hello, "man".
4  A, b.
5  42

bash 可加载的.C编译模块。

下,您可以创建、编辑和使用可加载的编译模块。一旦加载,它们就像任何其他内置命令一样工作!(您可以在源代码树中找到更多信息。;)

当前源代码树(2021年10月15日,bash V5.1-rc3)包含了许多示例:

accept        listen for and accept a remote network connection on a given port
asort         Sort arrays in-place
basename      Return non-directory portion of pathname.
cat           cat(1) replacement with no options - the way cat was intended.
csv           process one line of csv data and populate an indexed array.
dirname       Return directory portion of pathname.
fdflags       Change the flag associated with one of bash's open file descriptors.
finfo         Print file info.
head          Copy first part of files.
hello         Obligatory "Hello World" / sample loadable.
...
tee           Duplicate standard input.
template      Example template for loadable builtin.
truefalse     True and false builtins.
tty           Return terminal name.
uname         Print system information.
unlink        Remove a directory entry.
whoami        Print out username of current user.

examples/loadables 目录下有一个完整可用的 cvs 解析器: csv.c!!

在基于 Debian GNU/Linux 的系统中,您可能需要通过以下方式安装 bash-builtins 包:

apt install bash-builtins

使用可加载的 bash 内建函数:

然后:

enable -f /usr/lib/bash/csv csv

从那里开始,您可以将csv用作bash内置命令

使用我的示例:12,22.45,“你好,”“人”“。”,“A,b。”,42

csv -a myArray '12,22.45,"Hello, ""man"".","A, b.",42'
printf "%s\n" "${myArray[@]}" | cat -n
     1      12
     2      22.45
     3      Hello, "man".
     4      A, b.
     5      42

然后在循环中处理文件。

while IFS= read -r line;do
    csv -a aVar "$line"
    printf "First two columns are: [ '%s' - '%s' ]\n" "${aVar[0]}" "${aVar[1]}"
done <myfile.csv

这种方法显然比使用任何其他内置或fork到任何二进制文件的组合更快、更强大。

不幸的是,取决于您的系统实现,如果您的版本编译时没有使用loadable,则可能无法正常工作...

带有多行CSV字段的完整示例。

符合RFC 4180的规范,像这样的字符串单个CSV行

12,22.45,"Hello ""man"",
This is a good day, today!","A, b.",42

应该被拆分为

1  12
2  22.45
3  Hello "man",
   This is a good day, today!
4  A, b.
5  42

解析包含多行字段的CSV完整示例脚本

这是一个包含1个标题、4列和3行的小样本文件。由于两个字段包含换行符,因此该文件长度为6行。

Id,Name,Desc,Value
1234,Cpt1023,"Energy counter",34213
2343,Sns2123,"Temperatur sensor
to trigg for alarm",48.4
42,Eye1412,"Solar sensor ""Day /
Night""",12199.21

还有一个能够正确解析这个文件的小脚本:

#!/bin/bash

enable -f /usr/lib/bash/csv csv

file="sample.csv"
exec {FD}<"$file"

read -ru $FD line
csv -a headline "$line"
printf -v fieldfmt '%-8s: "%%q"\\n' "${headline[@]}"
numcols=${#headline[@]}

while read -ru $FD line;do
    while csv -a row "$line" ; (( ${#row[@]} < numcols )) ;do
        read -ru $FD sline || break
        line+=$'\n'"$sline"
    done
    printf "$fieldfmt\\n" "${row[@]}"
done

这可能会呈现为:(我使用了printf "%q"来表示非可打印字符,如换行符,作为$'\n'

Id      : "1234"
Name    : "Cpt1023"
Desc    : "Energy\ counter"
Value   : "34213"

Id      : "2343"
Name    : "Sns2123"
Desc    : "$'Temperatur sensor\nto trigg for alarm'"
Value   : "48.4"

Id      : "42"
Name    : "Eye1412"
Desc    : "$'Solar sensor "Day /\nNight"'"
Value   : "12199.21"

你可以在这里找到一个完整的工作示例:csvsample.sh.txt 或者 csvsample.sh

注意:

在这个示例中,我使用标题行来确定行宽(列数)。如果你的标题行包含换行符(或者如果你的CSV文件使用多个标题行),你将需要将列数作为参数传递给你的脚本(以及标题行的数量)。

警告:

当然,使用这种方法解析CSV并不完美!这对于许多简单的CSV文件有效,但要注意编码和安全性!例如,该模块无法处理二进制字段!

请仔细阅读csv.c源代码注释RFC 4180

关于带引号的多行字段的注意事项

特别是如果多行字段位于最后一列,这种方法将无法正确循环到第二个引号。

因此,在使用csv模块解析之前,您必须在$line中检查引号匹配。

您可以在使用bash解析带有不寻常字符、间距、括号和不规则换行符的大型CSV文件中找到一个完整的可工作示例。


当然,在 [tag:bash] 下解析 csv 并不完美:csv 可加载 将无法处理二进制字段,您可能会遇到 编码 问题和/或 安全 问题... 请仔细阅读 RFC 4180!!! - F. Hauri - Give Up GitHub

11

man页面中得知:

-d delim 使用delim的第一个字符终止输入行,而不是换行符。

你正在使用-d,,它将在逗号处终止输入行,它不会读取该行的其余部分。这就是为什么$y为空的原因。


7
我们可以使用以下代码解析带引号字符串并以 | 为分隔符的 csv 文件。
while read -r line
do
    field1=$(echo "$line" | awk -F'|' '{printf "%s", $1}' | tr -d '"')
    field2=$(echo "$line" | awk -F'|' '{printf "%s", $2}' | tr -d '"')

    echo "$field1 $field2"
done < "$csvFile"

awk解析字符串字段到变量中,tr删除引号。

稍微慢一些,因为每个字段都会执行awk


1
好的,你也可以使用逗号(,)。 - pkarc
2
使用Awk逐行处理是一种粗糙的反模式。awk -F'|' '{ gsub(/"/, ""); print $1, $2 }' "$csvFile" - tripleee

3
除了@Dennis Williamson的回答之外,如果CSV文件的第一行是标题的话,跳过它可能会更有帮助:
{
  read
  while IFS=, read -r col1 col2
  do
    echo "I got:$col1|$col2"
  done 
} < myfile.csv

0
如果你想读取包含几行的CSV文件,这是解决方案。
while IFS=, read -ra line
do 
    test $i -eq 1 && ((i=i+1)) && continue
    for col_val in ${line[@]}
    do
        echo -n "$col_val|"                 
    done
    echo        
done < "$csvFile"

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