Comment imprimer certaines colonnes par nom?

32

J'ai le fichier suivant:

id  name  age
1   ed    50
2   joe   70   

Je veux imprimer uniquement les colonnes idet age. En ce moment je viens d'utiliser awk:

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

Cependant, cela nécessite de connaître les numéros de colonne. Y a-t-il un moyen de le faire où je peux utiliser le nom de la colonne (spécifié sur la première ligne) au lieu du numéro de la colonne?

Brett Thomas
la source
7
catn'est pas nécessaire, BTW. Vous pouvez utiliserawk '{ print $1, $3 }' file.tsv
Eric Wilson
Si ce n'est pas le numéro de colonne , sur quoi voudriez-vous compter?
rozcietrzewiacz
2
@rozcietrzewiacz Le nom; il veut dire idau lieu $1et ageau lieu de$3
Michael Mrozek
voir aussi discussion sur stackoverflow
Hotschke

Réponses:

37

Peut-être quelque chose comme ça:

$ 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

Si vous souhaitez spécifier les colonnes à imprimer sur la ligne de commande, vous pouvez procéder comme suit:

$ 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 

(Notez le -vcommutateur pour obtenir la variable définie dans le BEGINbloc.)

Tapis
la source
J'ai tardé à apprendre awk ... Quelle est la meilleure façon de prendre en charge un nombre variable de colonnes? awk -f t.awk col1 col2 ... coln inputserait idéal; awk -f t.awk cols=col1,col2,...,coln inputfonctionnerait aussi
Brett Thomas
1
Mis à jour ma réponse. Arrêtez de retarder votre apprentissage si vous voulez faire des choses avec ça :)
Mat
3
Le deuxième exemple ne génère pas les colonnes dans l'ordre prévu, il for (i in out)n'a pas d'ordre inhérent. gawkoffre PROCINFO["sorted_in"]comme une solution, itérer sur l'indice avec un for( ; ; )est sans doute mieux.
mr.spuratic
@BrettThomas, recommande fortement ce tutoriel . (Si vous avez accès à lynda.com, je recommande encore plus fortement "Awk Essential Training", qui couvre tout le même matériel, mais de manière plus concise et avec des exercices pratiques.)
Wildcard
M. Spuratic, vous êtes un homme. Je suis tombé sur le problème de (i in out), j'ai bien travaillé avec 3 champs. Quand j'ai ajouté 2, il a fait 4,5,1,2,3, au lieu de 1,2,3,4,5 comme je m'y attendais. . Pour les mettre en ordre, vous devez faire pour (i = 1; i <= longueur (out); i ++)
Severun
5

Introduire simplement une solution Perl dans le lot:

#!/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
la source
5

csvkit

Convertir les données d'entrée à un format csv et utiliser un outil de csv tels que csvcutdu csvkit:

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

Installez csvkit:

$ pip install csvkit

Utiliser travec son option squeeze -spour le convertir en un fichier csv valide et appliquer csvcut:

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

Si vous souhaitez revenir à l'ancien format de données, vous pouvez utiliser tr ',' ' ' | column -t

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

Remarques

  • csvkit prend également en charge différents délimiteurs ( option partagée -d ou --delimiter), mais renvoie un fichier csv:

    • Si le fichier utilise uniquement des espaces pour séparer les colonnes (aucun onglet), les travaux suivants

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Si le fichier utilise un onglet pour séparer les colonnes, les opérations suivantes csvformatpeuvent être utilisées pour récupérer le fichier tsv:

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

      Pour autant que j'ai vérifié, un seul onglet est autorisé.

  • csvlook peut formater la table dans un format de table de démarques

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (utilisation inutile de chat) : J'aime cette façon de construire la commande.

Hotschke
la source
+1 Mais les utilisations inutiles de tr, aussi. Les fichiers TSV sont pris en charge directement, sans qu'il soit nécessaire de les convertir au format CSV. L' option -t(aka --tabs) indique cvscutd'utiliser des tabulations comme délimiteur de champ. Et -dou --delimiterd'utiliser n'importe quel caractère comme délimiteur.
cas
Avec quelques tests, il semble que les options -det -tsont semi-cassées. ils travaillent pour spécifier le délimiteur d'entrée, mais le délimiteur de sortie est codé en dur pour toujours être une virgule. OMI qui est cassé - il devrait être identique au délimiteur d’entrée ou disposer d’une autre option permettant à l’utilisateur de définir le délimiteur de sortie, comme awkles vars FS et OFS.
cas
4

Si vous souhaitez simplement faire référence à ces champs par leur nom plutôt que par leur numéro, vous pouvez utiliser read:

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

MODIFIER

J'ai enfin compris votre signification! Voici une fonction bash qui imprimera uniquement les colonnes que vous spécifiez sur la ligne de commande (par nom ).

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

Voici comment vous pouvez l'utiliser avec le fichier que vous avez présenté:

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

(La fonction lit stdin. < file.tsv printColumns ... Est équivalent printColumns ... < file.tsvet 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

Remarque: Faites attention aux noms des colonnes que vous demandez! Cette version manque de contrôle de cohérence, donc des choses désagréables peuvent arriver si l’un des arguments est quelque chose comme:"anything; rm /my/precious/file"

rozcietrzewiacz
la source
1
Cela nécessite également de connaître les numéros de colonne. Ce n'est pas parce que vous les nommez id, nameet agecela ne change rien au fait que la commande est codée en dur dans votre readligne.
janmoesen
1
@janmoesen Oui, j'ai enfin compris le point :)
rozcietrzewiacz
C'est sympa, merci. Je travaille avec des fichiers volumineux (1000 colonnes, des millions de lignes), j'utilise donc awk pour plus de rapidité.
Brett Thomas
@BrettThomas Oh je vois. Je suis très curieux alors: pouvez-vous publier un repère qui donne la comparaison de temps? (Utilisation 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

Pour ce que ça vaut. Cela peut gérer un nombre quelconque de colonnes dans la source et un nombre illimité de colonnes à imprimer, dans la séquence de sortie de votre choix. il suffit de réorganiser les arguments ...

par exemple. appel: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[@]}") 

sortie

id      age
1       50
2       70
Peter.O
la source
2

Si le fichier que vous lisez ne pourrait jamais être généré par l'utilisateur, vous pouvez abuser de la lecture intégrée:

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

La première ligne entière du fichier d'entrée est remplacée par la liste d'arguments. readTous les noms de champs de la ligne d'en-tête sont donc transmis en tant que noms de variables. Le premier de ceux-ci se voit attribuer le 1 qui seq 100génère, le second le 2, le troisième le 3 et ainsi de suite. L'excédent de seqproduction est absorbé par la variable muette extra. Si vous connaissez le nombre de colonnes d'entrée à l'avance, vous pouvez modifier les 100 pour qu'elles correspondent et vous en débarrasser extra.

Le awkscript est une chaîne entre guillemets, permettant de readsubstituer dans le script les variables shell définies par en tant que awknuméros de champs.

flabdablet
la source
1

En général, il est plus facile de regarder l'en-tête du fichier, de compter le numéro de la colonne dont vous avez besoin ( c ), puis d'utiliser Unix cut:

cut -f c -d, file.csv

Mais quand il y a beaucoup de colonnes ou beaucoup de fichiers, j'utilise l'astuce laide suivante:

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

Testé sur OSX, il file.csvest délimité par des virgules.

srk
la source
1

Voici un moyen rapide pour sélectionner une seule colonne.

Disons que nous voulons la colonne nommée "foo":

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

Fondamentalement, prenez la ligne d'en-tête, divisez-la en plusieurs lignes avec un nom de colonne par ligne, numérotez les lignes, sélectionnez la ligne avec le nom souhaité et récupérez le numéro de ligne associé; utilisez ensuite ce numéro de ligne comme numéro de colonne de la commande de coupe.

Jdjensen
la source
0

À la recherche d'une solution similaire (j'ai besoin de la colonne nommée id, qui peut avoir un numéro de colonne différent), je suis tombé sur celle-ci:

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

J'ai écrit un script Python à cet effet qui fonctionne comme suit:

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

Je l'ai appelé hgrep pour l'en- tête grep , il peut être utilisé comme ceci:

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

Le script entier est un peu plus long, car il utilise argparsepour analyser les arguments en ligne de commande et le code est le suivant:

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