Awk programming: Dividir arquivos grandes em arquivos menores baseados no padrão

4

Eu tenho um arquivo grande input.dat que se parece com o mostrado abaixo.

kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000
 kpoint2 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

Eu preciso dividir o arquivo em dois menores, conforme abaixo

kpoint1.dat :

kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

e kpoint2.dat :

kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

Eu escrevi um pequeno script para fazer isso. O script é mostrado abaixo.

for j in {1..2} 
do
    awk '$1=="kpoint'$j'" {for(i=1; i<=3; i++){getline; print}}' tmp7 >kpoint'$j'.dat
done

O script cria arquivos de saída com os nomes desejados. Mas todos os arquivos estão vazios. Alguém pode me ajudar a resolver isso?

    
por Sruthil Lal S.B. 19.05.2017 / 10:16

2 respostas

3

Isso pode ser feito inteiramente em awk :

$ awk '$1 ~ /kpoint[0-9]/ { file = $1 ".dat" } {print > file}' file
$ head kpoint*
==> kpoint1.dat <==
kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

==> kpoint2.dat <==
 kpoint2 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

Awk também suporta > file para redirecionamento, com algumas diferenças sutis (consulte manual do GNU awk para mais).

    
por muru 19.05.2017 / 10:22
2

Embora a resposta do muru seja a mais simples, existem várias outras maneiras sem o uso do awk.

Perl

A abordagem com o awk é basicamente que escrevemos em um nome de arquivo específico, e alteramos esse nome de arquivo se e somente se encontrarmos o kpoint no começo da linha. Mesma abordagem pode ser feita com o Perl:

$ perl -ane '$p=$F[0] if $F[0] =~ /kpoint/;open($f,">>",$p . ".dat"); print $f $_' input.txt

Veja como isso funciona:

  • -a flag nos permite usar o array @F especial de palavras que foram automaticamente divididas a partir de cada linha do arquivo de entrada. Assim, $F[0] refere-se à primeira palavra, assim como $1 no awk
  • $p=$F[0] if $F[0] =~ /kpoint/ destina-se a alterar $p (que deve ser a variável do prefixo) se e somente se kpoint estiver na linha. A melhoria para essa correspondência de padrões pode ser /^ *kpoint/
  • em cada iteração, abrimos para acrescentar um arquivo com o nome $p associado a .dat string; note que acrescentar parte é importante. Se você deseja executar com clareza, provavelmente quer se livrar dos antigos arquivos kpoint . Se quisermos que o arquivo seja sempre criado novo e sobrescrito, podemos reescrever o comando original como:

    $ perl -ane 'if ($F[0] =~ /kpoint/){$p=$F[0]; open($f,">",$p . ".dat")}; print $f $_' input.txt
    
  • E, finalmente, print $f $_ apenas imprime em qualquer nome de arquivo que tenhamos aberto.

split

No seu exemplo, parece que cada entrada consiste em 5 linhas. Se isso for constante, podemos dividir o arquivo dessa maneira, sem depender da correspondência de padrões com split . Especificamente, este comando:

$ split --additional-suffix=".dat" --numeric-suffixes=1 -l 5 input.txt  kpoint

Neste comando, as opções são as seguintes:

  • --additional-suffix=".dat" é o sufixo .dat estático que será adicionado a cada arquivo criado
  • --numeric-suffixes=1 nos permitirá adicionar números que mudam a partir de 1 para cada nome de arquivo
  • -l 5 permitirá dividir o arquivo de entrada a cada 5 linhas
  • input.txt é o arquivo que estamos tentando dividir
  • kpoint será o prefixo do nome de arquivo estático

E como isso funciona na prática:

$ split --additional-suffix=".dat" --numeric-suffixes=1 -l 5 input.txt  kpoint                                                                        
$ cat kpoint01.dat                                                                                                                                    
kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000
$ cat kpoint02.dat                                                                                                                                    
 kpoint2 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

Opcionalmente, também podemos adicionar --suffix-length=1 para manter o comprimento de cada sufixo numérico menor, como kpoint1 em vez de kpoint01 , mas isso pode ser um problema se você tiver um grande número de kpoint s.

alternativa do awk

Este é similar a resposta do muru , exceto que aqui usamos combinações de padrões diferentes, bem como uma abordagem diferente para criar o variável de nome de arquivo via sprintf()

$ awk '/^\ *kpoint/{f=sprintf("%s.dat",$1)};{print > f}' input.txt

Python

Embora as abordagens awk e split sejam mais curtas, outras ferramentas, como Python, são adequadas para o processamento de texto, e podemos usá-las para implementar soluções mais detalhadas, mas úteis.

O script abaixo faz exatamente isso e opera com a ideia de olhar para trás na lista de linhas que salvamos. O script continua salvando linhas, até encontrar kpoint no início da linha, o que significa que chegamos a uma nova entrada, o que também significa que precisamos gravar a entrada anterior em seu respectivo arquivo.

#!/usr/bin/env python3
import sys

def write_entry(pref,line_list):
    # this function writes the actual file for each entry
    with open(".".join([pref,"dat"]),"w") as entry_file:
        entry_file.write("".join(line_list))

def main():
    prefix = ""
    old_prefix = ""
    entry=[]
    with open(sys.argv[1]) as fd:
        for line in fd:
            # if we encounter kpoint string, that's a signal
            # that we need to write out the list of things 
            if line.strip().startswith('kpoint'):
                prefix=line.strip().split()[0]
                # This if statement counters special case
                # when we just started reading the file
                if not old_prefix:
                    old_prefix = prefix
                    entry.append(line)
                    continue
                write_entry(old_prefix,entry)
                old_prefix = prefix
                entry=[]
            # Keep storing lines. This works nicely after old 
            # entry has been cleared out. 
            entry.append(line)
    # since we're looking backwards, we need one last call
    # to write last entry when input file has been closed
    write_entry(old_prefix,entry)

if __name__ == '__main__': main()

Pure Bash

Quase a mesma ideia da abordagem Perl - continuamos escrevendo tudo para um nome de arquivo específico e alteramos o nome do arquivo apenas quando encontramos uma linha com kpoint nele.

#!/usr/bin/env bash

while IFS= read -r line;
do
    case "$line" in
        # We found next entry. Use word-splitting to get
        # filename into fname variable, and truncate that filename
        *kpoint[0-9]*) read fname trash <<< $line  && 
                       echo "$line" > "$fname".dat ;;
        # That's just a line within entry. Append to 
        # current working file
        *) echo "$line" >> "$fname".dat ;;
    esac
done < "$1"

# Just in case there are trailing lines that weren't processed
# in while loop, append them to last filename
[ -n "$line" ] && echo "$line" >> "$fname".dat ;
    
por Sergiy Kolodyazhnyy 20.05.2017 / 00:21