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。对于这个任务,我会使用 PythonRuby 而不是 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的主要内容,如果未能解决你的问题,请参考以下文章

Ruby CSV写和读示例,字段中有引号换行符和逗号

如何用Java解析CSV文件

CSV 到数据表

CSV文件格式要求

导出csv文件示例

使用 awk 有效解析 CSV 的最可靠方法是啥?