Méthode rapide pour séparer la chaîne du fichier texte?

11

J'ai deux fichiers texte: string.txt et lengths.txt

String.txt:

abcdefghijklmnopqrstuvwxyz

lengths.txt

5
4
10
7

Je veux obtenir le fichier

>Entry_1
abcde
>Entry_2
fghi
>Entry_3
jklmnopqrs
>Entry_4
tuvwxyz

Je travaille avec environ 28 000 entrées et elles varient entre 200 et 56 000 caractères.

En ce moment, j'utilise:

start=1
end=0
i=0
while read read_l
do
    let i=i+1
    let end=end+read_l
    echo -e ">Entry_$i" >>outfile.txt
    echo "$(cut -c$start-$end String.txt)" >>outfile.txt
    let start=start+read_l
    echo $i
done <lengths.txt

Mais c'est très inefficace. De meilleures idées?

user3891532
la source
Que diriez-vous str="$(cat string.txt)"; i=0; while read j; do echo "${file:$i:$j}"; i=$((i+j)); done <length.txt.. semble assez rapide comme fait uniquement par shell ..
heemayl
Pour être honnête, ce n'est pas beaucoup plus rapide. Cela prend encore assez de temps. Je suis assez nouveau sur linux / programmation, donc si vous pensez qu'il existe une méthode plus rapide non seulement en utilisant le shell, je suis ouvert aux idées.
user3891532
4
Essayez { while read l<&3; do head -c"$l"; echo; done 3<lengths.txt; } <String.txt.
jimmij
@jimmij, que diriez-vous de coller cela dans une réponse
iruvar

Réponses:

7

Tu peux faire

{
  while read l<&3; do
    {
      head -c"$l"
      echo
    } 3<&-
  done 3<lengths.txt
} <String.txt

Cela nécessite quelques explications:

L'idée principale est d'utiliser { head ; } <fileet est dérivée de la réponse @mikeserv sous-estimée . Cependant, dans ce cas, nous devons utiliser de nombreux heads, donc une whileboucle est introduite et un peu de peaufinage avec les descripteurs de fichiers afin de passer à l' headentrée des deux fichiers (fichier String.txtcomme fichier principal à traiter et lignes depuis length.txtcomme argument à -coption) . L'idée est que l'avantage de la vitesse devrait provenir de ne pas avoir à chercher à String.txtchaque fois qu'une commande comme headou cutest invoquée. Il echosuffit d'imprimer la nouvelle ligne après chaque itération.

Combien il est plus rapide (le cas échéant) et l'ajout >Entry_ientre les lignes est laissé comme un exercice.

jimmij
la source
Utilisation soignée de la redirection d'E / S. Puisque la balise est Linux, vous pouvez raisonnablement supposer que le shell est Bash et utiliser read -u 3pour lire à partir du descripteur 3.
Jonathan Leffler
@JonathanLeffler, Linux n'a pas grand-chose à voir avec bash. La grande majorité des systèmes basés sur Linux ne sont pas bashinstallés (pensez à Android et autres systèmes embarqués). bashétant le shell le plus lent de tous, le passage à bash dégradera probablement les performances de manière plus significative que le petit gain que le passage de read <&3à read -u3pourrait apporter (ce qui en tout cas sera insignifiant par rapport au coût d'exécution d'une commande externe comme head). Passer à ksh93 qui a headintégré (et celui qui prend en charge l' -coption non standard ) améliorerait beaucoup plus les performances.
Stéphane Chazelas
Notez que l'argument de head -c(pour les headimplémentations où cette option non standard est disponible) est un nombre d'octets, pas de caractères. Cela ferait une différence dans les paramètres régionaux multi-octets.
Stéphane Chazelas
7

Généralement, vous ne voulez pas utiliser de boucles shell pour traiter du texte . Ici, j'utiliserais perl:

$ perl -lpe 'read STDIN,$_,$_; print ">Entry_" . ++$n' lengths.txt < string.txt
>Entry_1
abcde
>Entry_2
fghi
>Entry_3
jklmnopqrs
>Entry_4
tuvwxyz

C'est une commande, qui lit (avec une mise en mémoire tampon beaucoup plus efficace que la readcommande du shell qui lit un octet (ou quelques octets pour les fichiers normaux) à la fois) les deux fichiers une seule fois (sans les stocker en mémoire), tout comme va être plusieurs ordres de grandeur plus efficace que les solutions qui exécutent des commandes externes dans une boucle shell.

(ajoutez l' -Coption si ces nombres doivent être des nombres de caractères dans les paramètres régionaux actuels par opposition au nombre d'octets. Pour les caractères ASCII comme dans votre exemple, cela ne fera aucune différence).

Stéphane Chazelas
la source
Il s'agit d'une réutilisation compliquée de $_paramètres de sortie et d'entrée read, mais cela réduit le nombre d'octets dans le script.
Jonathan Leffler
Dans un test rapide (l'échantillon de l'OP est répété 100000 fois), je trouve que cette solution est environ 1200 fois plus rapide que celle de @ jimmij (0,3 seconde vs 6 minutes (avec bash, 16 secondes avec PATH=/opt/ast/bin:$PATH ksh93)).
Stéphane Chazelas
6

bash, version 4

mapfile -t lengths <lengths.txt
string=$(< String.txt)
i=0 
n=0
for len in "${lengths[@]}"; do
    echo ">Entry_$((++n))"
    echo "${string:i:len}"
    ((i+=len))
done

production

>Entry_1
abcde
>Entry_2
fghi
>Entry_3
jklmnopqrs
>Entry_4
tuvwxyz
glenn jackman
la source
4

Et alors awk?

Créez un fichier appelé process.awkavec ce code:

function idx(i1, v1, i2, v2)
{
     # numerical index comparison, ascending order
     return (i1 - i2)
}
FNR==NR { a[FNR]=$0; next }
{ i=1;PROCINFO["sorted_in"] = "idx";
        for (j in a) {
                print ">Entry"j;
                ms=substr($0, i,a[j])
                print ms
                i=i+length(ms)
        }
}

Enregistrez-le et exécutez awk -f process.awk lengths.txt string.txt

jcbermu
la source
Basé sur l'utilisation de PROCINFO, ce n'est pas standard awk, mais gawk. Dans ce cas, je préférerais une autre gawkseule fonctionnalité, le FIELDWIDTHS:awk -vFIELDWIDTHS="$(tr '\n' ' ' < lengths.txt)" '{for(i=1;i<=NF;i++)print">Entry"i ORS$i}' string.txt
manatwork