Wie drucke ich bestimmte Spalten nach Namen?

32

Ich habe folgende Datei:

id  name  age
1   ed    50
2   joe   70   

Ich möchte nur die Spalten idund drucken age. Im Moment benutze ich nur awk:

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

Dies setzt jedoch voraus, dass die Spaltennummern bekannt sind. Gibt es eine Möglichkeit, wie ich den Namen der Spalte (in der ersten Zeile angegeben) anstelle der Spaltennummer verwenden kann?

Brett Thomas
quelle
7
catist nicht nötig, übrigens. Sie könntenawk '{ print $1, $3 }' file.tsv
Eric Wilson
Wenn nicht die Spaltennummer , worauf möchten Sie sich dann verlassen ?
rozcietrzewiacz
2
@rozcietrzewiacz Der Name; er will idstatt $1und agestatt sagen$3
Michael Mrozek
siehe auch Diskussion über Stackoverflow
Hotschke

Antworten:

37

Vielleicht so etwas:

$ 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

Wenn Sie die Spalten angeben möchten, die in der Befehlszeile gedruckt werden sollen, können Sie Folgendes tun:

$ 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 

(Beachten Sie den -vSchalter, um die im BEGINBlock definierte Variable zu erhalten .)

Matte
quelle
Ich habe es aufgeschoben, awk zu lernen. Wie kann ich eine variable Anzahl von Spalten am besten unterstützen? awk -f t.awk col1 col2 ... coln inputwäre ideal; awk -f t.awk cols=col1,col2,...,coln inputwürde auch funktionieren
Brett Thomas
1
Aktualisiert meine Antwort. Hör auf damit aufzuhören, wenn du etwas damit anfangen willst :)
Mat
3
Das zweite Beispiel gibt die Spalten nicht in der erwarteten Reihenfolge aus und for (i in out)weist keine inhärente Reihenfolge auf. gawkbietet sich PROCINFO["sorted_in"]als Lösung an, über den Index mit a zu iterieren for( ; ; )ist wohl besser.
mr.spuratic
@BrettThomas, empfehle dieses Tutorial . (Wenn Sie Zugang zu lynda.com haben, empfehle ich "Awk Essential Training", das das gleiche Material, aber prägnanter und mit Übungsaufgaben behandelt.)
Wildcard
Mr. Spuratic, Sie sind ein Mann. Ich lief über das Problem für (i in out), arbeitete gut mit 3 Feldern, als ich 2 hinzufügte, tat es 4,5,1,2,3, anstatt 1,2,3,4,5, wie ich erwartet hatte . Um sie in Ordnung zu bringen, musst du (i = 1; i <= Länge (out); i ++)
Severun
5

Einfach eine Perl-Lösung in das Los werfen:

#!/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
}
Peter John Acklam
quelle
5

csvkit

Konvertieren Sie die Eingabedaten in ein CSV-Format und verwenden Sie ein CSV-Tool wie csvcutdas folgende csvkit:

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

Installieren Sie csvkit:

$ pip install csvkit

Mit trder Squeeze-Option können -sSie die Datei in eine gültige CSV-Datei konvertieren und Folgendes anwenden csvcut:

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

Wenn Sie zum alten Datenformat zurückkehren möchten, können Sie verwenden tr ',' ' ' | column -t

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

Anmerkungen

  • csvkit unterstützt auch verschiedene Trennzeichen ( shared option -d oder --delimiter), gibt aber eine csv-Datei zurück:

    • Wenn die Datei nur Leerzeichen zum Trennen von Spalten verwendet (überhaupt keine Tabulatoren), funktioniert Folgendes

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Wenn die Datei eine Registerkarte zum Trennen von Spalten verwendet, funktioniert Folgendes und csvformatkann verwendet werden, um die TSV-Datei zurückzugewinnen:

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

      Soweit ich geprüft habe, ist nur ein Tab erlaubt.

  • csvlook kann die Tabelle in einem Abzeichentabellenformat formatieren

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (Useless Use Of Cat) : Ich mag es so, den Befehl zu konstruieren.

Hotschke
quelle
+1. Aber auch unnötige Verwendungen von tr. TSV-Dateien werden direkt unterstützt, ohne dass sie in CSV konvertiert werden müssen. Die Option -t(aka --tabs) gibt cvscutan, dass Tabulatoren als Feldtrennzeichen verwendet werden sollen. Und -doder ein --delimiterbeliebiges Zeichen als Trennzeichen verwenden.
cas
Mit einigen Tests scheint es, als ob die -dund -t-Optionen teilweise gebrochen sind. Sie arbeiten, um das Eingabe-Begrenzungszeichen anzugeben, aber das Ausgabe-Begrenzungszeichen ist fest codiert, um immer ein Komma zu sein. IMO ist defekt - es sollte entweder das gleiche wie das Eingabe-Trennzeichen sein oder eine andere Option haben, mit der der Benutzer das Ausgabe-Trennzeichen festlegen kann, wie z. B. awkFS- und OFS-Variablen von.
cas
4

Wenn Sie diese Felder nur mit ihren Namen anstelle von Zahlen bezeichnen möchten , können Sie Folgendes verwenden read:

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

BEARBEITEN

Ich habe deine Bedeutung endlich gesehen! Hier ist eine Bash-Funktion, die nur die Spalten ausgibt, die Sie in der Befehlszeile angegeben haben (nach Namen ).

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

So können Sie es mit der präsentierten Datei verwenden:

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

(Die Funktion liest stdin. < file.tsv printColumns ... Ist äquivalent zu printColumns ... < file.tsvund 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

Hinweis: Achten Sie auf die Namen der gewünschten Spalten! In dieser Version gibt es keine Plausibilitätsprüfung, so dass schlimme Dinge passieren können, wenn eines der Argumente so etwas wie ist"anything; rm /my/precious/file"

rozcietrzewiacz
quelle
1
Dazu müssen auch die Spaltennummern bekannt sein. Nur weil man sich nennen id, nameund age, nicht die Tatsache ändern , dass die Reihenfolge hartcodiert ist in Ihrer readLinie.
22.
1
@janmoesen Ja, ich habe endlich verstanden :)
rozcietrzewiacz
Das ist schön, danke. Ich arbeite mit großen Dateien (1000 Spalten, Millionen Zeilen), also benutze ich awk für die Geschwindigkeit.
Brett Thomas
@BrettThomas Oh ich verstehe. Ich bin dann sehr gespannt: Könnten Sie einen Benchmark posten, der den Zeitvergleich ermöglicht? (Verwendung 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

Für was es wert ist. Dies kann eine beliebige Anzahl von Spalten in der Quelle und eine beliebige Anzahl von zu druckenden Spalten in der von Ihnen gewählten Ausgabereihenfolge verarbeiten. arrangieren Sie einfach die Argumente neu ...

z.B. Anruf: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[@]}") 

Ausgabe

id      age
1       50
2       70
Peter.O
quelle
2

Wenn die Datei, die Sie lesen, niemals vom Benutzer generiert werden könnte, könnten Sie den eingebauten Lesezugriff missbrauchen:

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

Die gesamte erste Zeile der Eingabedatei wird in die Argumentliste eingefügt, sodass readalle Feldnamen aus der Kopfzeile als Variablennamen übergeben werden. Dem ersten wird die seq 100erzeugende 1 zugewiesen , dem zweiten die 2, dem dritten die 3 und so weiter. Überschüssige seqLeistung wird von der Dummy-Variablen aufgenommen extra. Wenn Sie die Anzahl der Eingabespalten im Voraus kennen, können Sie die 100 ändern, um sie abzugleichen und loszuwerden extra.

Das awkSkript ist eine Zeichenfolge in doppelten Anführungszeichen, mit der die durch definierten Shell-Variablen readals Feldnummern in das Skript eingesetzt awkwerden können.

flabdablet
quelle
1

Normalerweise ist es einfacher, nur den Dateikopf zu betrachten, die Nummer der benötigten Spalte zu zählen ( c ) und dann Unix zu verwenden cut:

cut -f c -d, file.csv

Aber wenn es viele Spalten oder viele Dateien gibt, benutze ich den folgenden hässlichen Trick:

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

Getestet unter OSX ist das durch file.csvKommas getrennt.

srk
quelle
1

Hier ist eine schnelle Möglichkeit zum Auswählen einer einzelnen Spalte.

Angenommen, wir möchten die Spalte "foo":

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

Nehmen Sie im Allgemeinen die Kopfzeile, teilen Sie sie in mehrere Zeilen mit einem Spaltennamen pro Zeile auf, nummerieren Sie die Zeilen, wählen Sie die Zeile mit dem gewünschten Namen aus und rufen Sie die zugehörige Zeilennummer ab. Verwenden Sie dann diese Zeilennummer als Spaltennummer für den Befehl cut.

jdjensen
quelle
0

Auf der Suche nach einer ähnlichen Lösung (ich benötige die Spalte mit dem Namen id, die eine unterschiedliche Spaltennummer haben könnte), bin ich auf diese gestoßen:

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

Zu diesem Zweck habe ich ein Python-Skript geschrieben, das im Grunde so funktioniert:

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

Ich habe es hgrepfür header grep genannt , es kann so verwendet werden:

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

Das gesamte Skript ist etwas länger, da es argparsezum Parsen von Befehlszeilenargumenten verwendet wird und der Code wie folgt lautet:

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

Probieren Sie dieses kleine Hilfsprogramm awk aus, um bestimmte Header auszuschneiden - https://github.com/rohitprajapati/toyeca-cutter

Anwendungsbeispiel -

awk -f toyeca-cutter.awk -v c="col1, col2, col3, col4" my_file.csv
toyeca
quelle