¿Cómo imprimir ciertas columnas por nombre?

32

Tengo el siguiente archivo:

id  name  age
1   ed    50
2   joe   70   

Quiero imprimir solo las columnas idy age. En este momento solo uso awk:

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

Sin embargo, esto requiere conocer los números de columna. ¿Hay alguna manera de hacerlo donde pueda usar el nombre de la columna (especificado en la primera fila), en lugar del número de columna?

Brett Thomas
fuente
77
catno es necesario, por cierto. Podrías usarawk '{ print $1, $3 }' file.tsv
Eric Wilson el
Si no es el número de columna , ¿de qué le gustaría depender?
rozcietrzewiacz
2
@rozcietrzewiacz El nombre; quiere decir en idlugar de $1y en agelugar de$3
Michael Mrozek
ver también discusión sobre stackoverflow
Hotschke

Respuestas:

37

Tal vez algo como esto:

$ 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 desea especificar las columnas para imprimir en la línea de comando, puede hacer algo como esto:

$ 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 

(Tenga en cuenta el -vcambio para obtener la variable definida en el BEGINbloque).

Estera
fuente
He estado posponiendo aprender awk ... ¿cuál es la mejor manera de admitir un número variable de columnas? awk -f t.awk col1 col2 ... coln inputsería ideal awk -f t.awk cols=col1,col2,...,coln inputfuncionaría también
Brett Thomas
1
Actualicé mi respuesta. Deja de postergar el aprendizaje si quieres hacer cosas con él :)
Mat
3
El segundo ejemplo no muestra las columnas en el orden esperado, for (i in out)no tiene un orden inherente. gawkofertas PROCINFO["sorted_in"]como solución, iterar sobre el índice con a for( ; ; )probablemente sea mejor.
Sr.Spuratic
@BrettThomas, recomiendo este tutorial . (Si tiene acceso a lynda.com, recomiendo aún más "Awk Essential Training", que cubre todo el mismo material pero de manera más concisa y con ejercicios de práctica.)
Comodín
Sr. Spuratic, eres un hombre. Me encontré con el problema for (i in out), funcioné bien con 3 campos, cuando agregué 2 lo hice 4,5,1,2,3, en lugar de 1,2,3,4,5 como esperaba . Para ponerlos en orden, debe hacerlo por (i = 1; i <= length (out); i ++)
Severun
5

Simplemente lanzando una solución Perl al 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
fuente
5

csvkit

Convierta los datos de entrada a un formato csv y use una herramienta csv como csvcutla de csvkit:

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

Instale csvkit:

$ pip install csvkit

Úselo trcon su opción de compresión -spara convertirlo en un archivo csv válido y aplique csvcut:

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

Si desea volver al formato de datos anterior, puede usar tr ',' ' ' | column -t

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

Notas

  • csvkit también admite diferentes delimitadores ( opción compartida -d o --delimiter), pero devuelve un archivo csv:

    • Si el archivo usa solo espacios para separar columnas (sin pestañas), los siguientes trabajos

      $ csvcut -d ' ' -S -c 'id,age' test-cols.dat
      id,age
      1,50
      2,70
    • Si el archivo usa una pestaña para separar las columnas, los siguientes trabajos csvformatpueden usarse para recuperar el archivo tsv:

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

      Por lo que he comprobado, solo se permite una sola pestaña.

  • csvlook puede formatear la tabla en un formato de tabla de rebajas

    $ csvcut -t -c "id,age" test-cols.dat | csvlook
    | id | age |
    | -- | --- |
    |  1 |  50 |
    |  2 |  70 |
  • UUOC (Useless Use Of Cat) : me gusta de esta manera construir el comando.

Hotschke
fuente
+1. Pero usos innecesarios de tr, también. Los archivos TSV son compatibles directamente, sin necesidad de convertirlos a CSV. La opción -t(aka --tabs) le dice cvscutque use pestañas como delimitador de campo. Y -do --delimiterusar cualquier carácter como delimitador.
cas
Con algunas pruebas, parece que las opciones -dy -testán semi rotas. funcionan para especificar el delimitador de entrada, pero el delimitador de salida está codificado para ser siempre una coma. IMO que está roto: debe ser el mismo que el delimitador de entrada o tener otra opción para permitir que el usuario establezca el delimitador de salida, como awklas variables FS y OFS.
cas
4

Si solo quiere referirse a esos campos por sus nombres en lugar de números, puede usar read:

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

EDITAR

¡Por fin vi tu significado! Aquí hay una función bash que imprimirá solo las columnas que especifique en la línea de comando (por nombre ).

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

Así es como puede usarlo con el archivo que ha presentado:

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

(La función lee stdin. < file.tsv printColumns ... Es equivalente a printColumns ... < file.tsvy 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 atención a los nombres de las columnas que solicita! Esta versión carece de controles de cordura, por lo que pueden suceder cosas desagradables si uno de los argumentos es algo así como"anything; rm /my/precious/file"

rozcietrzewiacz
fuente
1
Esto también requiere conocer los números de columna. El hecho de que los nombra id, namey age, no cambia el hecho de que el pedido sea modificable en su readlínea.
Janmoesen
1
@janmoesen Sí, finalmente
entendí
Esto es bueno, gracias. Estoy trabajando con archivos grandes (1000 columnas, millones de filas), así que estoy usando awk para mayor velocidad.
Brett Thomas
@BrettThomas Oh, ya veo. Tengo mucha curiosidad entonces: ¿podrías publicar algún punto de referencia que dé la comparación de tiempo? (Uso 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

Por lo que vale. Esto puede manejar cualquier número de columnas en la fuente, y cualquier número de columnas para imprimir, en cualquier secuencia de salida que elija; solo reorganiza los args ...

p.ej. llamada: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[@]}") 

salida

id      age
1       50
2       70
Peter.O
fuente
2

Si el archivo que está leyendo nunca podría ser generado por el usuario, podría abusar de la lectura incorporada:

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

La primera línea completa del archivo de entrada se sustituye en la lista de argumentos, por lo que readse pasan todos los nombres de campo de la línea de encabezado como nombres de variables. Al primero de ellos se le asigna el 1 que seq 100genera, el segundo obtiene el 2, el tercero obtiene el 3 y así sucesivamente. El exceso de seqsalida es absorbido por la variable ficticia extra. Si conoce la cantidad de columnas de entrada con anticipación, puede cambiar las 100 para que coincidan y deshacerse de ellas extra.

El awkscript es una cadena entre comillas dobles, que permite que las variables de shell definidas por readsean sustituidas en el script como awknúmeros de campo.

Flabdablet
fuente
1

Por lo general, es más fácil solo mirar el encabezado del archivo, contar el número de la columna que necesita ( c ) y luego usar Unix cut:

cut -f c -d, file.csv

Pero cuando hay muchas columnas o muchos archivos, uso el siguiente truco feo:

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

Probado en OSX, file.csvestá delimitado por comas.

srk
fuente
1

Aquí hay una forma rápida de seleccionar una sola columna.

Digamos que queremos la columna llamada "foo":

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

Básicamente, tome la línea de encabezado, divídala en varias líneas con un nombre de columna por línea, numere las líneas, seleccione la línea con el nombre deseado y recupere el número de línea asociado; luego use ese número de línea como el número de columna para el comando de corte.

jdjensen
fuente
0

Buscando una solución similar (necesito la columna llamada id, que podría tener un número de columna variable), me encontré con esta:

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

Escribí un script de Python para este propósito que básicamente funciona así:

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

Lo llamé hgreppara encabezado grep , se puede usar así:

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

El script completo es un poco más largo, porque se usa argparsepara analizar argumentos de línea de comando y el código es el siguiente:

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