Como imprimir determinadas colunas pelo nome?

32

Eu tenho o seguinte arquivo:

id  name  age
1   ed    50
2   joe   70   

Quero imprimir apenas o ide agecolunas. Agora eu apenas uso awk:

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

No entanto, isso requer conhecer os números das colunas. Existe uma maneira de fazer isso onde eu possa usar o nome da coluna (especificado na primeira linha), em vez do número da coluna?

Brett Thomas
fonte
7
catnão é necessário, BTW. Você poderia usarawk '{ print $1, $3 }' file.tsv
Eric Wilson
Se não for o número da coluna , do que você gostaria de depender?
rozcietrzewiacz
2
@rozcietrzewiacz O nome; ele quer dizer em idvez de $1e em agevez de$3
Michael Mrozek
veja também discussão sobre stackoverflow
Hotschke 23/02

Respostas:

37

Talvez algo parecido com isto:

$ 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 você quiser especificar as colunas a serem impressas na linha de comando, faça algo assim:

$ 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 

(Observe a -vopção para obter a variável definida no BEGINbloco.)

Esteira
fonte
Estou adiando a aprendizagem do awk ... qual é a melhor maneira de oferecer suporte a um número variável de colunas? awk -f t.awk col1 col2 ... coln inputseria ideal; awk -f t.awk cols=col1,col2,...,coln inputiria trabalhar muito
Brett Thomas
1
Atualizei minha resposta. Pare de adiar a aprendizagem, se você quiser fazer coisas com ela :) #
Mat
3
O segundo exemplo não gera as colunas na ordem esperada, for (i in out)não possui ordem inerente. gawkoferece PROCINFO["sorted_in"]como solução, iterar sobre o índice com a for( ; ; )é provavelmente melhor.
mr.spuratic
@BrettThomas, recomendo este tutorial . (Se você tem acesso a lynda.com, eu ainda mais altamente recomendado "Awk Essential Training", que cobre todo o mesmo material, mas de forma mais concisa e com exercícios práticos.)
Wildcard
Sr. Spuratic, você é homem. Corri através do problema for (i in out), trabalhei bem com 3 campos, quando adicionei 2 fiz 4,5,1,2,3, em vez de 1,2,3,4,5 como eu esperava . Para obtê-los, a fim você tem que fazer para (i = 1; i <= comprimento (fora); i ++)
Severun
5

Apenas lançando uma solução Perl no lote:

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

csvkit

Converta os dados de entrada em um formato csv e use uma ferramenta csv, como csvcutem csvkit:

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

Instale o csvkit:

$ pip install csvkit

Use trcom a opção squeeze -spara convertê-lo em um arquivo csv válido e aplique csvcut:

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

Se você deseja retornar ao formato de dados antigo, pode usar tr ',' ' ' | column -t

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

Notas

  • O csvkit também suporta delimitadores diferentes ( opção compartilhada -d ou --delimiter), mas retorna um arquivo csv:

    • Se o arquivo usar apenas espaços para separar colunas (sem guias), os trabalhos a seguir

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Se o arquivo usar uma guia para separar colunas, o seguinte funciona e csvformatpode ser usado para recuperar o arquivo tsv:

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

      Tanto quanto eu verifiquei, apenas uma única guia é permitida.

  • csvlook pode formatar a tabela em um formato de tabela de remarcação

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (uso inútil de gato) : Eu gosto dessa maneira de construir o comando.

Hotschke
fonte
+1. Mas usos desnecessários trtambém. Os arquivos TSV são suportados diretamente, sem a necessidade de convertê-los para CSV. A opção -t(aka --tabs) diz cvscutpara usar guias como delimitador de campo. E -dou --delimiterpara usar qualquer caractere como delimitador.
cas
Com alguns testes, parece que as opções -de -testão semi-quebradas. eles trabalham para especificar o delimitador de entrada, mas o delimitador de saída é codificado permanentemente para ser sempre uma vírgula. OMI quebrado - deve ser o mesmo que o delimitador de entrada ou ter outra opção para permitir que o usuário defina o delimitador de saída, como awkos vars FS e OFS.
cas
4

Se você quiser apenas se referir a esses campos por seus nomes, em vez de números, você pode usar read:

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

EDITAR

Eu finalmente vi o seu significado! Aqui está uma função bash que imprimirá apenas as colunas que você especificar na linha de comando (por nome ).

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

Veja como você pode usá-lo com o arquivo que você apresentou:

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

(A função lê stdin. < file.tsv printColumns ... É equivalente 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: Preste atenção aos nomes das colunas solicitadas! Esta versão carece de verificações de sanidade, portanto, coisas desagradáveis ​​podem acontecer se um dos argumentos for algo como"anything; rm /my/precious/file"

rozcietrzewiacz
fonte
1
Isso também requer conhecer os números das colunas. Só porque você nomeá-los id, namee age, não muda o fato de que a ordem é codificado em sua readlinha.
21411 Janmoesen
1
@janmoesen Sim, eu finalmente consegui o ponto :)
rozcietrzewiacz
Isso é legal, obrigado. Estou trabalhando com arquivos grandes (1000 colunas, milhões de linhas), então estou usando o awk para velocidade.
Brett Thomas
@BrettThomas Oh, eu vejo. Estou muito curioso, então: você poderia postar algum benchmark que faça a comparação do tempo? (Use time { command(s); }).
Rozcietrzewiacz 01/12/11
@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

Pelo que vale a pena. Isso pode lidar com qualquer número de colunas na origem e qualquer número de colunas a serem impressas, em qualquer sequência de saída que você escolher; apenas reorganize os argumentos ...

por exemplo. ligar: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[@]}") 

saída

id      age
1       50
2       70
Peter.O
fonte
2

Se o arquivo que você está lendo nunca puder ser gerado pelo usuário, você poderá abusar da leitura incorporada:

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

A primeira linha inteira do arquivo de entrada é substituída na lista de argumentos, readpassando todos os nomes de campos da linha de cabeçalho como nomes de variáveis. O primeiro deles recebe o 1 que seq 100gera, o segundo recebe o 2, o terceiro recebe o 3 e assim por diante. O excesso de seqprodução é absorvido pela variável dummy extra. Se você souber o número de colunas de entrada com antecedência, poderá alterar os 100 para corresponder e se livrar extra.

O awkscript é uma sequência de aspas duplas, permitindo que as variáveis ​​de shell definidas por readsejam substituídas no script como awknúmeros de campo.

flabdablet
fonte
1

Geralmente é mais fácil olhar apenas o cabeçalho do arquivo, contar o número da coluna que você precisa ( c ) e usar o Unix cut:

cut -f c -d, file.csv

Mas quando existem muitas colunas ou muitos arquivos, eu uso o seguinte truque feio:

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

Testado no OSX, file.csvé delimitado por vírgula.

srk
fonte
1

Aqui está uma maneira rápida de selecionar uma única coluna.

Digamos que queremos a coluna chamada "foo":

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

Basicamente, pegue a linha do cabeçalho, divida-a em várias linhas com um nome de coluna por linha, numere as linhas, selecione a linha com o nome desejado e recupere o número da linha associada; use esse número de linha como o número da coluna para o comando recortar.

jdjensen
fonte
0

Procurando uma solução semelhante (eu preciso da coluna denominada id, que pode ter um número variável de colunas), me deparei com esta:

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

Eu escrevi um script Python para esse fim que basicamente funciona assim:

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

Eu o chamei hgrepde cabeçalho grep , ele pode ser usado assim:

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

O script inteiro é um pouco mais longo, porque ele usa argparsepara analisar argumentos da linha de comando e o código é o seguinte:

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

Experimente este pequeno utilitário awk para cortar cabeçalhos específicos - https://github.com/rohitprajapati/toyeca-cutter

Exemplo de uso -

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