Como posso dividir um arquivo de texto em vários arquivos de texto?


16

Eu tenho um arquivo de texto chamado entry.txtque contém o seguinte:

[ entry1 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3633 3634 3636 3690 3691 3693 3766
3767 3769 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5628 5629 5631
[ entry2 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4526
4527 4529 4583 4584 4586 4773 4774 4776 5153 5154
5156 5628 5629 5631
[ entry3 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4241
4242 4244 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5495 5496 5498 5628 5629 5631

Gostaria de dividi-lo em três arquivos de texto: entry1.txt, entry2.txt, entry3.txt. Seu conteúdo é o seguinte.

entry1.txt :

[ entry1 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3633 3634 3636 3690 3691 3693 3766
3767 3769 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5628 5629 5631

entry2.txt :

[ entry2 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4526
4527 4529 4583 4584 4586 4773 4774 4776 5153 5154
5156 5628 5629 5631

entry3.txt :

[ entry3 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4241
4242 4244 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5495 5496 5498 5628 5629 5631

Em outras palavras, o [caractere indica que um novo arquivo deve começar. As entradas ( [ entry*], onde *é um número inteiro) estão sempre em ordem numérica e são números inteiros consecutivos começando de 1 a N (no meu arquivo de entrada real, N = 200001).

Existe alguma maneira de realizar a divisão automática de arquivos de texto no bash? entry.txtNa verdade, minha entrada real contém 200.001 entradas.

Respostas:


11

E aqui está uma linha simples agradável, simples e empolgada:

$ gawk '/^\[/{match($0, /^\[ (.+?) \]/, k)} {print >k[1]".txt" }' entry.txt

Isso funcionará para qualquer tamanho de arquivo, independentemente do número de linhas em cada entrada, desde que pareça o cabeçalho de cada entrada [ blahblah blah blah ]. Observe o espaço logo após a abertura [e logo antes do fechamento ].


EXPLICAÇÃO:

awke gawkleia um arquivo de entrada linha por linha. À medida que cada linha é lida, seu conteúdo é salvo na $0variável. Aqui, estamos dizendo gawkpara combinar qualquer coisa entre colchetes e salvar sua correspondência na matriz k.

Portanto, toda vez que a expressão regular for correspondida, ou seja, para cada cabeçalho do seu arquivo, k [1] terá a região correspondente da linha. Ou seja, "entrada1", "entrada2" ou "entrada3" ou "entradaN".

Finalmente, imprimimos cada linha em um arquivo chamado <whatever value k currently has>.txt, ie entry1.txt, entry2.txt ... entryN.txt.

Este método será muito mais rápido do que Perl para arquivos maiores.


+1 legal. Você não precisa matchda entrada: /^\[/ { name=$2 }deve ser suficiente.
Thor

Obrigado @Thor. Sua sugestão está correta para o caso descrito, mas assume que nunca há um espaço no nome da entrada. É por isso que eu usei o exemplo [ blahblah blah blah ]na minha resposta.
terdon

Ah, eu perdi um pouco sobre entradas separadas por espaço. Você também pode acomodar pessoas com FS, por exemplo -F '\\[ | \\]'.
Thor

@terdon Eu realmente gosto dessas soluções curtas, infelizmente geralmente não consigo generalizá-las para as minhas necessidades. Você poderia me ajudar? Meu arquivo tem linhas começando com#S x , onde x é um número 1, 2, ou 3 dígitos. Apenas salvá-los no x.dat seria suficiente. Eu tentei: gawk '/^#S/{match($0, / [0-9]* /, k)} {print >k[1]".dat" }' myFile.txte algumas variações disso.
Mikuszefski

Entendi gawk '/^#S/{match($0, /^#S (\s+?)([0-9]+)(\s+?)/, k)} {print >k[2]".txt" }' test.txtfez o truque. 2Porém, não entendo muito bem o número da matriz .
Mikuszefski

17

Com o csplit do GNU coreutils (Linux não incorporado, Cygwin):

csplit -f entry -b '%d.txt' entry.txt '/^\[ .* \]$/' '{*}'

Você terminará com um arquivo vazio extra entry0.txt(contendo a parte antes do primeiro cabeçalho).

O csplit padrão não possui o {*}repetidor indefinido e a -bopção de especificar o formato do sufixo; portanto, em outros sistemas, você terá que contar primeiro o número de seções e renomear os arquivos de saída posteriormente.

csplit -f entry -n 9 entry.txt '/^\[ .* \]$/' "{$(egrep -c '^'\[ .* \]$' <entry.txt)}"
for x in entry?????????; do
  y=$((1$x - 1000000000))
  mv "entry$x" "entry$y.txt"
done

Acho que o csplit é um pouco peculiar de vez em quando, mas incrivelmente útil quando eu quero fazer esse tipo de coisa.
Ixtmixilix

10

No perl, isso pode ser muito mais simples:

perl -ne 'open(F, ">", ($1).".txt") if /\[ (entry\d+) \]/; print F;' file

9

Aqui está uma breve linha de código awk:

awk '/^\[/ {ofn=$2 ".txt"} ofn {print > ofn}' input.txt

Como é que isso funciona?

  • /^\[/ corresponde a linhas que começam com um colchete esquerdo e
  • {ofn=$2 ".txt"}define uma variável para a segunda palavra delimitada por espaços em branco como o nome do arquivo de saída. Então,
  • ofn é uma condição que é avaliada como verdadeira se a variável estiver definida (fazendo com que as linhas antes do primeiro cabeçalho sejam ignoradas)
  • {print > ofn} redireciona a linha atual para o arquivo especificado.

Observe que todos os espaços neste script awk podem ser removidos, se a compactação o deixar feliz.

Observe também que o script acima realmente precisa que os cabeçalhos das seções tenham espaços ao redor e não dentro deles. Se você quiser lidar com cabeçalhos de seção como [foo]e [ this that ], precisará de um pouco mais de código:

awk '/^\[/ {sub(/^\[ */,""); sub(/ *\] *$/,""); ofn=$0 ".txt"} ofn {print > ofn}' input.txt

Isso usa a sub()função do awk para remover espaços à esquerda e à esquerda entre colchetes e espaços em branco. Observe que, de acordo com o comportamento padrão do awk, isso recolhe o espaço em branco (o separador de campos) em um único espaço (ou seja, [ this that ]é salvo em "this that.txt"). Se a manutenção do espaço em branco original nos nomes dos arquivos de saída for importante, experimente definir FS.


2

Isso pode ser feito na linha de comando em python como:

paddy$ python3 -c 'out=0
> with open("entry.txt") as f: 
>   for line in f:
>     if line[0] == "[":
>       if out: out.close()
>       out = open(line.split()[1] + ".txt", "w")
>     else: out.write(line)'

2

Essa é uma maneira um tanto grosseira, mas fácil de entender: use grep -l '[ entry ]' FILENAMEpara fazer com que os números das linhas sejam divididos em [entrada]. Use uma combinação da cabeça e cauda para obter as peças certas.

Como eu disse; não é bonito, mas é fácil de entender.


2

Que tal usar o awk [como separador de registros e o espaço como separador de campos. Isso nos fornece facilmente os dados a serem colocados no arquivo como $0onde ele deve colocar de volta a liderança removida [e o nome do arquivo como $1. Só precisamos lidar com o caso especial do 1º registro que está vazio. Isso nos dá:

awk -v "RS=[" -F " " 'NF != 0 {print "[" $0 > $1}' entry.txt

2

A resposta de Terdon funciona para mim, mas eu precisava usar o gawk, não o awk. O manual do gawk (procure por 'match (') explica que o argumento do array em match () é uma extensão gawk. Talvez dependa da instalação do Linux e das versões awk / nawk / gawk, mas na minha máquina Ubuntu apenas o gawk executou o excelente responda:

$ gawk '{if(match($0, /^\[ (.+?) \]/, k)){name=k[1]}} {print >name".txt" }' entry.txt

1

Aqui está uma solução perl. Este script detecta as [ entryN ]linhas e altera o arquivo de saída de acordo, mas não valida, analisa ou processa os dados em cada seção, apenas imprime a linha de entrada no arquivo de saída.

#! /usr/bin/perl 

# default output file is /dev/null - i.e. dump any input before
# the first [ entryN ] line.

$outfile='/dev/null';
open(OUTFILE,">",$outfile) || die "couldn't open $outfile: $!";

while(<>) {
  # uncomment next two lines to optionally remove comments (starting with
  # '#') and skip blank lines.  Also removes leading and trailing
  # whitespace from each line.
  # s/#.*|^\s*|\s*$//g;
  # next if (/^$/)

  # if line begins with '[', extract the filename
  if (m/^\[/) {
    (undef,$outfile,undef) = split ;
    close(OUTFILE);
    open(OUTFILE,">","$outfile.txt") || die "couldn't open $outfile.txt: $!";
  } else {
    print OUTFILE;
  }
}
close(OUTFILE);

1

Oi, eu escrevi esse script simples usando ruby ​​para resolver seu problema

#!ruby
# File Name: split.rb

fout = nil

while STDIN.gets
  line = $_
  if line.start_with? '['
    fout.close if fout
    fname = line.split(' ')[1] + '.txt'
    fout = File.new fname,'w'
  end
  fout.write line if fout
end

fout.close if fout

você pode usá-lo desta maneira:

ruby split.rb < entry.txt

eu testei e funciona bem ..


1

Eu prefiro a csplitopção, mas como alternativa, aqui está uma solução GNU awk:

parse.awk

BEGIN { 
  RS="\\[ entry[0-9]+ \\]\n"  # Record separator
  ORS=""                      # Reduce whitespace on output
}
NR == 1 { f=RT }              # Entries are of-by-one relative to matched RS
NR  > 1 {
  split(f, a, " ")            # Assuming entries do not have spaces 
  print f  > a[2] ".txt"      # a[2] now holds the bare entry name
  print   >> a[2] ".txt"
  f = RT                      # Remember next entry name
}

Execute-o assim:

gawk -f parse.awk entry.txt

11
FWIW, a RTvariável parece ser específica ao Gawk . Esta solução não funciona para mim usando o awk do FreeBSD.
ghoti 08/07/2015

@hothot: Certo, eu deveria ter mencionado isso. Eu incluí isso na resposta agora. Obrigado.
Thor
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.