Podziel jeden plik na wiele plików na podstawie separatora

86

Mam jeden plik z -|separatorem po każdej sekcji ... muszę utworzyć osobne pliki dla każdej sekcji używając unixa.

przykład pliku wejściowego

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

Oczekiwany wynik w pliku 1

wertretr
ewretrtret
1212132323
000232
-|

Oczekiwany wynik w pliku 2

ereteertetet
232434234
erewesdfsfsfs
0234342343
-|

Oczekiwany wynik w pliku 3

jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|
user1499178
źródło
1
Piszesz program, czy chcesz to zrobić za pomocą narzędzi wiersza poleceń?
rkyser
1
preferowane będzie używanie narzędzi wiersza poleceń ..
user1499178
Mógłbyś użyć awk, byłoby łatwo napisać do tego 3 lub 4-liniowy program. Niestety nie mam praktyki.
ctrl-alt-delor

Odpowiedzi:

97

Jedna linijka, bez programowania. (oprócz wyrażenia regularnego itp.)

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

testowano na: csplit (GNU coreutils) 8.30

Uwagi dotyczące używania na Apple Mac

„W przypadku użytkowników OS X, pamiętaj, że wersja csplittego systemu operacyjnego nie działa. Będziesz potrzebować wersji w coreutils (do zainstalowania przez Homebrew), która nazywa się gcsplit”. - @Danial

„Wystarczy dodać, że możesz sprawić, aby wersja dla OS X działała (przynajmniej z High Sierra). Wystarczy trochę poprawić argumenty csplit -k -f=outfile infile "/-\|/+1" "{3}". Funkcje, które wydają się nie działać, to takie "{*}", musiałem być konkretny liczbę separatorów, które trzeba dodać, -kaby uniknąć usunięcia wszystkich plików wyjściowych, jeśli nie może znaleźć końcowego separatora. Jeśli chcesz --digits, musisz użyć -nzamiast tego. " - @Pebbl

ctrl-alt-delor
źródło
31
@ zb226 Zrobiłem to długo, więc żadne wyjaśnienia nie były potrzebne.
ctrl-alt-delor,
5
Proponuję dodać --elide-empty-files, w przeciwnym razie na końcu będzie pusty plik.
luator
8
Użytkownicy systemu OS X powinni pamiętać, że wersja csplit dostarczona z systemem operacyjnym nie działa. Będziesz potrzebować wersji w coreutils (do zainstalowania przez Homebrew), która nazywa się gcsplit .
Daniel,
10
Tylko dla tych, którzy zastanawiają się, co oznaczają parametry: --digits=2kontroluje liczbę cyfr użytych do numerowania plików wyjściowych (dla mnie 2 jest domyślna, więc nie jest to konieczne). --quietblokuje wyjście (również nie jest naprawdę konieczne ani nie jest tutaj wymagane). --prefixokreśla przedrostek plików wyjściowych (domyślnie xx). Możesz więc pominąć wszystkie parametry i otrzymać pliki wyjściowe, takie jak xx12.
Christopher K.
3
Wystarczy dodać, że możesz pobrać wersję dla OS X do pracy (przynajmniej z High Sierra). Musisz tylko trochę poprawić argumenty csplit -k -f=outfile infile "/-\|/+1" "{3}". Funkcje, które wydają się nie działać "{*}", to: musiałem określić liczbę separatorów i musiałem je dodać, -kaby uniknąć usunięcia wszystkich plików wyjściowych, jeśli nie może znaleźć końcowego separatora. Jeśli chcesz --digits, musisz -nzamiast tego użyć .
Pebbl
38
awk '{f="file" NR; print $0 " -|"> f}' RS='-\\|'  input-file

Wyjaśnienie (zredagowano):

RSjest separatorem rekordów, a to rozwiązanie wykorzystuje rozszerzenie gnu awk, które pozwala na użycie więcej niż jednego znaku. NRto numer rekordu.

Instrukcja print drukuje rekord, po którym następuje, " -|"do pliku, który zawiera numer rekordu w swojej nazwie.

William Pursell
źródło
1
RSjest separatorem rekordów, a to rozwiązanie wykorzystuje rozszerzenie gnu awk, które pozwala na użycie więcej niż jednego znaku. NR to numer rekordu. Instrukcja print drukuje rekord, po którym następuje „- |” do pliku, który zawiera numer rekordu w swojej nazwie.
William Pursell
1
@rzetterbeg Powinno to działać dobrze w przypadku dużych plików. awk przetwarza plik po jednym rekordzie na raz, więc czyta tylko tyle, ile potrzebuje. Jeśli pierwsze wystąpienie separatora rekordów pojawia się bardzo późno w pliku, może to oznaczać usterkę pamięci, ponieważ jeden cały rekord musi zmieścić się w pamięci. Należy również zauważyć, że używanie więcej niż jednego znaku w RS nie jest standardowym awk, ale będzie działać w gnu awk.
William Pursell
4
Dla mnie podzielił 3,3 GB na 31,728s
Cleankod
3
@ccf Nazwa pliku to po prostu ciąg znaków po prawej stronie >, więc możesz go skonstruować w dowolny sposób . np.print $0 "-|" > "file" NR ".txt"
William Pursell
1
@AGrush To zależy od wersji. Możesz to zrobićawk '{f="file" NR; print $0 " -|" > f}'
William Pursell
7

Debian tak csplit, ale nie wiem, czy jest to wspólne dla wszystkich / większości / innych dystrybucji. Jeśli nie, to nie powinno być zbyt trudno wyśledzić źródło i skompilować je ...

twalberg
źródło
1
Zgadzam się. Moje pudełko Debiana mówi, że csplit jest częścią coreutils gnu. Tak więc każdy system operacyjny Gnu, taki jak wszystkie dystrybucje Gnu / Linux, będzie go miał. Wikipedia wspomina również o „The Single UNIX® Specification, Issue 7” na stronie csplit, więc podejrzewam, że ją masz.
ctrl-alt-delor,
3
Ponieważ csplitjest w POSIX, spodziewałbym się, że będzie dostępny w zasadzie we wszystkich systemach uniksopodobnych.
Jonathan Leffler
1
Chociaż csplit to POISX, problem (wydaje się, że testuje go w systemie Ubuntu siedzącym przede mną) polega na tym, że nie ma oczywistego sposobu, aby użyć bardziej nowoczesnej składni wyrażeń regularnych. Porównaj: csplit --prefix gold-data - "/^==*$/vs csplit --prefix gold-data - "/^=+$/. Przynajmniej GNU grep ma -e.
nowy123456
5

Rozwiązałem nieco inny problem, w którym plik zawiera wiersz z nazwą miejsca, w którym powinien znajdować się następujący tekst. Ten kod w Perlu załatwia sprawę:

#!/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
źródło
Czy możesz wyjaśnić, dlaczego ten kod działa? Mam podobną sytuację do opisanej tutaj - wymagane nazwy plików wyjściowych są osadzone w pliku. Ale nie jestem zwykłym użytkownikiem Perla, więc nie do końca rozumiem ten kod.
shiri
Prawdziwa wołowina jest w ostatniej whilepętli. Jeśli znajdzie mffwyrażenie regularne na początku wiersza, użyje reszty wiersza jako nazwy pliku do otwarcia i rozpoczęcia zapisu. Nigdy niczego nie zamyka, więc po kilkudziesięciu zabraknie uchwytów plików.
tripleee
Skrypt zostałby faktycznie ulepszony poprzez usunięcie większości kodu przed końcową whilepętlą i przejście dowhile (<>)
tripleee
4

Następujące polecenie działa dla mnie. Mam nadzieję, że to pomoże.

awk 'BEGIN{file = 0; filename = "output_" file ".txt"}
    /-|/ {getline; file ++; filename = "output_" file ".txt"}
    {print $0 > filename}' input
Niż
źródło
1
Skończy się to z uchwytami plików po zazwyczaj kilkudziesięciu plikach. Rozwiązaniem jest jawne otwarcie closestarego pliku podczas uruchamiania nowego.
tripleee
@tripleee, jak to zamknąć (pytanie dla początkujących awk). Czy możesz podać zaktualizowany przykład?
Jesper Rønn-Jensen
1
@ JesperRønn-Jensen To pole jest prawdopodobnie za małe na jakikolwiek użyteczny przykład, ale przede wszystkim if (file) close(filename);przed przypisaniem nowej filenamewartości.
tripleee
aaa okazało się, jak go zamknąć: ; close(filename). Naprawdę proste, ale naprawdę rozwiązuje powyższy przykład
Jesper Rønn-Jensen
1
@ JesperRønn-Jensen Wycofałem twoją edycję, ponieważ dostarczyłeś zepsuty skrypt. Prawdopodobnie należy unikać znaczących zmian w odpowiedziach innych osób - nie krępuj się opublikować własnej odpowiedzi (na przykład jako wiki społeczności ), jeśli uważasz, że zasługuje na osobną odpowiedź.
tripleee
2

Możesz także użyć awk. Nie jestem zaznajomiony z awk, ale wydaje mi się, że działają w moim przypadku poniższe. Wygenerował part1.txt, part2.txt, part3.txt i part4.txt. Zwróć uwagę, że ostatni generowany przez to plik partn.txt jest pusty. Nie jestem pewien, jak to naprawić, ale jestem pewien, że można to zrobić przy niewielkich poprawkach. Jakieś sugestie ktoś?

plik awk_pattern:

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

bash polecenie:

awk -f awk_pattern input.file

rkyser
źródło
2

Oto skrypt Python 3, który dzieli plik na wiele plików na podstawie nazwy pliku podanej przez ograniczniki. Przykładowy plik wejściowy:

# Ignored

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

# Ignored

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

Oto skrypt:

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

Wreszcie, jak to uruchomić:

$ python3 script.py -i input-file.txt -o ./output-folder/
ctrlc-root
źródło
2

Użyj, csplitjeśli go masz.

Jeśli nie, ale masz Pythona ... nie używaj Perla.

Leniwe czytanie pliku

Twój plik może być zbyt duży, aby przechowywać go w pamięci od razu - preferowane może być czytanie wiersz po wierszu. Załóżmy, że plik wejściowy nosi nazwę „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
źródło
Spowoduje to wczytanie całego pliku do pamięci, co oznacza, że ​​będzie nieefektywny lub nawet zawiedzie w przypadku dużych plików.
tripleee
1
@tripleee Zaktualizowałem odpowiedź, aby obsługiwać bardzo duże pliki.
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 )

i wersja sformatowana:

#!/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
źródło
4
Jak zawsze, jest bezużyteczny . cat
tripleee
1
@Reishin Strona, do której prowadzi łącze, wyjaśnia znacznie bardziej szczegółowo, w jaki sposób można uniknąć catpojedynczego pliku w każdej sytuacji. Jest pytanie przepełnienia stosu z większą dyskusją (chociaż zaakceptowana odpowiedź to IMHO wyłączone); stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee
1
Powłoka i tak jest zazwyczaj bardzo nieefektywna w tego typu sytuacjach; jeśli nie możesz użyć csplit, rozwiązanie Awk jest prawdopodobnie o wiele lepsze niż to rozwiązanie (nawet jeśli miałbyś naprawić problemy zgłoszone przez shellcheck.net itp; zauważ, że obecnie nie znajduje ono wszystkich błędów).
tripleee
@tripleee, ale jeśli zadaniem jest zrobienie tego bez awk, csplit itp. - tylko bash?
Reishin
1
Wtedy catjest on nadal bezużyteczny, a resztę skryptu można by znacznie uprościć i poprawić; ale nadal będzie powolny. Patrz np stackoverflow.com/questions/13762625/...
tripleee
0

To jest rodzaj problemu, dla którego napisałem podział kontekstu: 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
źródło
Uh, to zasadniczo wygląda na duplikat standardowego csplitnarzędzia. Zobacz odpowiedź @ richard .
tripleee
To właściwie najlepsze rozwiązanie imo. Z jakiegoś powodu musiałem podzielić zrzut mysql 98G i csplit zjadający całą pamięć RAM i zostaje zabity. Nawet jeśli w danym momencie powinno pasować tylko jedna linia. Nie ma sensu. Ten skrypt w Pythonie działa znacznie lepiej i nie zjada całego pamięci RAM.
Stefan Midjich,
0

Oto kod Perla, który zrobi to

#!/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
źródło