Bash:使用引号、逗号和换行符解析 CSV
Posted
技术标签:
【中文标题】Bash:使用引号、逗号和换行符解析 CSV【英文标题】:Bash: Parse CSV with quotes, commas and newlines 【发布时间】:2016-07-17 05:25:16 【问题描述】:假设我有以下 csv 文件:
id,message,time
123,"Sorry, This message
has commas and newlines",2016-03-28T20:26:39
456,"It makes the problem non-trivial",2016-03-28T20:26:41
我想编写一个只返回时间列的 bash 命令。即
time
2016-03-28T20:26:39
2016-03-28T20:26:41
最直接的方法是什么?您可以假设标准 unix utils 的可用性,例如 awk、gawk、cut、grep 等。
注意 "" 的存在,它会转义,而换行符会用
进行微不足道的尝试cut -d , -f 3 file.csv
徒劳的。
【问题讨论】:
使用带有真正 CSV 解析器的语言,而不是bash
。
我完全同意@chepner。对于这个任务,我会使用 Python
或 Ruby
而不是 Bash。
@chepner 不久前,bash 提供了一个 CSV 解析器作为 可加载内置!见How to parse CSV in bash
【参考方案1】:
作为chepner said,我们鼓励您使用能够解析csv的编程语言。
下面是python中的一个例子:
import csv
with open('a.csv', 'rb') as csvfile:
reader = csv.reader(csvfile, quotechar='"')
for row in reader:
print(row[-1]) # row[-1] gives the last column
【讨论】:
查看 OP 的问题。他在引号中有换行符。 awk 不会考虑这个 @realspirituals 是的,没错。我已将awk
替换为python
。
小修正 - 文件应该以“文本模式”而不是“二进制模式”打开 - 在 open() 中使用 rt
而不是 rb
。【参考方案2】:
正如here所说的
gawk -v RS='"' 'NR % 2 == 0 gsub(/\n/, "") printf("%s%s", $0, RT) ' file.csv \
| awk -F, 'print $NF'
使用GNU awk
(对于RT
)专门处理双引号字符串中的换行符,并保留它们之外的换行符:
gawk -v RS='"' 'NR % 2 == 0 gsub(/\n/, "") printf("%s%s", $0, RT) ' file
这是通过沿"
字符拆分文件并在每个其他块中删除换行符来实现的。
输出
time
2016-03-28T20:26:39
2016-03-28T20:26:41
然后使用awk拆分列并显示最后一列
【讨论】:
【参考方案3】:CSV 是一种需要适当解析器的格式(即不能单独使用正则表达式进行解析)。如果您安装了Python,请使用csv
module 而不是普通的 BASH。
如果没有,请考虑csvkit,它有很多强大的工具可以从命令行处理 CSV 文件。
另见:
https://unix.stackexchange.com/questions/7425/is-there-a-robust-command-line-tool-for-processing-csv-files【讨论】:
FWIW,一个 csv 可能可以用正则表达式解析,但这肯定会很痛苦。 它令人讨厌的部分原因是 csv 实际上是一个松散的方言家族,而且很难找到一个适合所有变体的所有正则表达式。【参考方案4】:另一个使用 FS 的 awk
替代方案
$ awk -F'"' '!(NF%2)getline remainder;$0=$0 OFS remainder
NR>1sub(/,/,"",$NF); print $NF' file
2016-03-28T20:26:39
2016-03-28T20:26:41
【讨论】:
【参考方案5】:我在尝试处理 lspci -m 输出时遇到了类似的情况,但是需要首先转义嵌入的换行符(尽管 IFS=,应该在这里工作,因为它滥用了 bash 的引号评估)。 这是一个例子
f:13.3 "System peripheral" "Intel Corporation" "Xeon E7 v4/Xeon E5 v4/Xeon E3 v4/Xeon D Memory Controller 0 - Channel Target Address Decoder" -r01 "Super Micro Computer Inc" "Device 0838"
我能找到的将它带入 bash 的唯一合理方法是:
# echo 'f:13.3 "System peripheral" "Intel Corporation" "Xeon E7 v4/Xeon E5 v4/Xeon E3 v4/Xeon D Memory Controller 0 - Channel Target Address Decoder" -r01 "Super Micro Computer Inc" "Device 0838"' | eval array=($(cat)); declare -p array;
declare -a array='([0]="f:13.3" [1]="System peripheral" [2]="Intel Corporation" [3]="Xeon E7 v4/Xeon E5 v4/Xeon E3 v4/Xeon D Memory Controller 0 - Channel Target Address Decoder" [4]="-r01" [5]="Super Micro Computer Inc" [6]="Device 0838")'
#
不是一个完整的答案,但可能会有所帮助!
【讨论】:
“合理”,前提是您信任您的数据。读取为"Super 1337 $(rm -rf ~) Hey I Changed Your DMI Info"
的硬件将导致糟糕的时期。
当然.. 任何带有 'eval' 的 bash 评论都应该被标记为 'caveat emptor'【参考方案6】:
原版 bash 脚本
将此代码保存为 parse_csv.sh,赋予其执行权限 (chmod +x parse_csv.sh)
#!/bin/bash
# vim: ts=4 sw=4 hidden nowrap
# @copyright Copyright © 2021 Carlos Barcellos <carlosbar at gmail.com>
# @license https://www.gnu.org/licenses/lgpl-3.0.en.html
if [ "$1" = "-h" -o "$1" = "--help" -o "$1" = "-v" ]; then
echo "parse csv 0.1"
echo ""
echo "parse_csv.sh [csv file] [delimiter]"
echo " csv file csv file to parse; default stdin"
echo " delimiter delimiter to use. default is comma"
exit 0
fi
delim=,
if [ $# -ge 1 ]; then
[ -n "$1" ] && file="$1"
[ -n "$2" -a "$2" != "\"" ] && delim="$2"
fi
processLine()
if [[ ! "$1" =~ \" ]]; then
(
IFSS="$delim" fields=($1)
echo "$fields[@]"
)
return 0
fi
under_scape=0
fields=()
acc=
for (( x=0; x < $#1; x++ )); do
if [ "$1:x:1" = "$delim:0:1" -o $((x+1)) -ge $#1 ] && [ $under_scape -ne 1 ]; then
[ "$1:x:1" != "$delim:0:1" ] && acc="$acc$1:x:1"
fields+=($acc)
acc=
elif [ "$1:x:1" = "\"" ]; then
if [ $under_scape -eq 1 ] && [ "$1:x+1:1" = "\"" ]; then
acc="$acc$1:x:1"
else
under_scape=$((!under_scape))
fi
[ $((x+1)) -ge $#1 ] && fields+=($acc)
else
acc="$acc$1:x:1"
fi
done
echo "$fields[@]"
return 0
while read -r line; do
processLine "$line"
done < $file:-/dev/stdin
然后使用:parse_csv.sh“csv 文件”。要仅打印最后一列,您可以将 echo "$fields[@]" 更改为 echo "$fields[-1]"
【讨论】:
【参考方案7】:Perl 来救援!使用Text::CSV_XS 模块处理CSV。
perl -MText::CSV_XS=csv -we 'csv(in => $ARGV[0],
on_in => sub $_[1] = [ $_[1][-1] ] )
' -- file.csv
csv
子例程处理 csv
in
指定输入文件,$ARGV[0]
包含第一个命令行参数,即file.csv
这里
on_in
指定要运行的代码。它获取当前行作为第二个参数,即$_[1]
。我们只是将整行设置为最后一列的内容。
【讨论】:
【参考方案8】:我觉得你想多了。
$: echo time; grep -Eo '[0-9]4-[0-9]2-[0-9]2T[0-9]2:[0-9]2:[0-9]2$' file
time
2016-03-28T20:26:39
2016-03-28T20:26:41
如果你想检查那个逗号只是为了确定,
$: echo time; sed -En '/,[0-9]4-[0-9]2-[0-9]2T[0-9]2:[0-9]2:[0-9]2$/ s/.*,//; p; ' file
time
2016-03-28T20:26:39
2016-03-28T20:26:41
【讨论】:
【参考方案9】:csvquote 正是为这种事情而设计的。它(可逆地)清理文件并允许 awk 依赖逗号作为字段分隔符和换行符作为记录分隔符。
【讨论】:
【参考方案10】:sed -e 's/,/\n/g' file.csv | egrep ^201[0-9]-
【讨论】:
【参考方案11】:awk -F, '!/This/print $NF' file
time
2016-03-28T20:26:39
2016-03-28T20:26:41
【讨论】:
以上是关于Bash:使用引号、逗号和换行符解析 CSV的主要内容,如果未能解决你的问题,请参考以下文章