Método rápido de dividir string de arquivo de texto?

11

Eu tenho dois arquivos de texto: string.txt e lengths.txt

String.txt:

abcdefghijklmnopqrstuvwxyz

lengths.txt

5
4
10
7

Eu quero pegar o arquivo

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

Estou trabalhando com cerca de 28.000 entradas e elas variam entre 200 e 56.000 caracteres.

No momento, estou usando:

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

Mas é muito ineficiente. Alguma ideia melhor?

user3891532
fonte
Como cerca de str="$(cat string.txt)"; i=0; while read j; do echo "${file:$i:$j}"; i=$((i+j)); done <length.txt..seems rápido o suficiente como é feito apenas por shell ..
heemayl
Não é muito mais rápido para ser honesto. Ainda está demorando bastante tempo. Eu sou bastante novo em linux / programação; se você acha que existe um método mais rápido, não apenas usando o shell, estou aberto a idéias.
user3891532
4
Tente { while read l<&3; do head -c"$l"; echo; done 3<lengths.txt; } <String.txt.
jimmij
@jimmij, como sobre furar isso em uma resposta
Iruvar

Respostas:

7

Você pode fazer

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

Requer alguma explicação:

A idéia principal é usar { head ; } <filee deriva da resposta subestimada @mikeserv . No entanto, neste caso, precisamos usar muitos heads, para que o whileloop seja introduzido e um pouco de ajustes nos descritores de arquivo para passar para a headentrada de ambos os arquivos (arquivo String.txtcomo arquivo principal para processar e linhas length.txtcomo argumento para -copção) . A idéia é que o benefício na velocidade deve vir da não necessidade de buscar String.txtsempre que um comando é chamado headou cuté invocado. O echoé apenas para imprimir uma nova linha após cada iteração.

Quanto é mais rápido (se houver) e a adição >Entry_ientre linhas é deixada como exercício.

jimmij
fonte
Uso puro do redirecionamento de E / S. Desde que a tag é Linux, você pode razoavelmente assumir o shell é Bash e uso read -u 3de ler descritor 3.
Jonathan Leffler
@ JonathanLeffler, o Linux tem pouco a ver bash. A grande maioria dos sistemas baseados em Linux não tem bashinstalado (pense no Android e outros sistemas embarcados). bashsendo o shell mais lento de todos, a mudança para o bash provavelmente diminuirá o desempenho mais significativamente do que o pequeno ganho que a mudança de read <&3para read -u3pode trazer (que, em qualquer caso, será insignificante em comparação com o custo de executar um comando externo como head). Mudar para o ksh93 que foi headincorporado (e que suporta a -copção não padrão ) melhoraria muito mais o desempenho.
Stéphane Chazelas
Observe que o argumento de head -c(para as headimplementações em que essa opção não padrão está disponível) é um número de bytes, não caracteres. Isso faria diferença nos códigos de idioma de vários bytes.
Stéphane Chazelas
7

Geralmente, você não deseja usar loops de shell para processar texto . Aqui, eu usaria perl:

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

Esse é um comando que lê (com buffer muito mais eficiente do que o readcomando do shell que lê um byte (ou alguns bytes para arquivos regulares) por vez)) os dois arquivos apenas uma vez (sem armazená-los na memória), assim é será várias ordens de magnitude mais eficientes do que soluções que executam comandos externos em um loop de shell.

(adicione a -Copção se esses números tiverem o número de caracteres no código do idioma atual, em vez do número de bytes. Para caracteres ASCII, como na sua amostra, isso não fará diferença).

Stéphane Chazelas
fonte
Essa é uma reutilização complicada dos $_parâmetros de saída e de entrada read, mas reduz a contagem de bytes no script.
22815 Jonathan
Em um teste rápido (a amostra do OP repetiu 100000 vezes), acho que essa solução é cerca de 1200 vezes mais rápida que a do @ jimmij (0,3 segundos vs 6 minutos (com bash16 segundos com PATH=/opt/ast/bin:$PATH ksh93)).
Stéphane Chazelas 13/08/2015
6

bash, versão 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

resultado

>Entry_1
abcde
>Entry_2
fghi
>Entry_3
jklmnopqrs
>Entry_4
tuvwxyz
Glenn Jackman
fonte
4

Que tal awk?

Crie um arquivo chamado process.awkcom este código:

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

Salve e execute awk -f process.awk lengths.txt string.txt

jcbermu
fonte
Com base no uso de PROCINFO, isso não é padrão awk, mas gawk. Nesse caso, eu preferiria outro gawkrecurso único, o FIELDWIDTHS:awk -vFIELDWIDTHS="$(tr '\n' ' ' < lengths.txt)" '{for(i=1;i<=NF;i++)print">Entry"i ORS$i}' string.txt
manatwork