Bash:将键值行转换为CSV格式

编者按:我已经澄清了问题的定义,因为我觉得这个问题很有意思,这个问题值得重新打开。

我有一个文本文件,其中包含以下格式的键值行 – 请注意,下面的#行仅用于显示重复块,不是input的一部分

 Country:United Kingdom Language:English Capital city:London # Country:France Language:French Capital city:Paris # Country:Germany Language:German Capital city:Berlin # Country:Italy Language:Italian Capital city:Rome # Country:Russia Language:Russian Capital city:Moscow 

使用shell命令和实用程序,我怎样才能将这样的文件转换为CSV格式,所以它看起来像这样?

 Country,Language,Capital city United Kingdom,English,London France,French,Paris Germany,German,Berlin Italy,Italian,Rome Russia,Russian,Moscow 

换一种说法:

  • 将键名称设置为CSV标题行的列名称。
  • 使每个块的值分别为数据行。

编辑:我的想法是将条目分开,例如:国家:法国将成为国家法国,然后grep / sed标题。 但是,我不知道如何将标题从一个列移动到几个单独的列。

Solutions Collecting From Web of "Bash:将键值行转换为CSV格式"

一个简单的解决方案, cutpastehead (假设输入文件的file ,输出到文件out.csv ):

 #!/usr/bin/env bash { cut -d':' -f1 file | head -n 3 | paste -d, - - -; cut -d':' -f2- file | paste -d, - - -; } >out.csv 
  • cut -d':' -f1 file | head -n 3 cut -d':' -f1 file | head -n 3创建标题行:

    • cut -d':' -f1 file从每个输入行提取第一个: -based字段,并且head -n 3在3行之后停止,因为头每3行重复一次。

    • paste -d, - - -从标准输入(每个输入一行paste -d, - - - )3个输入行,并将它们组合到一个逗号分隔的输出行( -d,

  • cut -d':' -f2- file | paste -d, - - - cut -d':' -f2- file | paste -d, - - -创建数据线:

    • cut -d':' -f2- file从每个输入行提取:之后的所有内容。

    • 如上所述, paste然后将3个值组合成一个逗号分隔的输出行。


agc在评论中指出列计数( 3 )和paste操作数( - - - )是上面硬编码的

以下解决方案将列计数参数化 (通过n=... ):

 { n=3; pasteOperands=$(printf '%.s- ' $(seq $n)) cut -d':' -f1 file | head -n $n | paste -d, $pasteOperands; cut -d':' -f2- file | paste -d, $pasteOperands; } >out.csv 
  • printf '%.s- ' $(seq $n)是一个能产生尽可能多空格分隔字符的列表。 因为有列( $n )。

虽然之前的解决方案现在已经参数化了,但是仍然假设列计数是预先知道的; 以下解决方案动态确定列数 (由于使用readarray ,需要Bash 4+,但可以使用Bash 3.x):

 # Determine the unique list of column headers and # read them into a Bash array. readarray -t columnHeaders < <(awk -F: 'seen[$1]++ { exit } { print $1 }' file) # Output the header line. (IFS=','; echo "${columnHeaders[*]}") >out.csv # Append the data lines. cut -d':' -f2- file | paste -d, $(printf '%.s- ' $(seq ${#columnHeaders[@]})) >>out.csv 
  • awk -F: 'seen[$1]++ { exit } { print $1 }输出每个输入行的列名称(1st : -separated字段),记住关联数组中的列名,并停在第一列名被第二次看到。

  • readarray -t columnHeadersawk的输出逐行读入数组columnHeaders

  • (IFS=','; echo "${columnHeaders[*]}") >out.csv使用空格作为分隔符(通过$IFS指定(IFS=','; echo "${columnHeaders[*]}") >out.csv打印数组元素; 请注意使用subshel​​l( (...) )以便本地化修改$IFS的效果,否则这将会产生全局效果。

  • cut ... pipeline使用与之前相同的方法,根据数组columnHeaders${#columnHeaders[@]} )的元素计数创建paste的操作数。


将上面的代码封装到一个输出到stdout的函数中 ,并且可以和Bash 3.x一起使用

 toCsv() { local file=$1 columnHeaders # Determine the unique list of column headers and # read them into a Bash array. IFS=$'\n' read -d '' -ra columnHeaders < <(awk -F: 'seen[$1]++ { exit } { print $1 }' "$file") # Output the header line. (IFS=','; echo "${columnHeaders[*]}") # Append the data lines. cut -d':' -f2- "$file" | paste -d, $(printf '%.s- ' $(seq ${#columnHeaders[@]})) } # Sample invocation toCsv file > out.csv 

我的这个bash脚本是:

 #!/bin/bash count=0 echo "Country,Language,Capital city" while read line do (( count++ )) (( count -lt 3 )) && printf "%s," "${line##*:}" (( count -eq 3 )) && printf "%s\n" "${line##*:}" && (( count = 0 )) done<file 

产量

 Country,Language,Capital city United Kingdom,English,London France,French,Paris Germany,German,Berlin Italy,Italian,Rome Russia,Russian,Moscow 

编辑

(( stuff ))替换[ stuff ] ,即用用于算术扩展的 double parenthesis test

使用datamashtrjoin

 datamash -t ':' -s -g 1 collapse 2 < country.txt | tr ',' ':' | \ datamash -t ':' transpose | \ join -t ':' -a1 -o 1.2,1.3,1.1 - /dev/null | tr ':' ',' 

输出:

 Country,Language,Capital city United Kingdom,English,London France,French,Paris Germany,German,Berlin Italy,Italian,Rome Russia,Russian,Moscow 

上述代码的一个缺点,即datamash输出被排序 ,并且需要被使用硬编码join命令的未排序 (恢复到原来的顺序)。 这个令人讨厌的前置单行( 修订悬而未决,不需要解包 )是自动化unsortrevnlsortcuttrsed的哈希)的第一次尝试:

 unsort=$({ IFS=: read ab; m=$a ; echo "$m"; while IFS=: read ab ; do [ "$m" = "$a" ] && break ; echo $a ; done ; } < country.txt | rev | nl | rev | sort | rev | nl | sort -k2 | cut -f1 | tr -d '\n' | sed 's/ /1./;s/ /,1./g') datamash -t ':' -s -g 1 collapse 2 < country.txt | tr ',' ':' | \ datamash -t ':' transpose | \ join -t ':' -a1 -o $unsort - /dev/null | tr ':' ',' 

你也可以编写一个稍微更一般的bash脚本版本,它可以获取保存数据的重复行的数量,并在此基础上产生输出,以避免硬编码标题值和处理额外的字段。 (您也可以只扫描第一个重复的字段名称,并以这种方式设置重复行)。

 #!/bin/bash declare -i rc=0 ## record count declare -i hc=0 ## header count record="" header="" fn="${1:-/dev/stdin}" ## filename as 1st arg (default: stdin) repeat="${2:-3}" ## number of repeating rows (default: 3) while read -r line; do record="$record,${line##*:}" ((hc == 0)) && header="$header,${line%%:*}" if ((rc < (repeat - 1))); then ((rc++)) else ((hc == 0)) && { printf "%s\n" "${header:1}"; hc=1; } printf "%s\n" "${record:1}" record="" rc=0 fi done <"$fn" 

有许多方法可以解决这个问题。 您将不得不尝试找到最有效的数据文件大小等等。无论您使用脚本还是shell工具的组合, cutpaste等都在很大程度上由您决定。

产量

 $ bash readcountry.sh country.txt Country,Language,Capital city United Kingdom,English,London France,French,Paris Germany,German,Berlin Italy,Italian,Rome Russia,Russian,Moscow 

输出4个字段

添加“ Population字段的示例输入文件:

 $ cat country2.txt Country:United Kingdom Language:English Capital city:London Population:20000000 <snip> 

产量

 $ bash readcountry.sh country2.txt 4 Country,Language,Capital city,Population United Kingdom,English,London,20000000 France,French,Paris,10000000 Germany,German,Berlin,150000000 Italy,Italian,Rome,9830000 Russia,Russian,Moscow,622000000