Dividi un file in più file in base al delimitatore


88

Ho un file con -|come delimitatore dopo ogni sezione ... è necessario creare file separati per ogni sezione utilizzando unix.

esempio di file di input

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

Risultato previsto nel file 1

wertretr
ewretrtret
1212132323
000232
-|

Risultato previsto nel file 2

ereteertetet
232434234
erewesdfsfsfs
0234342343
-|

Risultato previsto nel file 3

jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|

1
Stai scrivendo un programma o vuoi farlo usando le utilità della riga di comando?
rkyser

1
sarà preferibile utilizzare le utilità della riga di comando ..
user1499178

Potresti usare awk, sarebbe facile scrivere un programma a 3 o 4 righe per farlo. Purtroppo sono fuori allenamento.
ctrl-alt-delor

Risposte:


98

Una linea, nessuna programmazione. (tranne la regexp ecc.)

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

testato su: csplit (GNU coreutils) 8.30

Note sull'utilizzo su Apple Mac

"Per gli utenti di OS X, nota che la versione csplitfornita con il sistema operativo non funziona. Ti consigliamo la versione in coreutils (installabile tramite Homebrew), che si chiama gcsplit." - @Danial

"Solo per aggiungere, puoi far funzionare la versione per OS X (almeno con High Sierra). Devi solo modificare un po 'gli argomenti csplit -k -f=outfile infile "/-\|/+1" "{3}". Le caratteristiche che non sembrano funzionare sono "{*}", dovevo essere specifico su il numero di separatori e necessario aggiungere -kper evitare che cancelli tutti i file in uscita se non riesce a trovare un separatore finale. Inoltre, se lo desideri --digits, devi -ninvece usarlo . " - @Pebbl


31
@ zb226 L'ho fatto a lungo, quindi non era necessaria alcuna spiegazione.
ctrl-alt-delor

5
Suggerisco di aggiungere --elide-empty-files, altrimenti ci sarà un file vuoto alla fine.
luator

8
Per gli utenti di OS X, nota che la versione di csplit fornita con il sistema operativo non funziona. Ti consigliamo la versione in coreutils (installabile tramite Homebrew), che si chiama gcsplit .
Daniel

10
Solo per coloro che si chiedono cosa significano i parametri: --digits=2controlla il numero di cifre utilizzate per numerare i file di output (2 è l'impostazione predefinita per me, quindi non necessario). --quietsopprime l'output (anche non realmente necessario o richiesto qui). --prefixspecifica il prefisso dei file di output (il valore predefinito è xx). Quindi puoi saltare tutti i parametri e otterrai file di output come xx12.
Christopher K.

3
Solo per aggiungere, puoi far funzionare la versione per OS X (almeno con High Sierra). Hai solo bisogno di modificare un po 'gli argomenti csplit -k -f=outfile infile "/-\|/+1" "{3}". Le caratteristiche che non sembrano funzionare sono "{*}", dovevo essere specifico sul numero di separatori e dovevo aggiungere -kper evitare che cancellasse tutti i file in uscita se non riesce a trovare un separatore finale. Inoltre, se vuoi --digits, devi usare -ninvece.
Pebbl

39
awk '{f="file" NR; print $0 " -|"> f}' RS='-\\|'  input-file

Spiegazione (modificata):

RSè il separatore di record e questa soluzione utilizza un'estensione gnu awk che gli consente di contenere più di un carattere. NRè il numero di record.

L'istruzione print stampa un record seguito da " -|"in un file che contiene il numero di record nel suo nome.


1
RSè il separatore di record e questa soluzione utilizza un'estensione gnu awk che gli consente di contenere più di un carattere. NR è il numero di record. L'istruzione print stampa un record seguito da "- |" in un file che contiene il numero di record nel suo nome.
William Pursell

1
@rzetterbeg Questo dovrebbe funzionare bene con file di grandi dimensioni. awk elabora il file un record alla volta, quindi legge solo quanto necessario. Se la prima occorrenza del separatore di record viene visualizzata molto tardi nel file, potrebbe trattarsi di un crunch di memoria poiché un intero record deve rientrare nella memoria. Inoltre, nota che l'uso di più di un carattere in RS non è awk standard, ma funzionerà in gnu awk.
William Pursell

4
Per me ha diviso 3,3 GB in 31,728
Cleankod

3
@ccf Il nome del file è solo la stringa sul lato destro di >, quindi puoi costruirlo come preferisci. ad esempio,print $0 "-|" > "file" NR ".txt"
William Pursell

1
@AGrush Dipende dalla versione. Puoi fareawk '{f="file" NR; print $0 " -|" > f}'
William Pursell

7

Debian lo ha csplit, ma non so se sia comune a tutte / la maggior parte / altre distribuzioni. In caso contrario, però, non dovrebbe essere troppo difficile rintracciare il sorgente e compilarlo ...


1
Sono d'accordo. La mia macchina Debian dice che csplit fa parte di gnu coreutils. Quindi qualsiasi sistema operativo Gnu, come tutte le distribuzioni Gnu / Linux, lo avrà. Wikipedia menziona anche "The Single UNIX® Specification, Issue 7" sulla pagina csplit, quindi sospetto che tu abbia capito.
ctrl-alt-delor

3
Dato che csplitè in POSIX, mi aspetto che sia disponibile essenzialmente su tutti i sistemi Unix-like.
Jonathan Leffler

1
Sebbene csplit sia POISX, il problema (sembra fare un test con esso sul sistema Ubuntu seduto di fronte a me) è che non esiste un modo ovvio per far sì che utilizzi una sintassi regex più moderna. Confronta: csplit --prefix gold-data - "/^==*$/vs csplit --prefix gold-data - "/^=+$/. Almeno GNU grep ha -e.
123456

5

Ho risolto un problema leggermente diverso, in cui il file contiene una riga con il nome dove dovrebbe andare il testo che segue. Questo codice perl fa il trucco per me:

#!/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 "$_"};
  }

Puoi spiegare perché questo codice funziona? Ho una situazione simile a quella che hai descritto qui: i nomi dei file di output richiesti sono incorporati all'interno del file. Ma non sono un utente regolare di Perl, quindi non riesco a dare un senso a questo codice.
shiri

La vera carne è nel whileciclo finale . Se trova la mffregex all'inizio della riga, utilizza il resto della riga come nome del file per aprire e iniziare a scrivere. Non chiude mai nulla, quindi finirà gli handle di file dopo poche dozzine.
tripla

Lo script verrebbe effettivamente migliorato rimuovendo la maggior parte del codice prima del whileciclo finale e passando awhile (<>)
tripla

4

Il seguente comando funziona per me. Spero che sia d'aiuto.

awk 'BEGIN{file = 0; filename = "output_" file ".txt"}
    /-|/ {getline; file ++; filename = "output_" file ".txt"}
    {print $0 > filename}' input

1
Questo esaurirà gli handle di file dopo in genere poche dozzine di file. La correzione è esplicitamente closeil vecchio file quando si avvia uno nuovo.
tripla

@ tripleee come si chiude (domanda awk per principianti). Potete fornire un esempio aggiornato?
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Questa casella è probabilmente troppo piccola per qualsiasi esempio utile, ma fondamentalmente if (file) close(filename);prima di assegnare un nuovo filenamevalore.
tripleee

aah ha trovato il modo di chiuderlo: ; close(filename). Veramente semplice, ma risolve davvero l'esempio sopra
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Ho annullato la tua modifica perché hai fornito uno script rotto. Probabilmente dovrebbero essere evitate modifiche significative alle risposte di altre persone: sentiti libero di pubblicare una tua nuova risposta (magari come wiki della comunità ) se pensi che una risposta separata sia meritata.
tripla

2

Puoi anche usare awk. Non ho molta familiarità con awk, ma quanto segue sembra funzionare per me. Ha generato part1.txt, part2.txt, part3.txt e part4.txt. Nota che l'ultimo file partn.txt che questo genera è vuoto. Non sono sicuro di come risolverlo, ma sono sicuro che potrebbe essere fatto con un piccolo ritocco. Qualcuno ha suggerimenti?

file 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


2

Ecco uno script Python 3 che divide un file in più file in base a un nome file fornito dai delimitatori. File di input di esempio:

# Ignored

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

# Ignored

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

Ecco lo 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 ecco come lo esegui:

$ python3 script.py -i input-file.txt -o ./output-folder/

2

Usalo csplitse ce l'hai.

Se non lo fai, ma hai Python ... non usare Perl.

Lettura pigra del file

Il tuo file potrebbe essere troppo grande per essere conservato in memoria tutto in una volta - la lettura riga per riga potrebbe essere preferibile. Supponiamo che il file di input si chiami "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"

Questo leggerà l'intero file in memoria, il che significa che sarà inefficiente o addirittura fallirà per file di grandi dimensioni.
tripla

1
@ tripleee Ho aggiornato la risposta per gestire file molto grandi.
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 la versione formattata:

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

4
Come sempre, l' catè inutile .
tripleee

1
@Reishin La pagina collegata spiega in modo molto più dettagliato come puoi evitare catsu un singolo file in ogni situazione. C'è una domanda Stack Overflow con più discussioni (anche se la risposta accettata è IMHO disattivata); stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

1
La shell è comunque molto inefficiente in questo genere di cose; se non puoi usare csplit, una soluzione Awk è probabilmente di gran lunga preferibile a questa soluzione (anche se dovessi risolvere i problemi segnalati da shellcheck.net ecc; nota che attualmente non trova tutti i bug in questo).
tripleee

@ tripleee ma se il compito è farlo senza awk, csplit e così via - solo bash?
Reishin

1
Allora catè ancora inutile, e il resto della sceneggiatura potrebbe essere semplificato e corretto molto; ma sarà comunque lento. Vedi ad esempio stackoverflow.com/questions/13762625/…
tripleee

0

Questo è il tipo di problema per cui ho scritto la suddivisione del contesto: 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

Uh, sembra essenzialmente un duplicato csplitdell'utilità standard . Vedi la risposta di @ richard .
tripleee

Questa è in realtà la migliore soluzione imo. Ho dovuto dividere un dump mysql 98G e csplit per qualche motivo consuma tutta la mia RAM e viene ucciso. Anche se dovrebbe essere necessario abbinare solo una riga alla volta. Non ha senso. Questo script Python funziona molto meglio e non consuma tutta la ram.
Stefan Midjich

0

Ecco un codice Perl che farà la cosa

#!/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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.