Contents

如何在Bash中解析CSV文件

1. 概述

在本教程中,我们将了解如何使用各种 Bash 内置实用程序解析逗号分隔值 (CSV) 文件中的值。

首先,我们将讨论从文件中读取记录的先决条件。

之后,我们将检查将 CSV 文件解析为 Bash 变量和数组列表的不同技术。

最后,我们将讨论如何使用一些第三方工具进行高级 CSV 解析。

2. 先决条件

让我们简单回顾一下为 CSV 文件定义的标准

  1. 每条记录都在一个单独的行上,由换行符分隔
  2. 文件中的最后一条记录可能以换行符结尾,也可能不以换行符结尾。
  3. 可能有一个可选的标题行作为文件的第一行出现,其格式与常规记录行相同。
  4. 在标题和记录中,可能有一个或多个字段由逗号分隔
  5. 包含换行符、双引号和逗号的字段应该用双引号引起来。
  6. 如果使用双引号将字段括起来,那么出现在字段内的双引号必须通过在其前面加上另一个双引号来转义。

包含带引号字符串的逗号或换行符的记录的 CSV 文件不在我们的范围内。但是,我们将在本教程的最后一节中简要讨论它。

现在让我们设置我们的标准示例 CSV 文件:

$ cat input.csv
SNo,Quantity,Price,Value
1,2,20,40
2,5,10,50

2.1. 从文件中读取记录

我们现在将运行一个示例来从我们的输入文件中读取记录:

#!/bin/bash
while read line
do
   echo "Record is : $line"
done < input.csv

在这里,我们使用read 命令读取CSV 文件中以换行符 ( \n ) 分隔的记录。让我们检查脚本的输出:

Record is : SNo,Quantity,Price,Value
Record is : 1,2,20,40
Record is : 2,5,10,50

正如我们所注意到的,有一个复杂的问题:文件的标题也得到了处理。那么,让我们深入研究解决方案。

2.2. 忽略标题行

让我们运行另一个示例以从输出中排除标题行:

#!/bin/bash
while read line
do
   echo "Record is : $line"
done < <(tail -n +2 input.csv)

在这里,我们使用tail 命令从文件的第二行开始读取。随后,我们使用进程替换 将输出作为文件传递给 while循环。< (..)部分使我们能够指定tail命令并让 Bash 像读取文件一样从其输出中读取:

Record is : 1,2,20,40
Record is : 2,5,10,50

我们现在将尝试另一种方法来获得相同的结果:

#!/bin/bash
exec < input.csv
read header
while read line
do
   echo "Record is : $line"
done

在这种方法中,我们使用exec 命令将标准输入更改为从文件中读取。后来,我们使用read命令来处理标题行。随后,我们在while循环中处理剩余的文件。

3. 从 CSV 文件解析值

到目前为止,我们一直在从 CSV 文件中读取以换行符分隔的记录。此后,我们将研究从每个数据记录中读取值的方法

3.1. 来自所有专栏

让我们检查一种在遍历 CSV 文件时存储字段值的方法:

#! /bin/bash
while IFS="," read -r rec_column1 rec_column2 rec_column3 rec_column4
do
  echo "Displaying Record-$rec_column1"
  echo "Quantity: $rec_column2"
  echo "Price: $rec_column3"
  echo "Value: $rec_column4"
  echo ""
done < <(tail -n +2 input.csv)

请注意,我们**在while循环中将输入字段分隔符 (IFS) 设置为*“,”。**因此,我们可以使用read*命令将逗号分隔的字段值解析为 Bash 变量。

我们还要检查执行上述脚本时生成的输出:

Displaying Record-1
Quantity: 2
Price: 20
Value: 40
Displaying Record-2
Quantity: 5
Price: 10
Value: 50

3.2. 从前几列

在某些情况下,我们可能只想读取文件的前几列进行处理。

让我们用一个例子来证明它:

#! /bin/bash
while IFS="," read -r rec_column1 rec_column2 rec_remaining
do
  echo "Displaying Record-$rec_column1"
  echo "Quantity: $rec_column2"
  echo "Remaining fields of Record-$rec_column1 : $rec_remaining"
  echo ""
done < <(tail -n +2 input.csv)

在此示例中,我们可以将输入 CSV 的第一个和第二个字段中的值分别存储在rec_column1rec_column2变量中。值得注意的是,我们将剩余字段存储在rec_remaining变量中。

让我们看看脚本的输出:

Displaying Record-1
Quantity: 2
Remaining fields of Record-1 : 20,40
Displaying Record-2
Quantity: 5
Remaining fields of Record-2 : 10,50

3.3. 从特定的列号

同样,**我们将使用进程替换仅将特定列传递给while循环以供读取。此外,要获取这些列,我们将使用cut 命令:

#! /bin/bash
while IFS="," read -r rec1 rec2
do
  echo "Displaying Record-$rec1"
  echo "Price: $rec2"
done < <(cut -d "," -f1,3 input.csv | tail -n +2)

结果,我们只能解析输入 CSV 的第一列和第三列。

让我们用输出来验证它:

Displaying Record-1
Price: 20
Displaying Record-2
Price: 10

3.4. 来自特定的列名

在某些情况下,我们可能需要根据标题行中的列名解析 CSV 中的值。

让我们用一个简单的用户输入驱动的脚本来说明这一点:

#! /bin/bash
col_a='SNo'
read -p "Enter the column name to be printed for each record: " col_b
loc_col_a=$(head -1 input.csv | tr ',' '\n' | nl |grep -w "$col_a" | tr -d " " | awk -F " " '{print $1}')
loc_col_b=$(head -1 input.csv | tr ',' '\n' | nl |grep -w "$col_b" | tr -d " " | awk -F " " '{print $1}')
while IFS="," read -r rec1 rec2
do
  echo "Displaying Record-$rec1"
  echo "$col_b: $rec2"
  echo ""
done < <(cut -d "," -f${loc_col_a},${loc_col_b} input.csv | tail -n +2)

此脚本将col_b作为用户的输入,并为文件中的每条记录打印相应的列值。

我们使用trawkgrepnl 命令的组合来计算列的位置。

首先,我们使用tr命令将标题行中的逗号转换为换行符。然后,我们使用nl命令在每行的开头附加行号。随后,我们使用grep命令搜索输出中的列名,并使用tr命令截断前面的空格。

最后,我们使用awk命令获取了第一个字段,对应的是列号。

我们将上面的脚本保存为parse_csv.sh来执行:

$ ./parse_csv.sh
Enter the column name to be printed for each record: Price
Displaying Record-1
Price: 20
Displaying Record-2
Price: 10

正如预期的那样,当“Price”作为输入时,仅打印与标题中的字符串“Price”对应的列号的值。当不能保证 CSV 文件中的列顺序时,此方法特别有用。

4. 将 CSV 文件的列映射到 Bash 数组

在上一节中,我们将字段值解析为每条记录的 Bash 变量。现在我们将检查将 CSV 的整个列解析为 Bash 数组的方法:

#! /bin/bash
arr_record1=( $(tail -n +2 input.csv | cut -d ',' -f1) )
arr_record2=( $(tail -n +2 input.csv | cut -d ',' -f2) )
arr_record3=( $(tail -n +2 input.csv | cut -d ',' -f3) )
arr_record4=( $(tail -n +2 input.csv | cut -d ',' -f4) )
echo "array of SNos  : ${arr_record1[@]}"
echo "array of Qty   : ${arr_record2[@]}"
echo "array of Price : ${arr_record3[@]}"
echo "array of Value : ${arr_record4[@]}"

我们使用命令替换来使用 tail命令排除标题行,然后使用cut命令过滤相应的列。值得注意的是,需要第一组括号将命令替换的输出作为数组保存在变量arr_record1

让我们检查脚本输出:

array of SNos  : 1 2
array of Qty   : 2 5
array of Price : 20 10
array of Value : 40 50

5. 将 CSV 文件解析为 Bash 数组

在某些情况下,我们可能更愿意将整个 CSV 文件映射到一个数组中。实际上,我们可以使用数组来处理记录。

让我们检查一下实现:

#! /bin/bash 
arr_csv=() 
while IFS= read -r line 
do
    arr_csv+=("$line")
done < input.csv
echo "Displaying the contents of array mapped from csv file:"
index=0
for record in "${arr_csv[@]}"
do
    echo "Record at index-${index} : $record"
	((index++))
done

首先,在此示例中,我们从输入 CSV 中读取行,然后将其附加到数组arr_csv+=用于将记录附加到 Bash 数组)。然后,我们使用for循环打印数组的记录。

让我们检查一下输出:

Displaying the contents of array mapped from csv file:
Record at index-0 : SNo,Quantity,Price,Value
Record at index-1 : 1,2,20,40
Record at index-2 : 2,5,10,50

对于 Bash 版本 4 及更高版本,我们还可以使用*readarray * 命令填充数组:

readarray -t array_csv < input.csv

这会将input.csv中的行读入数组变量:array_csv-t选项 将从每行中删除尾随的换行符。

6. 解析记录中有换行符和逗号的 CSV 文件

到目前为止,在本教程中,我们使用文件input.csv来运行所有插图。

现在,让我们创建另一个 CSV 文件,其中包含带引号的字符串中的换行符和逗号:

$ cat address.csv
SNo,Name,Address
1,Bruce Wayne,"1007 Mountain Drive,
Gotham"
2,Sherlock Holmes,"221b Baker Street, London"

CSV 文件中可以有多个换行符、逗号和引号的排列和组合。因此,仅使用 Bash 内置实用程序来处理此类 CSV 文件是一项复杂的任务。通常,csvkit 等第三方工具用于高级 CSV 解析

然而,另一种合适的替代方法是使用PythonCSV 模块 ,因为 Python 通常预装在大多数 Linux 发行版上。