Come stampare determinate colonne per nome?


32

Ho il seguente file:

id  name  age
1   ed    50
2   joe   70   

Voglio stampare solo le colonne ide age. In questo momento uso solo awk:

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

Tuttavia, ciò richiede la conoscenza dei numeri di colonna. C'è un modo per farlo in cui posso usare il nome della colonna (specificato nella prima riga), anziché il numero della colonna?


7
catnon è necessario, BTW. Puoi usareawk '{ print $1, $3 }' file.tsv
Eric Wilson il

Se non il numero di colonna , da cosa vorresti dipendere?
rozcietrzewiacz,

2
@rozcietrzewiacz Il nome; vuole dire idinvece $1e ageinvece di$3
Michael Mrozek

si veda anche la discussione sulla StackOverflow
Hotschke

Risposte:


37

Forse qualcosa del genere:

$ 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

Se vuoi specificare le colonne da stampare sulla riga di comando, puoi fare qualcosa del genere:

$ 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 

(Nota l' -vinterruttore per ottenere la variabile definita nel BEGINblocco.)


Ho rimandato l'apprendimento di awk ... qual è il modo migliore per supportare un numero variabile di colonne? awk -f t.awk col1 col2 ... coln inputsarebbe l'ideale; awk -f t.awk cols=col1,col2,...,coln inputfunzionerebbe anche
Brett Thomas il

1
Aggiornato la mia risposta. Smetti di rimandare l'apprendimento se vuoi fare cose con esso :)
Mat

3
Il secondo esempio non genera le colonne nell'ordine previsto, for (i in out)non ha un ordine intrinseco. gawkoffre PROCINFO["sorted_in"]come soluzione, for( ; ; )probabilmente è meglio scorrere l'Indice con a .
mr.spuratic

@BrettThomas, consiglio vivamente questo tutorial . (Se hai accesso a lynda.com, consiglio vivamente ancora più "Awk Essential Training", che copre tutto lo stesso materiale ma in modo più conciso e con esercizi di pratica.)
Wildcard

Signor Spuratic, amico. Ho riscontrato il problema for (i in out), ho funzionato benissimo con 3 campi, quando ho aggiunto 2 ha fatto 4,5,1,2,3, anziché 1,2,3,4,5 come mi aspettavo . Per farli in ordine devi fare per (i = 1; i <= length (out); i ++)
Severun

5

Basta inserire una soluzione Perl nel lotto:

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

Convertire i dati di input in un formato CSV e utilizzare uno strumento CSV come csvcutdal csvkit:

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

Installa csvkit:

$ pip install csvkit

Utilizzare trcon l'opzione di compressione -sper convertirlo in un file CSV valido e applicare csvcut:

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

Se si desidera tornare al vecchio formato di dati, è possibile utilizzare tr ',' ' ' | column -t

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

Gli appunti

  • csvkit supporta anche diversi delimitatori ( opzione condivisa -d o --delimiter), ma restituisce un file csv:

    • Se il file utilizza solo spazi per separare le colonne (nessuna scheda), funziona come segue

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Se il file utilizza una scheda per separare le colonne, funziona come segue e csvformatpuò essere utilizzato per recuperare il file tsv:

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

      Per quanto ho verificato, è consentita una sola scheda.

  • csvlook può formattare la tabella in un formato tabella markdown

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (Useless Use Of Cat) : Mi piace in questo modo per costruire il comando.


+1. Ma anche usi superflui tr. I file TSV sono supportati direttamente, senza la necessità di convertirli in CSV. L' opzione -t(aka --tabs) dice cvscutdi usare le schede come delimitatore di campo. E -do --delimiterper usare qualsiasi personaggio come delimitatore.
Cas

Con alcuni test, sembra che le opzioni -de -tsiano semi-rotte. funzionano per specificare il delimitatore di input, ma il delimitatore di output è codificato in modo da essere sempre una virgola. IMO non funzionante: dovrebbe essere uguale al delimitatore di input o avere un'altra opzione per consentire all'utente di impostare il delimitatore di output, come awki var di FS e OFS.
Cas

4

Se vuoi solo fare riferimento a quei campi con i loro nomi anziché i numeri, puoi usare read:

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

MODIFICARE

Finalmente ho visto il tuo significato! Ecco una funzione bash che stamperà solo le colonne specificate sulla riga di comando (per nome ).

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

Ecco come puoi usarlo con il file che hai presentato:

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

(La funzione legge stdin. < file.tsv printColumns ... Equivale a printColumns ... < file.tsve 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

Nota: prestare attenzione ai nomi delle colonne richieste! Questa versione è priva di controlli di integrità, quindi possono succedere cose brutte se uno degli argomenti è qualcosa di simile"anything; rm /my/precious/file"


1
Ciò richiede anche la conoscenza dei numeri di colonna. Solo perché loro è il nome id, namee age, non cambia il fatto che l'ordine è hard-coded nella vostra readlinea.
Janmoesen,

1
@janmoesen Sì, ho finalmente capito il punto :)
rozcietrzewiacz

Questo è carino, grazie. Sto lavorando con file di grandi dimensioni (1000 colonne, milioni di righe), quindi sto usando awk per la velocità.
Brett Thomas,

@BrettThomas Oh, vedo. Sono molto curioso quindi: potresti pubblicare qualche benchmark che dia il confronto del tempo? (Usa 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 l'

3

Per quello che vale. Questo può gestire qualsiasi numero di colonne nell'origine e qualsiasi numero di colonne da stampare, in qualunque sequenza di output tu scelga; basta riorganizzare gli arg ...

per esempio. chiamata: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[@]}") 

produzione

id      age
1       50
2       70

2

Se il file che stai leggendo non potrebbe mai essere generato dall'utente, potresti abusare della lettura incorporata:

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

L'intera prima riga del file di input viene sostituita nell'elenco degli argomenti, quindi readtutti i nomi dei campi dalla riga di intestazione vengono passati come nomi di variabili. Al primo di questi viene assegnato l'1 che seq 100genera, il secondo ottiene il 2, il terzo ottiene il 3 e così via. L' seqoutput in eccesso viene assorbito dalla variabile fittizia extra. Se si conosce in anticipo il numero di colonne di input, è possibile modificare le 100 in modo che corrispondano e si eliminino extra.

Lo awkscript è una stringa tra virgolette doppie, che consente readdi sostituire le variabili shell definite dallo script come awknumeri di campo.


1

Di solito è più semplice guardare l'intestazione del file, contare il numero della colonna di cui hai bisogno ( c ) e quindi usare Unix cut:

cut -f c -d, file.csv

Ma quando ci sono molte colonne o molti file uso il seguente brutto trucco:

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

Testato su OSX, file.csvè delimitato da virgole.


1

Ecco un modo rapido per selezionare una singola colonna.

Supponiamo di voler la colonna denominata "pippo":

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

Fondamentalmente, prendi la riga di intestazione, dividila in più righe con un nome di colonna per riga, numera le righe, seleziona la riga con il nome desiderato e recupera il numero di riga associato; quindi usa quel numero di riga come numero di colonna per il comando cut.


0

Alla ricerca di una soluzione simile (ho bisogno della colonna denominata id, che potrebbe avere un numero di colonna variabile), mi sono imbattuto in questo:

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

0

Ho scritto uno script Python per questo scopo che sostanzialmente funziona così:

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

L'ho chiamato hgrepper header grep , può essere usato in questo modo:

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

L'intero script è un po 'più lungo, perché utilizza argparseper analizzare gli argomenti della riga di comando e il codice è il seguente:

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


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.