Divida um arquivo em vários arquivos com base no delimitador

86

Eu tenho um arquivo com -|como delimitador após cada seção ... preciso criar arquivos separados para cada seção usando unix.

exemplo de arquivo de entrada

wertretr
ewretrtret
1212132323
000232
-|
ereteertetet
232434234
erewesdfsfsfs
0234342343
-|
jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|

Resultado esperado no arquivo 1

wertretr
ewretrtret
1212132323
000232
-|

Resultado esperado no arquivo 2

ereteertetet
232434234
erewesdfsfsfs
0234342343
-|

Resultado esperado no arquivo 3

jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|
user1499178
fonte
1
Você está escrevendo um programa ou deseja fazer isso usando utilitários de linha de comando?
rkyser
1
utilizando utilitários de linha de comando será preferível ..
user1499178
Você poderia usar o awk, seria fácil escrever um programa de 3 ou 4 linhas para fazer isso. Infelizmente, estou sem prática.
ctrl-alt-delor

Respostas:

97

Uma linha, sem programação. (exceto o regexp etc.)

csplit --digits=2  --quiet --prefix=outfile infile "/-|/+1" "{*}"

testado em: csplit (GNU coreutils) 8.30

Notas sobre o uso no Apple Mac

"Para usuários do OS X, observe que a versão csplitque vem com o sistema operacional não funciona. Você vai querer a versão em coreutils (instalável via Homebrew), que é chamado gcsplit." - @Danial

"Só para adicionar, você pode fazer com que a versão do OS X funcione (pelo menos com High Sierra). Você só precisa ajustar um pouco as args csplit -k -f=outfile infile "/-\|/+1" "{3}". Os recursos que parecem não funcionar são "{*}", eu tive que ser específico o número de separadores e necessário adicionar -kpara evitar a exclusão de todos os arquivos de saída se não conseguir encontrar um separador final. Além disso, se você quiser --digits, será necessário usar -n. " - @Pebbl

ctrl-alt-delor
fonte
31
@ zb226 Fiz isso há muito tempo, então nenhuma explicação foi necessária.
ctrl-alt-delor
5
Sugiro adicionar --elide-empty-files, caso contrário, haverá um arquivo vazio no final.
luator de
8
Para usuários do OS X, observe que a versão do csplit que vem com o sistema operacional não funciona. Você vai querer a versão em coreutils (instalável via Homebrew), que é chamado gcsplit .
Daniel
10
Apenas para aqueles que se perguntam o que significam os parâmetros: --digits=2controla o número de dígitos usados ​​para numerar os arquivos de saída (2 é o padrão para mim, então não é necessário). --quietsuprime a saída (também não é realmente necessário ou solicitado aqui). --prefixespecifica o prefixo dos arquivos de saída (o padrão é xx). Assim, você pode pular todos os parâmetros e obter arquivos de saída como xx12.
Christopher K.
3
Só para adicionar, você pode obter a versão do OS X para funcionar (pelo menos com o High Sierra). Você só precisa ajustar um pouco os argumentos csplit -k -f=outfile infile "/-\|/+1" "{3}". Recursos que parecem não funcionar são o "{*}", eu tive que ser específico sobre o número de separadores, e precisava adicionar -kpara evitar a exclusão de todos os outfiles se não conseguir encontrar um separador final. Além disso, se quiser --digits, você precisa usar -n.
Pebbl de
38
awk '{f="file" NR; print $0 " -|"> f}' RS='-\\|'  input-file

Explicação (editado):

RSé o separador de registro e esta solução usa uma extensão gnu awk que permite ter mais de um caractere. NRé o número do registro.

A instrução print imprime um registro seguido por " -|"em um arquivo que contém o número do registro em seu nome.

William Pursell
fonte
1
RSé o separador de registro e esta solução usa uma extensão gnu awk que permite ter mais de um caractere. NR é o número do registro. A instrução print imprime um registro seguido por "- |" em um arquivo que contém o número do registro em seu nome.
William Pursell
1
@rzetterbeg Isso deve funcionar bem com arquivos grandes. awk processa o arquivo um registro por vez, então ele lê apenas o que é necessário. Se a primeira ocorrência do separador de registro aparecer muito tarde no arquivo, pode ser um problema de memória, pois um registro inteiro deve caber na memória. Além disso, observe que usar mais de um caractere no RS não é o awk padrão, mas funcionará no gnu awk.
William Pursell
4
Para mim, ele dividiu 3,3 GB em 31,728s
Cleankod
3
@ccf O nome do arquivo é apenas a string no lado direito do >, então você pode construí-lo como quiser. por exemplo,print $0 "-|" > "file" NR ".txt"
William Pursell,
1
@AGrush Depende da versão. Você pode fazerawk '{f="file" NR; print $0 " -|" > f}'
William Pursell
7

O Debian tem csplit, mas não sei se isso é comum a todas / à maioria / outras distribuições. Se não, porém, não deve ser muito difícil rastrear o código-fonte e compilá-lo ...

Twalberg
fonte
1
Concordo. Minha caixa Debian diz que csplit faz parte do gnu coreutils. Portanto, qualquer sistema operacional Gnu, como todas as distros Gnu / Linux, terá. A Wikipedia também menciona 'The Single UNIX® Specification, Issue 7' na página csplit, então eu suspeito que você entendeu.
ctrl-alt-delor
3
Uma vez que csplitestá no POSIX, eu esperaria que estivesse disponível essencialmente em todos os sistemas do tipo Unix.
Jonathan Leffler
1
Embora csplit seja POISX, o problema (parece que estou fazendo um teste com ele no sistema Ubuntu sentado na minha frente) é que não há uma maneira óbvia de fazê-lo usar uma sintaxe regex mais moderna. Compare: csplit --prefix gold-data - "/^==*$/vs csplit --prefix gold-data - "/^=+$/. Pelo menos GNU grep tem -e.
novo123456
5

Resolvi um problema um pouco diferente, onde o arquivo contém uma linha com o nome onde o texto a seguir deve ir. Este código perl faz o truque para mim:

#!/path/to/perl -w

#comment the line below for UNIX systems
use Win32::Clipboard;

# Get command line flags

#print ($#ARGV, "\n");
if($#ARGV == 0) {
    print STDERR "usage: ncsplit.pl --mff -- filename.txt [...] \n\nNote that no space is allowed between the '--' and the related parameter.\n\nThe mff is found on a line followed by a filename.  All of the contents of filename.txt are written to that file until another mff is found.\n";
    exit;
}

# this package sets the ARGV count variable to -1;

use Getopt::Long;
my $mff = "";
GetOptions('mff' => \$mff);

# set a default $mff variable
if ($mff eq "") {$mff = "-#-"};
print ("using file switch=", $mff, "\n\n");

while($_ = shift @ARGV) {
    if(-f "$_") {
    push @filelist, $_;
    } 
}

# Could be more than one file name on the command line, 
# but this version throws away the subsequent ones.

$readfile = $filelist[0];

open SOURCEFILE, "<$readfile" or die "File not found...\n\n";
#print SOURCEFILE;

while (<SOURCEFILE>) {
  /^$mff (.*$)/o;
    $outname = $1;
#   print $outname;
#   print "right is: $1 \n";

if (/^$mff /) {

    open OUTFILE, ">$outname" ;
    print "opened $outname\n";
    }
    else {print OUTFILE "$_"};
  }
John David Smith
fonte
Você pode explicar por que esse código funciona? Tenho uma situação semelhante à que você descreveu aqui - os nomes dos arquivos de saída necessários estão incorporados ao arquivo. Mas eu não sou um usuário perl comum, então não consigo entender esse código.
shiri
A verdadeira carne está no whilelaço final . Se encontrar o mffregex no início da linha, ele usará o resto da linha como o nome do arquivo para abrir e começar a escrever. Ele nunca fecha nada, então ficará sem identificadores de arquivo após algumas dezenas.
tripleee
O script seria realmente melhorado removendo a maior parte do código antes do whileloop final e mudando parawhile (<>)
triplo
4

O seguinte comando funciona para mim. Espero que ajude.

awk 'BEGIN{file = 0; filename = "output_" file ".txt"}
    /-|/ {getline; file ++; filename = "output_" file ".txt"}
    {print $0 > filename}' input
Thanh
fonte
1
Os identificadores de arquivo ficarão esgotados após algumas dezenas de arquivos. A correção é explicitamente closeo arquivo antigo ao iniciar um novo.
tripleee
@tripleee como você fechá-lo (pergunta para iniciante). Você pode fornecer um exemplo atualizado?
Jesper Rønn-Jensen
1
@ JesperRønn-Jensen Esta caixa é provavelmente muito pequena para qualquer exemplo útil, mas basicamente if (file) close(filename);antes de atribuir um novo filenamevalor.
tripleee
aah descobri como fechá-lo: ; close(filename). Muito simples, mas realmente corrige o exemplo acima
Jesper Rønn-Jensen
1
@ JesperRønn-Jensen Eu reverti sua edição porque você forneceu um script corrompido. Edições significativas nas respostas de outras pessoas provavelmente devem ser evitadas - sinta-se à vontade para postar uma nova resposta de sua preferência (talvez como um wiki da comunidade ) se você achar que uma resposta separada é merecida.
tripleee
2

Você também pode usar o awk. Não estou muito familiarizado com o awk, mas as opções a seguir parecem funcionar para mim. Ele gerou part1.txt, part2.txt, part3.txt e part4.txt. Observe que o último arquivo partn.txt gerado está vazio. Não sei como consertar isso, mas tenho certeza de que poderia ser feito com um pequeno ajuste. Alguma sugestão de alguém?

arquivo awk_pattern:

BEGIN{ fn = "part1.txt"; n = 1 }
{
   print > fn
   if (substr($0,1,2) == "-|") {
       close (fn)
       n++
       fn = "part" n ".txt"
   }
}

comando bash:

awk -f awk_pattern input.file

rkyser
fonte
2

Aqui está um script Python 3 que divide um arquivo em vários arquivos com base em um nome de arquivo fornecido pelos delimitadores. Arquivo de entrada de exemplo:

# Ignored

######## FILTER BEGIN foo.conf
This goes in foo.conf.
######## FILTER END

# Ignored

######## FILTER BEGIN bar.conf
This goes in bar.conf.
######## FILTER END

Aqui está o script:

#!/usr/bin/env python3

import os
import argparse

# global settings
start_delimiter = '######## FILTER BEGIN'
end_delimiter = '######## FILTER END'

# parse command line arguments
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input-file", required=True, help="input filename")
parser.add_argument("-o", "--output-dir", required=True, help="output directory")

args = parser.parse_args()

# read the input file
with open(args.input_file, 'r') as input_file:
    input_data = input_file.read()

# iterate through the input data by line
input_lines = input_data.splitlines()
while input_lines:
    # discard lines until the next start delimiter
    while input_lines and not input_lines[0].startswith(start_delimiter):
        input_lines.pop(0)

    # corner case: no delimiter found and no more lines left
    if not input_lines:
        break

    # extract the output filename from the start delimiter
    output_filename = input_lines.pop(0).replace(start_delimiter, "").strip()
    output_path = os.path.join(args.output_dir, output_filename)

    # open the output file
    print("extracting file: {0}".format(output_path))
    with open(output_path, 'w') as output_file:
        # while we have lines left and they don't match the end delimiter
        while input_lines and not input_lines[0].startswith(end_delimiter):
            output_file.write("{0}\n".format(input_lines.pop(0)))

        # remove end delimiter if present
        if not input_lines:
            input_lines.pop(0)

Finalmente, aqui está como você o executa:

$ python3 script.py -i input-file.txt -o ./output-folder/
ctrlc-root
fonte
2

Use csplitse você tiver.

Se você não tem, mas tem Python ... não use Perl.

Leitura lenta do arquivo

Seu arquivo pode ser muito grande para ser guardado na memória de uma vez - ler linha por linha pode ser preferível. Suponha que o arquivo de entrada seja denominado "samplein":

$ python3 -c "from itertools import count
with open('samplein') as file:
    for i in count():
        firstline = next(file, None)
        if firstline is None:
            break
        with open(f'out{i}', 'w') as out:
            out.write(firstline)
            for line in file:
                out.write(line)
                if line == '-|\n':
                    break"
Aaron Hall
fonte
Isso lerá todo o arquivo na memória, o que significa que será ineficiente ou até mesmo falhará para arquivos grandes.
tripleee
1
@tripleee Eu atualizei a resposta para lidar com arquivos muito grandes.
Aaron Hall
0
cat file| ( I=0; echo -n "">file0; while read line; do echo $line >> file$I; if [ "$line" == '-|' ]; then I=$[I+1]; echo -n "" > file$I; fi; done )

e a versão formatada:

#!/bin/bash
cat FILE | (
  I=0;
  echo -n"">file0;
  while read line; 
  do
    echo $line >> file$I;
    if [ "$line" == '-|' ];
    then I=$[I+1];
      echo -n "" > file$I;
    fi;
  done;
)
mbonnin
fonte
4
Como sempre, o caté inútil .
tripleee
1
@Reishin A página vinculada explica com muito mais detalhes como você pode evitar catem um único arquivo em todas as situações. Há uma pergunta sobre Stack Overflow com mais discussão (embora a resposta aceita seja IMHO desativado); stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee
1
O shell é normalmente muito ineficiente nesse tipo de coisa; se você não puder usar csplit, uma solução Awk é provavelmente muito preferível a esta solução (mesmo se você fosse corrigir os problemas relatados por shellcheck.net etc; note que atualmente ele não encontra todos os bugs nele).
tripleee
@tripleee mas se a tarefa for fazer sem awk, csplit e etc - apenas bash?
Reishin
1
Então, o catainda é inútil, e o resto do script pode ser bastante simplificado e corrigido; mas ainda será lento. Veja, por exemplo, stackoverflow.com/questions/13762625/…
tripleee
0

Este é o tipo de problema para o qual escrevi divisão de contexto: http://stromberg.dnsalias.org/~strombrg/context-split.html

$ ./context-split -h
usage:
./context-split [-s separator] [-n name] [-z length]
        -s specifies what regex should separate output files
        -n specifies how output files are named (default: numeric
        -z specifies how long numbered filenames (if any) should be
        -i include line containing separator in output files
        operations are always performed on stdin
user1277476
fonte
Uh, isso parece essencialmente uma duplicata do csplitutilitário padrão . Veja a resposta de @richard .
tripleee
Esta é realmente a melhor solução para mim. Eu tive que dividir um dump do mysql 98G e csplit por algum motivo consome toda a minha RAM e é morto. Mesmo que ele precise corresponder apenas a uma linha de cada vez. Não faz sentido. Este script python funciona muito melhor e não consome toda a memória RAM.
Stefan Midjich de
0

Aqui está um código perl que fará a coisa

#!/usr/bin/perl
open(FI,"file.txt") or die "Input file not found";
$cur=0;
open(FO,">res.$cur.txt") or die "Cannot open output file $cur";
while(<FI>)
{
    print FO $_;
    if(/^-\|/)
    {
        close(FO);
        $cur++;
        open(FO,">res.$cur.txt") or die "Cannot open output file $cur"
    }
}
close(FO);
amaksr
fonte