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

时间:2016-07-20 01:59:55

标签: linux bash csv awk sed

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

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

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标题行的列名称。
  • 使每个块的值成为数据行。

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

4 个答案:

答案 0 :(得分:4)

包含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在3行后停止,因为标题重复每3行。

    • paste -d, - - -从stdin获取3个输入行(每个-一个)并将它们组合成一个逗号分隔的输出行(-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 }输出每个输入行的列名(第一个: - 分隔字段),记住关联数组seen中的列名,并在第一列名称处停止 second 时间。

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

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

  • cut ...管道使用与以前相同的方法,paste的操作数基于数组columnHeaders的元素数创建({{1} })。

将上述内容包含在输出到stdout的函数中,也适用于Bash 3.x

${#columnHeaders[@]}

答案 1 :(得分:1)

使用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命令。这个令人讨厌的前置单行(修订版待定,无需解包)是第一次尝试自动化 unsort rev的散列,{{ 1}},nlsortcuttr):

sed

答案 2 :(得分:1)

我的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 ))test替换为double parenthesis Public Sub Command4_Click() Dim myProfiling As Recordset Set myProfiling = CurrentDb.OpenRecordset("Profiling") varChangePicture = Forms!sfrChangeProfilePics!FileName.value DoCmd.Close Forms![Main Form].[Crafter Default].Form!sfrProfiling.Form!pic.value=varChangePicture End Sub

答案 3 :(得分:0)

您还可以编写一个稍微更通用的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