web-dev-qa-db-fra.com

Programmation Awk: divise un fichier volumineux en un fichier plus petit basé sur un motif

J'ai un gros fichier input.dat qui ressemble à l'illustration ci-dessous.

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

J'ai besoin de diviser le fichier en 2 plus petits comme ci-dessous

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

et 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

J'ai écrit un petit script pour le faire. Le script est présenté ci-dessous.

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

Le script crée des fichiers de sortie avec les noms souhaités. Mais tous les fichiers sont vides. Quelqu'un peut-il m'aider à résoudre ce problème?

4
Sruthil Lal S.B.

Cela peut être fait entièrement dans 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 supporte également > file pour la redirection, avec quelques différences subtiles (voir manuel de GNU awk pour plus).

3
muru

Bien que réponse de mur soit le plus simple, il existe plusieurs autres moyens sans utiliser awk.

Perl

L'approche avec awk consiste essentiellement à écrire dans un nom de fichier spécifique et à modifier ce nom de fichier si et seulement si nous rencontrons kpoint au début de la ligne. La même approche peut être faite avec Perl:

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

Voici comment cela fonctionne:

  • L'indicateur -a nous permet d'utiliser le tableau spécial @F de mots automatiquement séparés de chaque ligne du fichier d'entrée. Ainsi, $F[0] fait référence au premier mot, tout comme $1 in awk
  • $p=$F[0] if $F[0] =~ /kpoint/ est censé modifier $p (qui est censé être une variable de préfixe) si et seulement si kpoint est dans la ligne. L'amélioration de cette correspondance de motif pourrait être /^ *kpoint/
  • à chaque itération, nous ouvrons un fichier en ajoutant à un nom portant le nom $p associé à .dat chaîne; notez que l'ajout d'une partie est important. Si vous voulez que tout soit clair, vous voulez probablement vous débarrasser des anciens fichiers kpoint. Si nous voulons que le fichier soit toujours créé frais et écrasé, nous pouvons ré-écrire la commande originale en tant que:

    $ Perl -ane 'if ($F[0] =~ /kpoint/){$p=$F[0]; open($f,">",$p . ".dat")}; print $f $_' input.txt
    
  • Et enfin, print $f $_ imprime simplement le nom de fichier que nous avons ouvert.

split

D'après votre exemple, il apparaît que chaque entrée est composée de 5 lignes. Si cela est constant, nous pouvons diviser le fichier de cette façon, sans recourir à la correspondance de modèle avec split. Plus précisément cette commande:

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

Dans cette commande, les options sont les suivantes:

  • --additional-suffix=".dat" est le suffixe statique .dat qui sera ajouté à chaque fichier créé.
  • --numeric-suffixes=1 nous permettra d'ajouter des numéros de changement commençant par 1 à chaque nom de fichier
  • -l 5 permettra de fractionner le fichier d'entrée toutes les 5 lignes
  • input.txt est le fichier que nous essayons de scinder
  • kpoint sera le préfixe statique du nom de fichier

Et voici comment cela fonctionne dans la pratique:

$ 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

Facultativement, nous pourrions également ajouter --suffix-length=1 pour garder la longueur de chaque suffixe numérique plus courte, comme kpoint1 au lieu de kpoint01, mais cela pourrait poser problème si vous avez un grand nombre de kpoints.

awk alternative

Celui-ci est similaire à réponse de mur , sauf qu'ici nous utilisons une correspondance de modèle différente ainsi qu'une approche différente pour créer la variable de nom de fichier via sprintf()

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

Python

Alors que les approches awk et split sont plus courtes, d'autres outils tels que Python conviennent bien au traitement de texte, et nous pouvons les utiliser pour mettre en œuvre des solutions plus complètes mais fonctionnelles.

Le script ci-dessous fait exactement cela, et il repose sur l'idée de regarder en arrière dans la liste des lignes que nous sauvegardons. Le script conserve les lignes en attente jusqu'à ce qu'il rencontre kpoint au début de la ligne, ce qui signifie que nous avons atteint une nouvelle entrée et que nous devons également écrire l'entrée précédente dans son fichier respectif.

#!/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 __== '__main__': main()

Pur bash

Presque la même idée que l'approche Perl - nous continuons à tout écrire dans un nom de fichier spécifique et ne modifions le nom de fichier que lorsque nous trouvons une ligne avec kpoint dedans.

#!/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 ;
2