将 Bash 中的 CSV 读入 Dictionary/Associative 数组

Reading CSV in Bash into a Dictionary/Associative array

我正在尝试将 csv 文件读入 bash 关联数组,但没有得到我期望的结果。

使用 Bash 5.0.18

Bellum:fox3-api rocky$ bash --version
GNU bash, version 5.0.18(1)-release (x86_64-apple-darwin19.5.0)

foobar.csv

的内容
Bellum:scripts rocky$ cat ./foobar.csv
foo-1,bar-1
foo-2,bar-2
foo-3,bar-3

problem.sh

的内容
#!/usr/bin/env bash

declare -A descriptions
while IFS=, read name title; do
      echo "I got:$name|$title"
      descriptions[$name]=$title
done < foobar.csv

echo ${descriptions["foo-1"]}
echo ${descriptions["foo-2"]}
echo ${descriptions["foo-3"]}

来自 problem.sh

的实际输出
Bellum:scripts rocky$ ./problem.sh
I got:foo-1|bar-1
I got:foo-2|bar-2

bar-2

Bellum:scripts rocky$

期望的输出:

I got:foo-1|bar-1
I got:foo-2|bar-2
I got:foo-3|bar-3    
bar-1
bar-2
bar-3

评论请求的输出

    Bellum:scripts rocky$ head -n 1 ./foobar.csv | hexdump -C
    00000000  ef bb bf 66 6f 6f 2d 31  2c 62 61 72 2d 31 0d 0a  |...foo-1,bar-1..|
    00000010
    Bellum:scripts rocky$ od -c foobar.csv
    0000000  357 273 277   f   o   o   -   1   ,   b   a   r   -   1  \r  \n
    0000020    f   o   o   -   2   ,   b   a   r   -   2  \r  \n   f   o   o
    0000040    -   3   ,   b   a   r   -   3
    0000050

Cyrus 的 dos2unix 变化

    #!/usr/bin/env bash
    
    declare -A descriptions
    dos2unix < foobar.csv | while IFS=, read name title; do
          echo "I got:$name|$title"
          descriptions[$name]=$title
    done
    
    echo ${descriptions["foo-1"]}
    echo ${descriptions["foo-2"]}
    echo ${descriptions["foo-3"]}

Cyrus 的 dos2unix 更改的输出

    Bellum:scripts rocky$ ./problem.sh
    I got:foo-1|bar-1
    I got:foo-2|bar-2
    
    
    
    
    Bellum:scripts rocky$

csv 文件是通过从 Microsoft Excel 另存为 csv 在 Mac 上创建的。提前感谢您的任何见解。

混合解决方案

对于未来的人来说,这个问题其实是两个问题。第一个是从 Microsoft Excel 为 Mac 工作簿保存我的 CSV 文件。我另存为...“CSV UTF-8”格式(Excel 下拉菜单中列出的第一个 CSV 文件格式)。这会添加额外的字节,这些字节会扰乱 bash 中的读取命令。有趣的是,这些字节不会出现在 cat 命令中(请参阅原始 post 问题描述)。 将 Excel 中的 CSV 保存为“逗号分隔值”(在格式下拉列表的下方),解决了第一个问题。

其次,@Léa Gris 和@glenn jackman 为我指明了正确的方向 我的脚本的修饰符,这有助于一些换行符和回车符 return 字符 出现在 Excel 保存的文件中。

谢谢大家。我花了一整天试图弄清楚这一点。 经验教训:我应该早点转向 Whosebug。

以下是您未获得预期输出的原因:

    Bellum:scripts rocky$ od -c foobar.csv
    0000000  357 273 277   f   o   o   -   1   ,   b   a   r   -   1  \r  \n
    0000020    f   o   o   -   2   ,   b   a   r   -   2  \r  \n   f   o   o
    0000040    -   3   ,   b   a   r   -   3
    0000050
  1. 第一行的名称不仅仅包含“foo-1”——其中还有额外的字符。
    • 可以使用 "${name#$'737'}"
    • 删除它们
  2. 最后一行没有换行结束,所以while-read循环只迭代了两次。
    • read returns non-zero 如果它不能读取整行,即使它读取一些字符。
    • 因为读到returns“false”,while循环结束。
    • 可以使用以下方法解决此问题:
      while IFS=, read -r name title || [[ -n $title ]]; do ... 
      #............................. ^^^^^^^^^^^^^^^^^^ 
      
    • 或者,只修复文件。

结果:

BOM=$'737'
CR=$'\r'

declare -A descriptions
while IFS=, read name title || [[ $title ]]; do
  descriptions["${name#$BOM}"]=${title%$CR}
done < foobar.csv

declare -p descriptions
echo "${descriptions["foo-1"]}"
echo "${descriptions["foo-2"]}"
echo "${descriptions["foo-3"]}"
declare -A descriptions=([foo-1]="bar-1" [foo-2]="bar-2" [foo-3]="bar-3" )
bar-1
bar-2
bar-3

这将适用于您的输入文件,无论是 Unix 还是 DOS 换行符,无论 UTF-8 BOM 标记如何,也无论最后一行在文件结尾之前是否有换行符标记:

#!/usr/bin/env bash

declare -A descriptions
# IFS=$',\r\n' allow to capture either Unix or DOS Newlines
# read -r warrant not to expand \ escaped special characters
# || [ "$name" ] will make sure to capture last line
# even if it does not end with a newline marker
while IFS=$',\r\n' read -r name title || [ "$name" ]; do
      echo "I got:$name|$title"
      descriptions[$name]=$title
done < <(
  # Filter-out UTF-8 BOM if any
  sed $'1s/^737//' foobar.csv
)

echo "${descriptions["foo-1"]}"
echo "${descriptions["foo-2"]}"
echo "${descriptions["foo-3"]}"

# A shorter option for debug, is to dump the variable as a declaration
typeset -p descriptions

现在可以通过一种非常紧凑的方式将 CSV 一次性全部传输到关联数组中

#!/usr/bin/env bash

# shellcheck disable=SC2155 # Safe generated assignment with printf %q
declare -A descriptions="($(
  # Collect all values from file into an array
  IFS=$'\r\n,' read -r -d '' -a elements < <(
    # Discard the UTF-8 BOM from the input file if any
    sed $'1s/^737//' foobar.csv
  )
  # Format the elements into an Associative array declaration [key]=value 
  printf '[%q]=%q ' "${elements[@]}"
))"

echo "${descriptions["foo-1"]}"
echo "${descriptions["foo-2"]}"
echo "${descriptions["foo-3"]}"

# A shorter option for debug, is to dump the variable as a declaration
typeset -p descriptions

问题出在前 3 个字节上,您可以使用以下命令删除它们:

dd bs=1 skip=3 if=foobar.csv of=foobar2.csv

并尝试 foobar2.csv