Bash:使用引号、逗号和换行符解析 CSV

问题描述 投票:0回答:11

假设我有以下 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 实用程序的可用性,例如 awk、gawk、cut、grep 等。

注意转义的“”的存在,以及使用

进行简单尝试的换行符
cut -d , -f 3 file.csv

徒劳。

bash csv awk cut gawk
11个回答
19
投票

正如chepner所说,我们鼓励您使用能够解析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

7
投票

正如这里所说

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分割列并显示最后一列


6
投票

CSV 是一种需要适当解析器的格式(即不能单独使用正则表达式进行解析)。如果您安装了 Python,请使用

csv
模块 而不是普通的 BASH。

如果没有,请考虑 csvkit,它有很多强大的工具可以从命令行处理 CSV 文件。

另请参阅:


1
投票

csvcut
来自
csvkit
示例

csvkit 在以下位置提到:https://stackoverflow.com/a/36288388/895245 但这里是示例。

安装:

pip install csvkit

CSV 输入文件示例:

主.csv

a,"b
c",d
e,f

获取第一列:

csvcut -c 1 main.csv

输出:

a
e

或者获取第二列:

csvcut -c 1 main.csv

输出以下有效的单列 CSV:

"b
c"
f

或者交换两列:

csvcut -c 2,1 main.csv

输出另一个有效的 CSV 文件:

"b
c",a
f,e

在 Ubuntu 23.04 上测试,csvkit==1.1.1。

csv工具

这又是一件好事。它是编译后的可执行文件而不是 Python 脚本,因此对于大型数据集来说速度要快得多。如果您想打印没有转义的单列,那么

format
操作很有用,只要没有多行条目,这就会很有用,而
csvkit
似乎不支持。

安装:

sudo apt install csvtool

使用示例:

printf 'a,"b,c",d\ne,"f""g",h\n' | csvtool format '%(2)\n' -

输出:

b,c
f"g

另请参阅:如何提取 csv 文件的一列

在 Ubuntu 23.10、csvtool 2.4-3 上测试。


0
投票

使用 FS 的另一种

awk
替代方案

$ awk -F'"' '!(NF%2){getline remainder;$0=$0 OFS remainder}
                NR>1{sub(/,/,"",$NF); print $NF}' file

2016-03-28T20:26:39
2016-03-28T20:26:41

0
投票

在尝试处理 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")'
# 

不是完整的答案,但可能有帮助!


0
投票

原版 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]}"


0
投票

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]
    。我们只是将整行设置为最后一列的内容。

0
投票

我觉得你想多了。

$: echo time; grep -Eo '[0-9]{4}-[0-9]{2}-[0-9]{2}T[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]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/{ s/.*,//; p; }' file
time
2016-03-28T20:26:39
2016-03-28T20:26:41

0
投票

csvquote 正是为此类事情而设计的。它对文件进行消毒(可逆),并允许 awk 依赖逗号作为字段分隔符,将换行符作为记录分隔符。


-3
投票
awk -F, '!/This/{print $NF}' file

time
2016-03-28T20:26:39
2016-03-28T20:26:41
© www.soinside.com 2019 - 2024. All rights reserved.