Teilen Sie eine Datei basierend auf dem Trennzeichen in mehrere Dateien auf


85

Ich habe eine Datei mit einem -|Trennzeichen nach jedem Abschnitt ... muss mit Unix separate Dateien für jeden Abschnitt erstellen.

Beispiel einer Eingabedatei

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

Erwartetes Ergebnis in Datei 1

wertretr
ewretrtret
1212132323
000232
-|

Erwartetes Ergebnis in Datei 2

ereteertetet
232434234
erewesdfsfsfs
0234342343
-|

Erwartetes Ergebnis in Datei 3

jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|

1
Schreiben Sie ein Programm oder möchten Sie dies mit Befehlszeilenprogrammen tun?
Rkyser

1
Die Verwendung von Befehlszeilenprogrammen ist vorzuziehen.
user1499178

Sie könnten awk verwenden, es wäre einfach, ein 3- oder 4-Zeilen-Programm zu schreiben, um dies zu tun. Leider bin ich außer Übung.
Strg-Alt-Delor

Antworten:


97

Ein Einzeiler, keine Programmierung. (außer dem regulären Ausdruck usw.)

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

getestet am: csplit (GNU coreutils) 8.30

Hinweise zur Verwendung auf Apple Mac

"Beachten Sie für OS X-Benutzer, dass die csplitmit dem Betriebssystem gelieferte Version nicht funktioniert. Sie möchten die Version in coreutils (über Homebrew installierbar), die aufgerufen wird gcsplit." - @Danial

"Nur um hinzuzufügen, Sie können die Version für OS X zum Laufen bringen (zumindest mit High Sierra). Sie müssen nur die Argumente ein wenig optimieren csplit -k -f=outfile infile "/-\|/+1" "{3}". Funktionen, die nicht zu funktionieren scheinen, sind die "{*}", auf die ich mich genau konzentrieren musste die Anzahl der Trennzeichen, die hinzugefügt werden müssen -k, um zu vermeiden, dass alle Outfiles gelöscht werden, wenn kein endgültiges Trennzeichen gefunden wird. Auch wenn Sie möchten --digits, müssen Sie -nstattdessen verwenden. " - @Pebbl


31
@ zb226 Ich habe es lange gemacht, so dass keine Erklärung nötig war.
Strg-Alt-Delor

5
Ich schlage vor, hinzuzufügen --elide-empty-files, sonst wird am Ende eine leere Datei angezeigt.
Luator

8
Beachten Sie für OS X-Benutzer, dass die mit dem Betriebssystem gelieferte Version von csplit nicht funktioniert. Sie möchten die Version in coreutils (über Homebrew installierbar), die als gcsplit bezeichnet wird .
Daniel

10
Nur für diejenigen, die sich fragen, was die Parameter bedeuten: --digits=2Steuert die Anzahl der Ziffern, die zum Nummerieren der Ausgabedateien verwendet werden (2 ist für mich die Standardeinstellung, daher nicht erforderlich). --quietunterdrückt die Ausgabe (auch nicht wirklich notwendig oder hier gefragt). --prefixGibt das Präfix der Ausgabedateien an (Standard ist xx). So können Sie alle Parameter überspringen und erhalten Ausgabedateien wie xx12.
Christopher K.

3
Nur um hinzuzufügen, können Sie die Version für OS X zum Laufen bringen (zumindest mit High Sierra). Sie müssen nur die Argumente ein wenig optimieren csplit -k -f=outfile infile "/-\|/+1" "{3}". Funktionen, die nicht zu funktionieren scheinen, sind "{*}": Ich musste die Anzahl der Trennzeichen -kgenau angeben und sie hinzufügen , um zu vermeiden, dass alle Outfiles gelöscht werden, wenn kein endgültiges Trennzeichen gefunden werden kann. Auch wenn Sie möchten --digits, müssen Sie -nstattdessen verwenden.
Pebbl

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

Erklärung (bearbeitet):

RSist das Datensatztrennzeichen, und diese Lösung verwendet eine gnu awk-Erweiterung, die es erlaubt, mehr als ein Zeichen zu sein. NRist die Datensatznummer.

Die print-Anweisung druckt einen Datensatz gefolgt von " -|"einer Datei, deren Name die Datensatznummer enthält.


1
RSist das Datensatztrennzeichen, und diese Lösung verwendet eine gnu awk-Erweiterung, die es erlaubt, mehr als ein Zeichen zu sein. NR ist die Datensatznummer. Die print-Anweisung druckt einen Datensatz gefolgt von "- |" in eine Datei, deren Name die Datensatznummer enthält.
William Pursell

1
@rzetterbeg Dies sollte gut mit großen Dateien funktionieren. awk verarbeitet die Datei Datensatz für Datensatz, sodass nur so viel gelesen wird, wie benötigt wird. Wenn das erste Auftreten des Datensatztrennzeichens sehr spät in der Datei angezeigt wird, kann es zu einer Speicherkrise kommen, da ein ganzer Datensatz in den Speicher passen muss. Beachten Sie auch, dass die Verwendung von mehr als einem Zeichen in RS kein Standard-Awk ist, dies jedoch in Gnu-Awk funktioniert.
William Pursell

4
Für mich teilte es 3,3 GB in 31.728s
Cleankod

3
@ccf Der Dateiname ist nur die Zeichenfolge auf der rechten Seite von >, sodass Sie ihn beliebig erstellen können. zBprint $0 "-|" > "file" NR ".txt"
William Pursell

1
@AGrush Das ist versionabhängig. Sie können tunawk '{f="file" NR; print $0 " -|" > f}'
William Pursell

7

Debian hat csplit, aber ich weiß nicht, ob das allen / den meisten / anderen Distributionen gemeinsam ist. Wenn nicht, sollte es nicht zu schwierig sein, die Quelle aufzuspüren und zu kompilieren ...


1
Genau. Meine Debian-Box sagt, dass csplit Teil von gnu coreutils ist. Jedes Gnu-Betriebssystem, wie alle Gnu / Linux-Distributionen, wird es also haben. Wikipedia erwähnt auf der csplit-Seite auch "The Single UNIX® Specification, Issue 7". Ich vermute, Sie haben es verstanden.
Strg-Alt-Delor

3
Da csplites sich um POSIX handelt, würde ich erwarten, dass es auf im Wesentlichen allen Unix-ähnlichen Systemen verfügbar ist.
Jonathan Leffler

1
Obwohl csplit POISX ist, besteht das Problem (es scheint einen Test damit auf dem vor mir sitzenden Ubuntu-System durchzuführen) darin, dass es keinen offensichtlichen Weg gibt, eine modernere Regex-Syntax zu verwenden. Vergleichen: csplit --prefix gold-data - "/^==*$/vs csplit --prefix gold-data - "/^=+$/. Zumindest hat GNU grep -e.
new123456

5

Ich habe ein etwas anderes Problem gelöst, bei dem die Datei eine Zeile mit dem Namen enthält, in die der folgende Text gehen soll. Dieser Perl-Code macht den Trick für mich:

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

Können Sie bitte erklären, warum dieser Code funktioniert? Ich habe eine ähnliche Situation wie hier beschrieben - die erforderlichen Namen der Ausgabedateien sind in die Datei eingebettet. Aber ich bin kein normaler Perl-Benutzer, daher kann dieser Code keinen Sinn ergeben.
Shiri

Das echte Rindfleisch ist in der letzten whileSchleife. Wenn der mffreguläre Ausdruck am Zeilenanfang gefunden wird, wird der Rest der Zeile als Dateiname zum Öffnen und Schreiben verwendet. Es schließt nie etwas, so dass nach ein paar Dutzend keine Dateihandles mehr vorhanden sind.
Tripleee

Das Skript würde tatsächlich verbessert, indem der größte Teil des Codes vor der letzten whileSchleife entfernt und while (<>)
Uhr

4

Der folgende Befehl funktioniert für mich. Ich hoffe es hilft.

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

1
Nach einigen Dutzend Dateien gehen die Dateihandles aus. Das Update besteht darin, closedie alte Datei explizit zu verwenden, wenn Sie eine neue starten.
Tripleee

@tripleee wie schließt man es (Anfänger awk Frage). Können Sie ein aktualisiertes Beispiel bereitstellen?
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Dieses Feld ist wahrscheinlich zu klein für ein nützliches Beispiel, aber im Grunde genommen, if (file) close(filename);bevor ein neuer filenameWert zugewiesen wird.
Tripleee

aah hat herausgefunden, wie man es schließt : ; close(filename). Wirklich einfach, aber es behebt wirklich das obige Beispiel
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Ich habe Ihre Bearbeitung zurückgesetzt, weil Sie ein defektes Skript bereitgestellt haben. Wesentliche Änderungen an den Antworten anderer Personen sollten wahrscheinlich vermieden werden. Sie können gerne eine eigene neue Antwort veröffentlichen (möglicherweise als Community-Wiki ), wenn Sie der Meinung sind, dass eine separate Antwort sinnvoll ist.
Tripleee

2

Sie können auch awk verwenden. Ich bin mit awk nicht sehr vertraut, aber das Folgende schien für mich zu funktionieren. Es wurden part1.txt, part2.txt, part3.txt und part4.txt generiert. Beachten Sie, dass die letzte partn.txt-Datei, die dadurch generiert wird, leer ist. Ich bin mir nicht sicher, wie ich das beheben soll, aber ich bin mir sicher, dass dies mit ein wenig Optimierung erreicht werden könnte. Irgendwelche Vorschläge jemand?

awk_pattern Datei:

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

Bash-Befehl:

awk -f awk_pattern input.file


2

Hier ist ein Python 3-Skript, das eine Datei basierend auf einem von den Trennzeichen angegebenen Dateinamen in mehrere Dateien aufteilt. Beispiel für eine Eingabedatei:

# Ignored

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

# Ignored

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

Hier ist das Skript:

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

Zum Schluss führen Sie Folgendes aus:

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

2

Verwenden csplitSie, wenn Sie es haben.

Wenn Sie dies nicht tun, aber Python haben, verwenden Sie Perl nicht.

Faules Lesen der Datei

Ihre Datei ist möglicherweise zu groß, um sie gleichzeitig im Speicher zu speichern. Das zeilenweise Lesen ist möglicherweise vorzuziehen. Angenommen, die Eingabedatei heißt "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"

Dadurch wird die gesamte Datei in den Speicher eingelesen, was bedeutet, dass sie ineffizient ist oder bei großen Dateien sogar fehlschlägt.
Tripleee

1
@tripleee Ich habe die Antwort aktualisiert, um sehr große Dateien zu verarbeiten.
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 )

und die formatierte Version:

#!/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
Wie immer ist das catnutzlos .
Tripleee

1
@Reishin Auf der verlinkten Seite wird ausführlicher erläutert, wie Sie catin jeder Situation eine einzelne Datei vermeiden können. Es gibt eine Stapelüberlauffrage mit mehr Diskussion (obwohl die akzeptierte Antwort IMHO aus ist); stackoverflow.com/questions/11710552/useless-use-of-cat
Tripleee

1
Die Schale ist bei solchen Dingen normalerweise sowieso sehr ineffizient; Wenn Sie nicht verwenden können csplit, ist eine Awk-Lösung dieser Lösung wahrscheinlich viel vorzuziehen (selbst wenn Sie die von shellcheck.net usw. gemeldeten Probleme beheben würden ; beachten Sie, dass derzeit nicht alle darin enthaltenen Fehler gefunden werden).
Tripleee

@tripleee aber wenn die aufgabe darin besteht, es ohne awk, csplit und etc zu machen - nur bash?
Reishin

1
Dann ist das catimmer noch nutzlos, und der Rest des Skripts könnte stark vereinfacht und korrigiert werden. aber es wird immer noch langsam sein. Siehe z. B. stackoverflow.com/questions/13762625/…
Tripleee

0

Dies ist die Art von Problem, für das ich Context-Split geschrieben habe: 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

Äh, das sieht im Wesentlichen wie ein Duplikat des Standarddienstprogramms aus csplit. Siehe die Antwort von @ richard .
Tripleee

Dies ist eigentlich die beste Lösung imo. Ich musste einen 98G-MySQL-Dump und csplit aus irgendeinem Grund aufteilen und verbraucht meinen gesamten RAM. Auch wenn es immer nur eine Zeile geben muss. Macht keinen Sinn. Dieses Python-Skript funktioniert viel besser und frisst nicht den ganzen RAM auf.
Stefan Midjich

0

Hier ist ein Perl-Code, der das Ding macht

#!/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);
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.