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


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
-|

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


31
@ zb226 Я зробив це довго, так що пояснення не потрібні.
ctrl-alt-delor

5
Пропоную додати --elide-empty-files, інакше в кінці буде порожній файл.
луатор

8
Для користувачів OS X зверніть увагу, що версія csplit, яка постачається з ОС, не працює. Вам потрібна версія в coreutils (що встановлюється через Homebrew), яка називається gcsplit .
Даніель

10
Тільки для тих, хто цікавиться, що означають параметри: --digits=2контролює кількість цифр, що використовуються для нумерації вихідних файлів (2 для мене за замовчуванням, тому не потрібно). --quietпригнічує вихід (також насправді не потрібен і не вимагається тут). --prefixвизначає префікс вихідних файлів (за замовчуванням xx). Таким чином, ви можете пропустити всі параметри і отримаєте вихідні файли типу xx12.
Крістофер К.

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

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

Пояснення (відредаговано):

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

Оператор print друкує запис, за яким слідує, " -|"у файл, що містить номер запису в його назві.


1
RSє роздільником записів, і це рішення використовує розширення gnu awk, що дозволяє йому мати більше одного символу. NR - номер запису. Оператор print друкує запис, за яким слідує "- |" у файл, який містить ім'я номера запису.
William Pursell

1
@rzetterbeg Це має добре працювати з великими файлами. awk обробляє файл по одному запису за раз, тому читає лише стільки, скільки потрібно. Якщо перша поява роздільника записів з’являється дуже пізно у файлі, це може бути проблема пам'яті, оскільки один цілий запис повинен поміститися в пам’яті. Також зверніть увагу, що використання декількох символів у RS не є стандартним awk, але це буде працювати в gnu awk.
Вільям Перселл,

4
Для мене це розділилося 3,3 ГБ за 31,728 с
Cleankod

3
@ccf Ім'я файлу - це лише рядок з правого боку >, тому ви можете створити його як завгодно. напр.,print $0 "-|" > "file" NR ".txt"
Вільям Перселл

1
@AGrush Це залежить від версії. Ви можетеawk '{f="file" NR; print $0 " -|" > f}'
Вільям Персел

7

Debian є csplit, але я не знаю, чи це спільно для всіх / більшості / інших дистрибутивів. Якщо ні, то не слід занадто важко відстежувати джерело та компілювати його ...


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, тому не можу зрозуміти цей код.
Ширі

Справжня яловичина знаходиться у фінальній whileпетлі. Якщо він знаходить mffрегулярний вираз на початку рядка, він використовує решту рядка як ім'я файлу для відкриття та початку записування. Він ніколи нічого не закриває, тому через кілька десятків дескрипторів файлів закінчиться.
триплеє

Сценарій насправді можна було б покращити, видаливши більшу частину коду перед фінальним 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 як його закрити (нове запитання для початківців). Чи можете ви навести оновлений приклад?
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Це поле, мабуть, замале для будь-якого корисного прикладу, але в основному if (file) close(filename);перед присвоєнням нового filenameзначення.
триплі

ааа дізнався, як його закрити: ; close(filename). Дійсно просто, але це справді виправляє приклад вище
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Я відкотив ваше редагування, оскільки ви надали непрацюючий сценарій. Варто уникати значних редагувань відповідей інших людей - сміливо публікуйте нову власну відповідь (можливо, у вікі-спільноті ), якщо вважаєте, що заслуговує окрема відповідь.
триплеє

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


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/

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"

Це зчитує весь файл у пам’ять, а це означає, що він буде неефективним або навіть не працюватиме для великих файлів.
триплі

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;
)

4
Як завжди, марно . cat
триплі

1
@Reishin Посилання на сторінку набагато детальніше пояснює, як catу будь-якій ситуації можна уникнути одного файлу. Існує запитання щодо переповнення стека з більшим обговоренням (хоча прийнята відповідь IMHO вимкнена); stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

1
Оболонка, як правило, дуже неефективна в подібних ситуаціях; якщо ви не можете використовувати csplit, рішення Awk, мабуть, набагато краще за це рішення (навіть якщо вам потрібно було виправити проблеми, про які повідомляє shellcheck.net тощо; зауважте, що наразі в ньому не знайдено всіх помилок).
триплеє

@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

Е-е, це схоже на дублікат стандартної csplitутиліти. Дивіться відповідь @ richard .
триплі

Це насправді найкраще рішення imo. Мені довелося розбити дамп 98G MySQL і csplit чомусь з'їдає всю мою оперативну пам'ять і вбивається. Незважаючи на те, що в той час йому повинно бути лише один рядок. Не має сенсу. Цей скрипт на python працює набагато краще і не з’їдає всього барана.
Стефан Міджич

0

Ось Perl-код, який все зробить

#!/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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.