Jak wydrukować niektóre kolumny według nazwy?


32

Mam następujący plik:

id  name  age
1   ed    50
2   joe   70   

Chcę wydrukować tylko kolumny idi age. Teraz używam po prostu awk:

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

Wymaga to jednak znajomości numerów kolumn. Czy istnieje sposób, aby to zrobić, w którym mogę użyć nazwy kolumny (określonej w pierwszym wierszu) zamiast numeru kolumny?


7
catnie jest konieczne, BTW. Możesz użyćawk '{ print $1, $3 }' file.tsv
Eric Wilson

Jeśli nie numer kolumny , to na czym chciałbyś polegać?
rozcietrzewiacz

2
@rozcietrzewiacz Nazwa; chce powiedzieć idzamiast $1i agezamiast$3
Michael Mrozek

zobacz także dyskusję na temat stackoverflow
Hotschke

Odpowiedzi:


37

Może coś takiego:

$ 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

Jeśli chcesz określić kolumny do wydrukowania w wierszu polecenia, możesz zrobić coś takiego:

$ 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 

(Zwróć uwagę na -vprzełącznik, aby uzyskać zmienną zdefiniowaną w BEGINbloku).


Odkładam naukę awk ... jaki jest najlepszy sposób na obsługę zmiennej liczby kolumn? awk -f t.awk col1 col2 ... coln inputbyłoby idealne; awk -f t.awk cols=col1,col2,...,coln inputteż by działał
Brett Thomas

1
Zaktualizowałem moją odpowiedź. Przestań odkładać naukę, jeśli chcesz coś z tym zrobić :)
Mat

3
Drugi przykład nie wyświetla kolumn w oczekiwanej kolejności, for (i in out)nie ma właściwej kolejności. gawkoferuje PROCINFO["sorted_in"]jako rozwiązanie, iteracja indeksu za pomocą a for( ; ; )jest prawdopodobnie lepsza.
mr. Spuratic

@BrettThomas, bardzo polecam ten samouczek . (Jeśli masz dostęp do lynda.com, jeszcze bardziej polecam „Awk Essential Training”, który obejmuje ten sam materiał, ale bardziej zwięźle i z ćwiczeniami.)
Wildcard

Panie Spuratic, ty człowieku. Natknąłem się na problem za (i wyjąłem), działałem dobrze w / 3 polach, kiedy dodałem 2, zrobiło to 4,5,1,2,3, zamiast 1,2,3,4,5, jak się spodziewałem . Aby je uporządkować, musisz zrobić dla (i = 1; i <= długość (out); i ++)
Severun

5

Wystarczy wrzucić rozwiązanie Perla do partii:

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

Konwersja danych wejściowych do formatu csv i użyć narzędzia takie jak csv csvcutz csvkit:

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

Zainstaluj csvkit:

$ pip install csvkit

Użyj trz opcją ściśnięcia, -saby przekonwertować go na prawidłowy plik csv i zastosować csvcut:

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

Jeśli chcesz wrócić do starego formatu danych, możesz użyć tr ',' ' ' | column -t

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

Notatki

  • csvkit obsługuje również różne ograniczniki ( opcja współdzielona -d lub --delimiter), ale zwraca plik csv:

    • Jeśli plik używa tylko spacji do oddzielenia kolumn (bez żadnych tabulatorów), następujące czynności

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Jeśli plik używa karty do oddzielenia kolumn, następujące prace csvformatmogą być użyte do odzyskania pliku tsv:

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

      O ile sprawdziłem, dozwolona jest tylko jedna karta.

  • csvlook może sformatować tabelę w formacie tabeli przecenionej

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (Useless Use Of Cat) : Podoba mi się w ten sposób, aby zbudować polecenie.


+1. Ale także niepotrzebne zastosowania tr. Pliki TSV są obsługiwane bezpośrednio, bez potrzeby konwertowania ich na CSV. Opcja -t(aka --tabs) mówi, cvscutaby używać tabulatorów jako ograniczników pól. I -dlub --delimiterużyć dowolnego znaku jako separatora.
cas

Z jakiegoś badania, wydaje się, że -di -topcje są pół złamane. działają, aby określić separator wejściowy, ale separator wyjściowy jest zakodowany na stałe, aby zawsze był przecinkiem. IMO, który jest zepsuty - powinien być taki sam jak separator wejściowy lub mieć inną opcję umożliwiającą użytkownikowi ustawienie separatora wyjściowego, np awk. Zmienne FS i OFS.
cas

4

Jeśli chcesz odwoływać się do tych pól według ich nazw zamiast cyfr, możesz użyć read:

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

EDYTOWAĆ

W końcu zrozumiałem twoje znaczenie! Oto funkcja bash, która wypisze tylko kolumny określone w wierszu poleceń (według nazwy ).

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

Oto jak możesz go użyć z prezentowanym plikiem:

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

(Funkcja czyta stdin. < file.tsv printColumns ... Jest równoważna z printColumns ... < file.tsvi 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

Uwaga: zwróć uwagę na nazwy żądanych kolumn! Ta wersja nie ma kontroli poprawności, więc mogą się zdarzyć nieprzyjemne rzeczy, jeśli jeden z argumentów jest podobny"anything; rm /my/precious/file"


1
Wymaga to również znajomości numerów kolumn. Tylko dlatego, że ich nazwa id, namea agenie zmienia faktu, że kolejność jest zakodowane w swojej readlinii.
janmoesen

1
@janmoesen Tak, wreszcie dostałem punkt :)
rozcietrzewiacz

To miło, dzięki. Pracuję z dużymi plikami (1000 kolumn, miliony wierszy), więc używam awk dla szybkości.
Brett Thomas,

@BrettThomas Oh rozumiem. Jestem więc bardzo ciekawy: czy mógłbyś opublikować jakiś test porównawczy, który da porównanie czasu? (Użyj 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
Brett Thomas

3

Tyle ile jest warte. Może to obsłużyć dowolną liczbę kolumn w źródle i dowolną liczbę kolumn do wydrukowania, niezależnie od wybranej sekwencji wyjściowej; po prostu przeorganizuj argumenty ...

na przykład. połączenie: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[@]}") 

wydajność

id      age
1       50
2       70

2

Jeśli plik, który czytasz, nigdy nie mógłby zostać wygenerowany przez użytkownika, możesz nadużyć wbudowanego odczytu:

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

Cały pierwszy wiersz pliku wejściowego jest podstawiany na listę argumentów, więc readwszystkie nazwy pól z wiersza nagłówka są przekazywane jako nazwy zmiennych. Pierwszy z nich otrzymuje 1, który seq 100generuje, drugi otrzymuje 2, trzeci otrzymuje 3 i tak dalej. Nadmiar seqwyjściowy jest pochłaniany przez zmienną fikcyjną extra. Jeśli znasz liczbę kolumn wejściowych z wyprzedzeniem, możesz zmienić 100, aby dopasować i się go pozbyć extra.

awkSkryptu jest dwukrotnie podane łańcuch, co pozwala na zmienne powłoki zdefiniowane readmogą być podstawione do scenariusza jako awkliczby pól.


1

Zwykle łatwiej jest spojrzeć na nagłówek pliku, policzyć potrzebną kolumnę ( c ), a następnie użyć Unixa cut:

cut -f c -d, file.csv

Ale kiedy jest wiele kolumn lub plików, używam następującej brzydkiej sztuczki:

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

Testowany na OSX file.csvjest rozdzielany przecinkami.


1

Oto jeden szybki sposób na wybranie pojedynczej kolumny.

Powiedzmy, że chcemy kolumny o nazwie „foo”:

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

Zasadniczo weź wiersz nagłówka, podziel go na wiele wierszy z jedną nazwą kolumny na wiersz, ponumeruj linie, wybierz wiersz o żądanej nazwie i odszukaj powiązany numer wiersza; następnie użyj tego numeru wiersza jako numeru kolumny dla polecenia cięcia.


0

Szukając podobnego rozwiązania (potrzebuję kolumny o nazwie id, która może mieć różny numer kolumny), natknąłem się na tę:

head -n 1 file.csv | awk -F',' ' {
      for(i=1;i < NF;i++) {
         if($i ~ /id/) { print i }
      }
} '

0

Napisałem do tego celu skrypt w języku Python, który działa w następujący sposób:

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))

Nazwałem go hgrepdo nagłówka grep , może on być stosowany w ten sposób:

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

Cały skrypt jest nieco dłuższy, ponieważ argparseanalizuje argumenty wiersza poleceń, a kod wygląda następująco:

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


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.