Substituir Coluna pelo Resultado do Comando

6

Estou tentando encontrar uma maneira de substituir rápida e eficientemente um valor de coluna para cada linha em um arquivo com a saída de um comando. Eu preciso trabalhar em vários arquivos com cerca de 500.000 linhas cada dia, então estou procurando algo que possa concluir a tarefa o mais rápido possível.

Eu preciso pegar a oitava coluna de uma linha delimitada por vírgula como entrada, executar um comando e substituir essa coluna pela saída do comando.

Isso é o que eu já tentei e, embora funcione, é muito lento:

awk -F "," 'NR > 1 {
    cmd = "cdrtoip " $8
    cmd | getline ip
    close(cmd)
    $8=ip
    print
}' $1.csv >> $1.csv.tmp

Eu preferiria manter o Bash ou outros programas do Linux que podem ser encontrados pré-instalados em um servidor Linux.

EDIT: Minhas desculpas, eu deveria ter incluído o que cdrtoip é.

# Convert CISCO format (signed integer) to Hex
# Capitalize or else conversion from hex to decimal doesn't work later
HEXIP=$(printf '%x\n' $1 | tr '[:lower:]' '[:upper:]')

# Negative numbers will get 8 'f' in front of them
# Trim that part off
if [[ ${#HEXIP} -eq 16 ]]; then
    HEXIP=${HEXIP:8:8}
fi

# Convert hex to decimal, separate into octets, put in order
OCTETS[0]=$(echo "ibase=16; ${HEXIP:6:2}" | bc)
OCTETS[1]=$(echo "ibase=16; ${HEXIP:4:2}" | bc)
OCTETS[2]=$(echo "ibase=16; ${HEXIP:2:2}" | bc)
OCTETS[3]=$(echo "ibase=16; ${HEXIP:0:2}" | bc)

# Print the IP
echo ${OCTETS[0]}.${OCTETS[1]}.${OCTETS[2]}.${OCTETS[3]}

O tempo de execução no cdrip fornece:

    0.23s real     0.00s user     0.02s system
    
por Michael 24.11.2015 / 00:43

4 respostas

3

O seguinte deve funcionar em qualquer versão de awk que suporte funções definidas pelo usuário, bem como as funções sprintf() e rshift() integradas. Isso inclui o GNU awk.

Eu pedi emprestado e adaptei o algoritmo decimal para o endereço IP quad-ponteado aqui:

link

Como mencionado no meu comentário, reescrever o script cdrtoip external como uma função awk evitará ter que chamar um script externo mais de 500.000 vezes.

awk -F, '
function cdrtoip(addr) {
  return sprintf ("%d.%d.%d.%d",
           rshift(and(addr,0xff000000),24),
           rshift(and(addr,0x00ff0000),16),
           rshift(and(addr,0x0000ff00),08),
           rshift(and(addr,0x000000ff),00))
};

NR > 1 {
    $8 = cdrtoip($8);
    print
}' "$1.csv" >> "$1.csv.tmp"

Eu executei isso em um arquivo de teste com 500.000 linhas e foi concluído em menos de 2 segundos:

$ wc -l input.csv 
500000 input.csv
$ time ./michael.sh < input.csv > output.csv

real 0m1.956s   user 0m1.935s   sys 0m0.018s
    
por 26.11.2015 / 00:30
3

Eu sei que você disse que queria se ater a aplicativos nativos, mas GNU Parallel permitiria que você executasse processos separados em paralelo, o que permitiria executar essa operação mais rapidamente:

sudo apt-get update
sudo apt-get install parallel
awk -F',' '{print $8}' file.csv | parallel -j+0 cdrtoip {}

Existem várias maneiras de invocar parallel , mas o método acima tiraria a saída da 8ª coluna do arquivo .csv e executaria um cdrtoip process por núcleo em seu sistema, em cada linha simultaneamente. Então, basicamente, se você estiver executando 4 núcleos, poderá concluir esse trabalho em 25% do tempo que normalmente levaria para executar.

O lado positivo de parallel é que ele controla a saída e a gera em ordem, como se fosse apenas uma tarefa em execução.

Após a instalação, man parallel para mais formas de execução (ou confira a documentação do link). Desculpe se isso não é o que você está procurando, mas veio para o resgate para mim inúmeras vezes no passado.

EDITAR: Se você quiser adicionar a saída de volta ao .csv para substituir a 8ª coluna, o exemplo abaixo funcionará , e tem

A configuração:

$ cat file.tmp
blah1,blah2,blah3,blah4,blah5,blah6,blah7,1175063050,blah9,blah10,blah11

$ for i in {1..5000}; do cat file.tmp; done > file.csv

$ wc -l < file.csv
5000

O script (usando o cdrtoip que você forneceu):

$ cat csvjob.sh
#!/bin/bash

fragment1="$(cut -d, -f1-7 file.csv | tr ',' "\t")"
fragment2="$(cut -d, -f8 file.csv | parallel -j+0 cdrtoip {})"
fragment3="$(cut -d',' -f9- file.csv | tr ',' "\t")"

paste <(echo "$fragment1") <(echo "$fragment2") <(echo "$fragment3") | sed "s/\t/,/g" > newfile.csv

O resultado:

$ time ./csvjob.sh
real    3m23.092s
user    1m22.245s
sys     2m57.794s

$ head -3 newfile.csv
blah1,blah2,blah3,blah4,blah5,blah6,blah7,10.10.10.70,blah9,blah10,blah11
blah1,blah2,blah3,blah4,blah5,blah6,blah7,10.10.10.70,blah9,blah10,blah11
blah1,blah2,blah3,blah4,blah5,blah6,blah7,10.10.10.70,blah9,blah10,blah11

Outra edição: O seguinte foi executado em um Mac Mini quad-core (também executando outras coisas):

$ time ./csvjob.sh
real    2m12.171s
user    2m59.816s
sys     2m15.787s

Eu também percebi que você disse 500.000 linhas ao invés de 5.000 linhas. Por que vale a pena, veja as estatísticas abaixo para executar cdrtoip 5.000 vezes consecutivas:

$ time for i in {1..5000}; do cdrtoip 1175063050; done > /dev/null
real    2m32.487s
user    1m26.537s
sys     1m8.270s

Edição final: O seguinte foi executado em um arquivo de 500.000 linhas em um Mac mini quad-core, que como declarado anteriormente, já estava executando vários aplicativos:

$ time ./csvjob.sh

real    216m22.780s
user    301m40.694s
sys     239m44.404s

Eu posso ver totalmente o que você quer dizer, OP.

Mesmo quando executado em paralelo, isso leva um bom tempo para ser executado.

Eu vejo que o OP encontrou uma solução melhor. 126 segundos por arquivo é difícil de bater. Novamente, para o que vale a pena, abaixo estão as estatísticas de executar o originalmente fornecido cdrtoip com uma linha de 500.000 .csv usando parallel (que eu percebo que o OP não é capaz de instalar) em uma VM Debian de 8 núcleos: / em>

$ time ./csvjob.sh
real    14m7.467s
user    6m3.883s
sys     4m18.556s
    
por 24.11.2015 / 06:22
2

cdrtoip é realmente muito lento, parece um script utilitário útil, mas provavelmente não foi planejado para ser chamado centenas de vezes em um loop. Estou assumindo que é uma ferramenta comum que é usada por outros scripts ou usuários, e você quer continuar a usá-la, mas torná-la mais rápida.

Basta fazer uma chamada para bc em vez de 4 para que o script seja executado em aproximadamente 1/3 do tempo. Usar conversões de shell em vez de bc pode fazer com que o script seja executado em aproximadamente 1/5 da hora.

Eu fiz uma pequena estrutura para gerar um grupo de entrada de amostra (cerca de 500 linhas) e, em seguida, executar dois scripts, orig.sh (o original cdrtoip ) e new.sh , uma versão modificada, e compará-los suas saídas. Parece:

INPUT_SIZE=500
SAMPLE_FILE=in.txt

rm -f $SAMPLE_FILE orig.out new.out

x=0
while [[ $((x++)) -le $INPUT_SIZE ]]; do
    tr -cd '[:digit:]' < /dev/urandom | head -c 10 | sed s/^0/1/ >> $SAMPLE_FILE
    echo >> $SAMPLE_FILE
    if [[ $((x%10)) -eq 0 ]]; then echo -n .; fi
    if [[ $((x%20)) -eq 0 ]]; then echo -n '-' >> $SAMPLE_FILE; fi # next num is negative
done
echo

echo new cdrtoip:
time while read line; do ./new.sh $line >> new.out; done < $SAMPLE_FILE

echo original cdrtoip:
time while read line; do ./orig.sh $line >> orig.out; done < $SAMPLE_FILE

diff -q orig.out new.out || echo "Output was different!"

A saída com uma chamada bc :

$ ./generate.sh 
..................................................
new cdrtoip:

real    0m1.431s
user    0m0.036s
sys     0m0.072s
original cdrtoip:

real    0m4.381s
user    0m0.040s
sys     0m0.084s

Aqui está o meu new.sh . Se você quiser a versão mais rápida, comente a linha bc e descomente as conversões abaixo dela (cerca de 0,85 segundo), e você também pode se livrar da capitalização ${HEXIP^^} . Se você mantiver ${HEXIP^^} , provavelmente deveria incluir um bash shebang porque ele não funcionará em todos os shells (ele irá, notavelmente, falhar no traço).

#!/bin/bash
# Convert CISCO format (signed integer) to Hex
# Capitalize or else conversion from hex to decimal doesn't work later
HEXIP=$(printf '%x' $1)
HEXIP=${HEXIP^^}

# Negative numbers will get 8 'f' in front of them
# Trim that part off
if [[ ${#HEXIP} -eq 16 ]]; then
    HEXIP=${HEXIP:8:8}
fi

# Convert hex to decimal, separate into octets, put in order
bc <<< "ibase=16; ${HEXIP:6:2}; ${HEXIP:4:2}; ${HEXIP:2:2}; ${HEXIP:0:2}" | tr '\n' . | sed 's/[\.]$/\n/'

# Convert hex to decimal, separate into octets, put in order
# using just bash: doesn't require hex characters to be upper case
#o0=$((16#${HEXIP:6:2}))
#o1=$((16#${HEXIP:4:2}))
#o2=$((16#${HEXIP:2:2}))
#o3=$((16#${HEXIP:0:2}))

# Print the IP
#echo $o0.$o1.$o2.$o3
    
por 24.11.2015 / 20:20
2

Como John1024 aponta, o grande suspeito de lentidão é o chamado do cdrtoip 500.000 vezes.

EDIT: baseado no script cdrtoip fornecido, toda a implementação está em Python. É muito mais rápido porque não há chamada para um script externo.

Eu recomendo que você olhe o Python para isso. O desempenho do Python é muito bom para esse tipo de tarefa; também existe um módulo para manipular arquivos csv incluídos na biblioteca Python padrão.

Aqui está uma implementação de amostra no Python. Este exemplo lê e grava em stdin / stdout como seu script awk, mas pode ser facilmente modificado para abrir arquivos. EDIT: limpeza e lidar com erros de conversão melhor. Forneça um resumo para stderr no final do processamento.

#!/usr/bin/python
import sys,csv

# Convert CISCO format (signed integer) to Hex
# Based on original cdrtoip script in bash
# Note that a ValueError is raised if conversion cannot be done.
def cdrtoip(addrfield):
  intaddr=int(addrfield)    # ValueError if not a valid int

  # Range-check the integer, make it unsigned
  # If out of range, raise a ValueError
  if intaddr < 0: intaddr=intaddr+0x100000000
  if intaddr < 0: raise ValueError
  if intaddr > 0xffffffff : raise ValueError

  return ".".join( [ str(intaddr >> i & 0xff) for i in (24,16,8,0) ] )

# There are other options, depending on the exact file format
# you want. See: https://docs.python.org/2/library/csv.html
indata=csv.reader(sys.stdin)
outdata=csv.writer(sys.stdout)
header=True
no_convert=0
invalid_row=0
row_converted=0
blank_row=0
for row in indata:
   # Write the first line unchanged...
   if header:
      header=False
   else:
      # Note that columns are numbered from 0
      if len(row) == 0:
         blank_row=blank_row+1
         continue
      elif len(row) > 7:
         try:
            row[7]=cdrtoip(row[7])
            row_converted=row_converted+1
         except ValueError:
            # if conversion fails, we count and leave the field unchanged.
            no_convert=no_convert+1
      else:
         # if there is no column 8 we count as invalid row.
         invalid_row=invalid_row+1

   outdata.writerow(row)

# Print a summary of work done (to stderr).
print >> sys.stderr,"%d values converted." % row_converted
if no_convert > 0:
   print >> sys.stderr,"%d values not converted." % no_convert
if invalid_row > 0:
   print >> sys.stderr,"%d rows not valid." % invalid_row
if blank_row > 0:
   print >> sys.stderr,"%d blank rows removed." % blank_row
    
por 24.11.2015 / 02:21

Tags