Разделить один файл на несколько файлов по разделителю

86

У меня есть один файл с -|разделителем после каждого раздела ... нужно создавать отдельные файлы для каждого раздела, используя unix.

пример входного файла

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

Ожидаемый результат в файле 1

wertretr
ewretrtret
1212132323
000232
-|

Ожидаемый результат в файле 2

ereteertetet
232434234
erewesdfsfsfs
0234342343
-|

Ожидаемый результат в файле 3

jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|
user1499178
источник
1
Вы пишете программу или хотите сделать это с помощью утилит командной строки?
rkyser
1
предпочтительнее использовать утилиты командной строки ..
user1499178
Вы можете использовать awk, для этого было бы легко написать программу из 3 или 4 строк. К сожалению, у меня нет практики.
ctrl-alt-delor

Ответы:

97

Один лайнер, без программирования. (кроме регулярного выражения и т. д.)

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

протестировано на: csplit (GNU coreutils) 8.30

Примечания по использованию на Apple Mac

«Для пользователей OS X обратите внимание, что версия csplit, поставляемая с ОС, не работает. Вам понадобится версия в coreutils (устанавливается через Homebrew), которая называется gcsplit». - @Danial

«Чтобы добавить, вы можете получить версию для OS X для работы (по крайней мере, с High Sierra). Вам просто нужно немного настроить аргументы csplit -k -f=outfile infile "/-\|/+1" "{3}". Функции, которые, похоже, не работают, - это то "{*}", о чем я должен был указать количество разделителей, и необходимо добавить, -kчтобы избежать удаления всех исходящих файлов, если он не может найти последний разделитель. Также, если вы хотите --digits, вам нужно использовать -nвместо этого. " - @Pebbl

ctrl-alt-delor
источник
31
@ zb226 Я делал это долго, так что объяснений не потребовалось.
ctrl-alt-delor 07
5
Предлагаю добавить --elide-empty-files, иначе в конце будет пустой файл.
luator
8
Для пользователей OS X обратите внимание, что версия csplit, поставляемая с ОС, не работает. Вам понадобится версия в coreutils (устанавливаемая через Homebrew), которая называется gcsplit .
Daniel
10
Только для тех, кому интересно, что означают параметры: --digits=2контролирует количество цифр, используемых для нумерации выходных файлов (2 по умолчанию для меня, поэтому не обязательно). --quietподавляет вывод (тоже не обязательно или запрашивается здесь). --prefixуказывает префикс выходных файлов (по умолчанию xx). Таким образом, вы можете пропустить все параметры и получить выходные файлы вроде xx12.
Christopher K.
3
Чтобы добавить, вы можете получить версию для работы OS X (по крайней мере, с High Sierra). Вам просто нужно немного настроить аргументы csplit -k -f=outfile infile "/-\|/+1" "{3}". Функции, которые, похоже, не работают, - это то "{*}", что я должен был -kуказать количество разделителей и добавить, чтобы избежать удаления всех исходящих файлов, если он не может найти последний разделитель. Также, если вы хотите --digits, вам нужно использовать -nвместо этого.
Pebbl 08
38
awk '{f="file" NR; print $0 " -|"> f}' RS='-\\|'  input-file

Пояснение (отредактировано):

RSявляется разделителем записей, и в этом решении используется расширение gnu awk, которое позволяет использовать более одного символа. NRэто номер записи.

Оператор печати печатает запись, за которой следует, " -|"в файл, который содержит номер записи в своем имени.

Уильям Перселл
источник
1
RSявляется разделителем записей, и в этом решении используется расширение gnu awk, которое позволяет использовать более одного символа. NR - номер рекорда. Оператор печати печатает запись, за которой следует "- |" в файл, в имени которого содержится номер записи.
Уильям Перселл
1
@rzetterbeg Это должно работать с большими файлами. awk обрабатывает файл по одной записи за раз, поэтому читает ровно столько, сколько нужно. Если первое вхождение разделителя записей появляется в файле очень поздно, это может означать нехватку памяти, так как одна запись целиком должна уместиться в памяти. Также обратите внимание, что использование более одного символа в RS не является стандартным awk, но это будет работать в gnu awk.
Уильям Перселл
4
Для меня он разделил 3,3 ГБ на 31,728 сек
Cleankod
3
@ccf Имя файла - это просто строка с правой стороны >, так что вы можете построить ее как хотите. например,print $0 "-|" > "file" NR ".txt"
Уильям Перселл 07
1
@AGrush Это зависит от версии. Вы можете сделатьawk '{f="file" NR; print $0 " -|" > f}'
Уильям Перселл
7

У Debian есть csplit, но я не знаю, является ли это общим для всех / большинства / других дистрибутивов. Если нет, то не должно быть слишком сложно отследить источник и скомпилировать его ...

Twalberg
источник
1
Согласен. В моем окне Debian написано, что csplit является частью gnu coreutils. Таким образом, любая операционная система Gnu, например, все дистрибутивы Gnu / Linux, будет иметь ее. В Википедии также упоминается «Единая спецификация UNIX®, выпуск 7» на странице csplit, так что я подозреваю, что вы ее поняли.
ctrl-alt-delor
3
Поскольку он csplitнаходится в POSIX, я ожидаю, что он будет доступен практически во всех Unix-подобных системах.
Джонатан Леффлер
1
Хотя csplit - это POISX, проблема (кажется, выполняется тест с ним в системе Ubuntu, сидящей передо мной) заключается в том, что нет очевидного способа заставить его использовать более современный синтаксис регулярных выражений. Для сравнения: csplit --prefix gold-data - "/^==*$/против csplit --prefix gold-data - "/^=+$/. По крайней мере, у GNU grep есть -e.
new123456
5

Я решил немного другую проблему, когда файл содержит строку с именем, в которую должен идти следующий текст. Этот код на Perl помогает мне:

#!/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 "$_"};
  }
Джон Дэвид Смит
источник
Не могли бы вы объяснить, почему этот код работает? У меня ситуация, аналогичная описанной здесь - требуемые имена выходных файлов встроены в файл. Но я не обычный пользователь Perl, поэтому не могу понять смысл этого кода.
shiri
Настоящая говядина находится в последней whileпетле. Если он находит mffрегулярное выражение в начале строки, он использует оставшуюся часть строки как имя файла, чтобы открыть и начать запись. Он никогда ничего не закрывает, поэтому дескрипторы файлов закончатся через несколько десятков.
tripleee
На самом деле сценарий можно было бы улучшить, удалив большую часть кода перед последним whileциклом и переключившись наwhile (<>)
tripleee
4

У меня работает следующая команда. Надеюсь, это поможет.

awk 'BEGIN{file = 0; filename = "output_" file ".txt"}
    /-|/ {getline; file ++; filename = "output_" file ".txt"}
    {print $0 > filename}' input
Тхань
источник
1
Обычно дескрипторы файлов заканчиваются после нескольких десятков файлов. Исправление состоит в том, чтобы явно указать closeстарый файл при запуске нового.
Tripleee
@tripleee, как его закрыть (вопрос для начинающих по awk). Вы можете привести обновленный пример?
Jesper Rønn-Jensen
1
@ JesperRønn-Jensen Это поле, вероятно, слишком мало для любого полезного примера, но в основном if (file) close(filename);перед присвоением нового filenameзначения.
tripleee
ааа узнал, как его закрыть: ; close(filename). Действительно просто, но это действительно исправляет приведенный выше пример
Джеспер Рённ-Йенсен
1
@ JesperRønn-Jensen Я откатил ваше редактирование, потому что вы предоставили неработающий скрипт. Существенных изменений в ответах других людей, вероятно, следует избегать - не стесняйтесь публиковать собственный новый ответ (возможно, в качестве вики-страницы сообщества ), если вы считаете, что отдельный ответ заслуживает.
tripleee
2

Вы также можете использовать awk. Я не очень знаком с awk, но мне показалось, что следующее работает. Он создал part1.txt, part2.txt, part3.txt и part4.txt. Обратите внимание, что последний созданный файл partn.txt пуст. Я не уверен, как это исправить, но уверен, что это можно сделать с помощью небольшой настройки. Есть какие-нибудь предложения?

awk_pattern файл:

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

команда bash:

awk -f awk_pattern input.file

rkyser
источник
2

Вот сценарий Python 3, который разбивает файл на несколько файлов на основе имени файла, указанного в разделителях. Пример входного файла:

# Ignored

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

# Ignored

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

Вот сценарий:

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

Наконец, вот как вы его запускаете:

$ python3 script.py -i input-file.txt -o ./output-folder/
ctrlc-корень
источник
2

Используйте, csplitесли он у вас есть.

Если нет, но у вас есть Python ... не используйте Perl.

Ленивое чтение файла

Ваш файл может быть слишком большим для одновременного хранения в памяти - предпочтительнее построчное чтение. Предположим, что входной файл назван «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"
Аарон Холл
источник
Это приведет к чтению всего файла в память, что означает, что это будет неэффективно или даже не удастся для больших файлов.
Tripleee
1
@tripleee Я обновил ответ, чтобы обрабатывать очень большие файлы.
Аарон Холл
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 )

и отформатированная версия:

#!/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
источник
4
Как всегда, бесполезно . cat
тройной
1
@Reishin На связанной странице более подробно объясняется, как можно избежать catиспользования одного файла в любой ситуации. Существует вопрос о переполнении стека с дополнительным обсуждением (хотя принятый ответ - IMHO off); stackoverflow.com/questions/11710552/useless-use-of-cat
Tripleee
1
В любом случае оболочка обычно очень неэффективна в подобных делах; если вы не можете его использовать csplit, решение Awk, вероятно, будет намного предпочтительнее этого решения (даже если вы должны были исправить проблемы, о которых сообщает shellcheck.net и т. д.; обратите внимание, что в настоящее время оно не находит в нем всех ошибок).
Tripleee
@tripleee, а если стоит задача сделать это без awk, csplit и т. д. - только bash?
Рейшин
1
Тогда он по- catпрежнему бесполезен, а остальную часть скрипта можно значительно упростить и исправить; но все равно будет медленно. См., Например, stackoverflow.com/questions/13762625/…
tripleee
0

Это проблема, для которой я написал разделение контекста: 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
источник
Это похоже на дубликат стандартной csplitутилиты. См . Ответ @ richard .
tripleee
This is actually the best solution imo. I've had to split a 98G mysql dump and csplit for some reason eats up all my RAM, and is killed. Even though it should only need to match one line at the time. Makes no sense. This python script works much better and doesn't eat up all the ram.
Stefan Midjich
0

Here is a perl code that will do the thing

#!/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
источник