2016-07-20 5 views
-2

Примечание редактора: Я уточнил определение проблемы, потому что я думаю, что проблема интересная, и этот вопрос заслуживает того, чтобы его вновь открывали.Bash: преобразовать строки ключа в формат CSV

У меня есть текстовый файл, содержащий ключ-значение строки в следующем формате - к сведению, что # линии ниже, только там, чтобы показать повторяющиеся блоки и не являются частью ввода:

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 

Использование команд оболочки и утилит, как я могу преобразовать такой файл в формат 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 заголовок. Однако я не знаю, как переместить заголовки из одного столбца в несколько отдельных.

+3

Совет: покажите нам какой-нибудь код. – agc

+6

К сожалению, вы забыли свой код? StackOverflow помогает людям исправить свой код. Это не бесплатный сервис кодирования. Любой код лучше, чем никакого кода. Мета-код, даже, продемонстрирует, как вы думаете, что программа должна работать, даже если вы не знаете, как ее написать. – ghoti

+0

Редактировать: Моя идея состоит в том, чтобы отделить записи, например. Страна: Франция станет страной Франция, а затем grep/sed заголовок. Однако я не знаю, как переместить заголовки из одного столбца в несколько отдельных, не нарушая порядок записей в списке. – Duzy

ответ

4

Простое решение с cut, paste и head (предполагается, что входной файл 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 извлекает первые : на основе f ield из каждой строки ввода, а 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).

В то время как предыдущее решение теперь параметризованное, он все еще предполагает, что счетчик столбца известен заранее; следующее решение динамически определяет кол-столбец (требуется Bash 4+ за счет использования readarray, но могут быть сделаны для работы с Bash 3.х):

# 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 } выходы столбцов каждой входной строки в name (1-е место :), запоминает имена столбцов в ассоциативном массиве seen и останавливается при первом имени столбца, которое отображается для второго времени.

  • readarray -t columnHeaders читает выходную линию awk «сек по линии в массив columnHeaders

  • (IFS=','; echo "${columnHeaders[*]}") >out.csv печатает элементы массива, используя пробел в качестве разделителя (указанный с помощью $IFS); обратите внимание на использование подоболочки ((...)), чтобы локализовать эффект модификации $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 
+1

Это прекрасно. Отлично сработано. –

+0

Не совсем совершенный, hardcoded * 3 * в 'head -n 3' предполагает, что мы знаем, сколько полей есть, как и' paste -d, - - -', который используется * дважды *. – agc

+1

@agc: см. Мое обновление. – mklement0

1

Используя datamash, tr и join:

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, nl, sort, cut, tr и sed):

unsort=$({ IFS=: read a b; m=$a ; echo "$m"; while IFS=: read a b ; 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 ':' ',' 
+0

'datamash' - это нестандартная утилита GNU [здесь] (https://www.gnu.org/software/datamash/). Однако, несмотря на то, что указывает страница загрузки, установка на Ubuntu (по крайней мере, 14.04), с 'sudo apt-get install datamash' не сработала для меня. – mklement0

+0

@ mklement0, попробуйте v1.06 [здесь] (https://www.gnu.org/software/datamash/download/#gnulinux) – agc

+1

Спасибо; учитывая, что 'datamash' не является стандартной утилитой, я предлагаю вам добавить объяснение вашего ответа. – mklement0

1

Мой Баш скрипт для этого будет:

#!/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.

+0

Жизнеспособное решение, но, скорее всего, на медленной стороне для больших входных файлов, потому что петли 'bash' по своей сути медленны (особенно с вызовами внешних утилит на каждой итерации, что, однако, не является здесь случаем, потому что' printf' является 'bash' _builtin_). – mklement0

+0

@ mklement0: Спасибо. IIRC the '((..))' первоначально был функцией ksh, которая была перенесена в 'bash 2.0'. Поддерживают ли другие оболочки? – sjsam

+0

В дополнение к 'ksh' и' bash', 'zsh' поддерживает' ((...)) 'слишком; не уверен в других. – mklement0

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" 

Существует множество способов решения проблемы. Вам нужно будет поэкспериментировать, чтобы найти наиболее эффективный размер вашего файла данных и т. Д. Независимо от того, используете ли вы скрипт или комбинацию инструментов оболочки, cut, paste и т. Д. В значительной степени оставлены вам.

Выходной

$ 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 
+0

Этот код приписывает часть столбцов проблемы, но требует, чтобы пользователь знал количество повторяющихся строк (как * $ 2 *). – agc

+0

Да, это можно преодолеть, просто путем сохранения строки (или первой части строки в массиве) и сравнения. Обычно это делается в отдельном цикле, который предшествует приведенному выше коду. Я думал об этом, но это бы омрачило прямой ответ. –

Смежные вопросы