【问题标题】:Bash: transform key-value lines to CSV format [closed]Bash:将键值行转换为 CSV 格式 [关闭]
【发布时间】:2016-11-23 02:13:37
【问题描述】:

编者注:我已经澄清了问题的定义,因为我认为这个问题很有趣,这个问题值得重新讨论。

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

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's original] 编辑:我的想法是将条目分开,例如Country:France 将变为 Country France,然后 grep/sed 标题。但是我不知道如何将标题从单列移动到几个单独的列。

【问题讨论】:

  • 建议:给我们看一些代码。
  • 糟糕,您忘记发布代码了! StackOverflow 旨在帮助人们修复他们的代码。这不是免费的编码服务。任何代码都比没有代码好。元代码甚至可以展示您认为程序应该如何工作,即使您不知道如何编写它。
  • 编辑:我的想法是将条目分开,例如Country:France 将变为 Country France,然后 grep/sed 标题。但是我不知道如何将标题从单个列移动到几个单独的列,而不会弄乱列表中条目的顺序。
  • 你想使用 awk。

标签: linux bash csv awk sed


【解决方案1】:

使用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, - - - 从标准输入获取 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 中的列名,并在看到的第一个列名处停止em>秒时间。

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

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

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


要将以上内容封装在一个输出到标准输出的函数中,并且也适用于 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

【讨论】:

  • 太完美了。干得好。
  • 不是很完美,head -n 3 中的硬编码 3 假设我们知道有多少字段,paste -d, - - - 也是如此,它被使用了 两次.
  • @agc:请看我的更新。
  • 这一切都做到了。
  • 这对我有用,非常感谢您如此详尽的回答!
【解决方案2】:

我的 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,用于arithmetic expansion

【讨论】:

  • 一个可行的解决方案,但对于大型输入文件可能会很慢,因为bash 循环本身就很慢(尤其是在每次迭代中调用外部实用程序时,然而,这是不是这里的情况,因为printfbash内置)。
  • @mklement0:谢谢。 IIRC ((..)) 最初是移植到 bash 2.0 的 ksh 功能。其他 shell 支持这个吗?
  • 除了kshbash之外,zsh也支持(( ... ));不确定其他人。
  • read line 替换为IFS=':' read line data"${line##*:}" 的两个参数扩展都可以替换为"$data"
【解决方案3】:

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

【讨论】:

  • 这段代码解决了问题的列部分,但要求用户知道重复行的数量(如 $2)。
  • 是的,这也可以通过存储行(或数组中行的第一部分)并进行比较来克服。这通常在上面代码之前的单独循环中完成。我想了想,但这会使直接答案蒙上阴影。
【解决方案4】:

使用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

【讨论】:

    猜你喜欢
    • 2011-05-07
    • 1970-01-01
    • 1970-01-01
    • 2019-05-28
    • 2016-07-02
    • 1970-01-01
    • 2022-01-22
    • 1970-01-01
    相关资源
    最近更新 更多