Как напечатать определенные столбцы по имени?

32

У меня есть следующий файл:

id  name  age
1   ed    50
2   joe   70   

Я хочу напечатать только idи ageстолбцы. Прямо сейчас я просто использую awk:

cat file.tsv | awk '{ print $1, $3 }'

Однако для этого необходимо знать номера столбцов. Есть ли способ сделать это, где я могу использовать имя столбца (указано в первой строке), а не номер столбца?

Бретт Томас
источник
7
catне обязательно, кстати. Вы могли бы использоватьawk '{ print $1, $3 }' file.tsv
Эрик Уилсон
Если не номер столбца , то от чего бы вы хотели зависеть?
rozcietrzewiacz
2
@rozcietrzewiacz Имя; он хочет сказать idвместо $1и ageвместо$3
Михаил Мрозек
Смотри также обсуждение на StackOverflow
Hotschke

Ответы:

37

Может быть, что-то вроде этого:

$ cat t.awk
NR==1 {
    for (i=1; i<=NF; i++) {
        ix[$i] = i
    }
}
NR>1 {
    print $ix[c1], $ix[c2]
}
$ awk -f t.awk c1=id c2=name input 
1 ed
2 joe
$ awk -f t.awk c1=age c2=name input 
50 ed
70 joe

Если вы хотите указать столбцы для печати в командной строке, вы можете сделать что-то вроде этого:

$ cat t.awk 
BEGIN {
    split(cols,out,",")
}
NR==1 {
    for (i=1; i<=NF; i++)
        ix[$i] = i
}
NR>1 {
    for (i in out)
        printf "%s%s", $ix[out[i]], OFS
    print ""
}
$ awk -f t.awk -v cols=name,age,id,name,id input 
ed 1 ed 50 1 
joe 2 joe 70 2 

(Обратите внимание на -vпереключатель, чтобы получить переменную, определенную в BEGINблоке.)

Мат
источник
Я откладывал изучение awk ... каков наилучший способ поддержки переменного числа столбцов? awk -f t.awk col1 col2 ... coln inputбыло бы идеально; awk -f t.awk cols=col1,col2,...,coln inputбудет работать тоже
Бретт Томас
1
Обновил мой ответ. Прекратите откладывать изучение этого, если вы хотите что-то с этим делать :)
Мат
3
2-й пример не выводит столбцы в ожидаемом порядке, for (i in out)не имеет внутреннего порядка. gawkпредлагает PROCINFO["sorted_in"]в качестве решения, итерации по индексу с, for( ; ; )вероятно, лучше.
mr.spuratic
@BrettThomas, очень рекомендую этот урок . (Если у вас есть доступ к lynda.com, я еще больше рекомендую «Awk Essential Training», который охватывает все тот же материал, но более кратко и с практическими упражнениями.)
Wildcard
Мистер Спуратик, вы, да, чувак. Я столкнулся с проблемой for (i in out), работал отлично с 3 полями, когда я добавил 2, он сделал 4,5,1,2,3 вместо 1,2,3,4,5, как я ожидал , Чтобы привести их в порядок, вы должны сделать для (i = 1; i <= length (out); i ++)
Severun
5

Просто добавим Perl-решение в пакет:

#!/usr/bin/perl -wnla

BEGIN {
    @f = ('id', 'age');   # field names to print
    print "@f";           # print field names
}

if ($. == 1) {            # if line number 1
    @n = @F;              #   get all field names
} else {                  # or else
    @v{@n} = @F;          #   map field names to values
    print "@v{@f}";       #   print values based on names
}
Питер Джон Аклам
источник
5

csvkit

Преобразуйте входные данные в формат CSV и используйте инструмент CSV, например csvcutиз csvkit:

$ cat test-cols.dat 
id  name  age
1   ed    50
2   joe   70 

Установите csvkit:

$ pip install csvkit

Используйте trего с параметром squeeze, -sчтобы преобразовать его в действительный файл CSV и примените csvcut:

$ cat test-cols.dat | tr -s ' ' ',' | csvcut -c id,age
id,age
1,50
2,70

Если вы хотите вернуться к старому формату данных, вы можете использовать tr ',' ' ' | column -t

$ cat test-cols.dat | tr -s ' ' ',' | csvcut -c id,age | tr ',' ' ' | column -t
id  age
1   50
2   70

Заметки

  • csvkit также поддерживает разные разделители ( общая опция -d или --delimiter), но возвращает файл csv:

    • Если файл использует только пробелы для разделения столбцов (без вкладок вообще), следующие работы

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Если файл использует вкладку для разделения столбцов, следующие действия и csvformatмогут быть использованы для возврата файла TSV:

      $ csvcut -t -c 'id,age' test-cols.dat | csvformat -T
      id  age
      1   50
      2   70

      Насколько я проверил, допускается только одна вкладка.

  • csvlook можно отформатировать таблицу в формате уценки

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (Бесполезное использование кошки) : мне нравится этот способ создания команды.

Hotschke
источник
+1. Но ненужное использование trтоже. Файлы TSV поддерживаются напрямую, без необходимости конвертировать их в CSV. Опция -t(aka --tabs) говорит cvscutиспользовать вкладки в качестве разделителя полей. И -dили --delimiterиспользовать любой символ в качестве разделителя.
Cas
С некоторым тестированием, кажется, -dи -tварианты пола разбитым. они работают для указания входного разделителя, но выходной разделитель жестко закодирован, чтобы всегда быть запятой. IMO не работает - он должен быть таким же, как входной разделитель, или иметь другую опцию, позволяющую пользователю установить выходной разделитель, например awk, FS и OFS.
Cas
4

Если вы просто хотите ссылаться на эти поля по именам, а не по номерам, вы можете использовать read:

while read id name age
do
  echo "$id $age"
done < file.tsv 

РЕДАКТИРОВАТЬ

Наконец-то я понял твой смысл! Вот функция bash, которая распечатывает только те столбцы, которые вы указали в командной строке (по имени ).

printColumns () 
{ 
read names
while read $names; do
    for col in $*
    do
        eval "printf '%s ' \$$col"
    done
    echo
done
}

Вот как вы можете использовать его с файлом, который вы представили:

$ < file.tsv printColumns id name
1 ed 
2 joe 

(Функция читает stdin. < file.tsv printColumns ... Эквивалентно printColumns ... < file.tsvи cat file.tsv | printColumns ...)

$ < file.tsv printColumns name age
ed 50 
joe 70 

$ < file.tsv printColumns name age id name name name
ed 50 1 ed ed ed 
joe 70 2 joe joe joe

Примечание: обратите внимание на названия столбцов, которые вы запрашиваете! В этой версии отсутствуют проверки работоспособности, поэтому могут произойти неприятные вещи, если один из аргументов"anything; rm /my/precious/file"

rozcietrzewiacz
источник
1
Это также требует знания номеров столбцов. Просто потому , что вы называете их id, nameи age, не меняет тот факт , что порядок жестко закодирован в вашей readлинии.
Янмезен
1
@janmoesen Да, я наконец-то понял: :)
rozcietrzewiacz
Это хорошо, спасибо. Я работаю с большими файлами (1000 столбцов, миллионы строк), поэтому я использую awk для скорости.
Бретт Томас
@BrettThomas О, я вижу. Мне очень любопытно: не могли бы вы опубликовать какой-нибудь тест, который дает сравнение времени? (Использовать time { command(s); }).
rozcietrzewiacz
@rozceitrewaicz:time cat temp.txt | ./col1 CHR POS > /dev/null 99.144u 38.966s 2:19.27 99.1% 0+0k 0+0io 0pf+0w time awk -f col2 c1=CHR c2=POS temp.txt > /dev/null 0.294u 0.127s 0:00.50 82.0% 0+0k 0+0io 0pf+0w
Бретт Томас
3

Для чего это стоит. Это может обрабатывать любое количество столбцов в источнике и любое количество столбцов для печати в любой выходной последовательности, которую вы выберете; просто переставить арги ...

например. вызов:script-name id age

outseq=($@)
colnum=($( 
  for ((i; i<${#outseq[@]}; i++)) ;do 
    head -n 1 file |
     sed -r 's/ +/\n/g' |
      sed -nr "/^${outseq[$i]}$/="
  done ))
tr ' ' '\t' <<<"${outseq[@]}"
sed -nr '1!{s/ +/\t/gp}' file |
  cut -f $(tr ' ' ','<<<"${colnum[@]}") 

выход

id      age
1       50
2       70
Peter.O
источник
2

Если файл, который вы читаете, никогда не может быть сгенерирован пользователем, вы можете использовать встроенную функцию чтения:

f=file.tsv
read $(head -n1 "$f") extra <<<`seq 100`
awk "{print \$$id, \$$age}" "$f"

Вся первая строка входного файла подставляется в список аргументов, поэтому readвсе имена полей из строки заголовка передаются как имена переменных. Первому из них присваивается 1, который seq 100генерирует, второму - 2, третьему - 3 и так далее. Избыточный seqвывод поглощается фиктивной переменной extra. Если вы знаете количество входных столбцов заблаговременно, вы можете изменить 100, чтобы соответствовать и избавиться отextra .

awkСценарий представляет собой двойные кавычки, позволяя переменные оболочки , определенный readбыть замещены в сценарий в качестве awkномера поля.

flabdablet
источник
1

Обычно проще взглянуть на заголовок файла, сосчитать номер нужного вам столбца ( c ) и затем использовать Unix cut:

cut -f c -d, file.csv

Но когда есть много столбцов или много файлов, я использую следующую уродливую уловку:

cut \
  -f $(head -1 file.csv | sed 's/,/\'$'\n/g' | grep -n 'column name' | cut -f1 -d,) \
  -d, \ 
  file.csv

Протестировано на OSX, file.csvразделено запятыми.

SRK
источник
1

Вот один быстрый способ выбора одного столбца.

Скажем, мы хотим столбец с именем "foo":

f=file.csv; colnum=`head -1 ${f} | sed 's/,/\n/g' | nl | grep 'foo$' | cut -f 1 `; cut -d, -f ${colnum} ${f}

В основном, возьмите строку заголовка, разделите ее на несколько строк с одним именем столбца на строку, пронумеруйте строки, выберите строку с нужным именем и получите соответствующий номер строки; затем используйте этот номер строки в качестве номера столбца для команды вырезания.

jdjensen
источник
0

Ища подобное решение (мне нужен столбец с именем id, который может иметь различный номер столбца), я наткнулся на это:

head -n 1 file.csv | awk -F',' ' {
      for(i=1;i < NF;i++) {
         if($i ~ /id/) { print i }
      }
} '
Huib te Pas
источник
0

Я написал скрипт Python для этой цели, который в основном работает так:

with fileinput.input(args.file) as data:
    headers = data.readline().split()
    selectors = [any(string in header for string in args.fixed_strings) or
                 any(re.search(pat, header) for pat in args.python_regexp)
                 for header in headers]

    print(*itertools.compress(headers, selectors))
    for line in data:
        print(*itertools.compress(line.split(), selectors))

Я назвал его hgrepдля заголовка grep , его можно использовать так:

$ hgrep data.txt -F foo bar -P ^baz$
$ hgrep -F foo bar -P ^baz$ -- data.txt
$ grep -v spam data.txt | hgrep -F foo bar -P ^baz$

Весь скрипт немного длиннее, потому что он использует argparseдля разбора аргументов командной строки и код выглядит следующим образом:

#!/usr/bin/python3

import argparse
import fileinput
import itertools
import re
import sys
import textwrap


def underline(s):
    return '\033[4m{}\033[0m'.format(s)


parser = argparse.ArgumentParser(
    usage='%(prog)s [OPTIONS] {} [FILE]'.format(
        underline('column-specification')),
    description=
        'Print selected columns by specifying patterns to match the headers.',
    epilog=textwrap.dedent('''\
    examples:
      $ %(prog)s data.txt -F foo bar -P ^baz$
      $ %(prog)s -F foo bar -P ^baz$ -- data.txt
      $ grep -v spam data.txt | %(prog)s -F foo bar -P ^baz$
    '''),
    formatter_class=argparse.RawTextHelpFormatter,
)

parser.add_argument(
    '-d', '--debug', action='store_true', help='include debugging information')
parser.add_argument(
    'file', metavar='FILE', nargs='?', default='-',
    help="use %(metavar)s as input, default is '-' for standard input")
spec = parser.add_argument_group(
    'column specification', 'one of these or both must be provided:')
spec.add_argument(
    '-F', '--fixed-strings', metavar='STRING', nargs='*', default=[],
    help='show columns containing %(metavar)s in header\n\n')
spec.add_argument(
    '-P', '--python-regexp', metavar='PATTERN', nargs='*', default=[],
    help='show a column if its header matches any %(metavar)s')

args = parser.parse_args()

if args.debug:
    for k, v in sorted(vars(args).items()):
        print('{}: debug: {:>15}: {}'.format(parser.prog, k, v),
              file=sys.stderr)

if not args.fixed_strings and not args.python_regexp:
    parser.error('no column specifications given')


try:
    with fileinput.input(args.file) as data:
        headers = data.readline().split()
        selectors = [any(string in header for string in args.fixed_strings) or
                     any(re.search(pat, header) for pat in args.python_regexp)
                     for header in headers]

        print(*itertools.compress(headers, selectors))
        for line in data:
            print(*itertools.compress(line.split(), selectors))

except BrokenPipeError:
    sys.exit(1)
except KeyboardInterrupt:
    print()
    sys.exit(1)
arekolek
источник
0

awkпри всей своей винтажности, по сути, имеет целочисленный индекс, как есть cut.

Вот несколько инструментов, разработанных для обработки именованных данных (большинство из них обрабатывают только CSV и TSV, которые являются очень популярными форматами файлов):

Джон Керл
источник
0

Попробуйте эту маленькую утилиту awk, чтобы вырезать определенные заголовки - https://github.com/rohitprajapati/toyeca-cutter

Пример использования -

awk -f toyeca-cutter.awk -v c="col1, col2, col3, col4" my_file.csv
toyeca
источник