แยกไฟล์หนึ่งไฟล์ออกเป็นหลายไฟล์ตามตัวคั่น


88

ฉันมีไฟล์เดียวที่มี-|ตัวคั่นหลังแต่ละส่วน ... จำเป็นต้องสร้างไฟล์แยกกันสำหรับแต่ละส่วนโดยใช้ยูนิกซ์

ตัวอย่างของไฟล์อินพุต

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

คำตอบ:


98

ซับเดียวไม่มีการเขียนโปรแกรม (ยกเว้น regexp เป็นต้น)

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มิฉะนั้นจะมีไฟล์ว่างในตอนท้าย
luator

8
สำหรับผู้ใช้ OS X โปรดทราบว่าเวอร์ชันของ csplit ที่มาพร้อมกับระบบปฏิบัติการไม่ทำงาน คุณจะต้องการรุ่นใน coreutils (ติดตั้งผ่าน Homebrew) ซึ่งเรียกว่าgcsplit
Daniel

10
สำหรับผู้ที่สงสัยว่าพารามิเตอร์หมายถึงอะไร: --digits=2ควบคุมจำนวนหลักที่ใช้ในการกำหนดหมายเลขไฟล์เอาต์พุต (2 เป็นค่าเริ่มต้นสำหรับฉันจึงไม่จำเป็น) --quietระงับเอาต์พุต (ยังไม่จำเป็นจริงๆหรือขอที่นี่) --prefixระบุคำนำหน้าของไฟล์เอาต์พุต (ค่าเริ่มต้นคือ xx) ดังนั้นคุณสามารถข้ามพารามิเตอร์ทั้งหมดและจะได้รับไฟล์เอาต์พุตเช่นxx12.
Christopher K.

3
เพียงเพิ่มคุณสามารถรับเวอร์ชันสำหรับ OS X เพื่อใช้งานได้ (อย่างน้อยก็มี High Sierra) คุณเพียงแค่ต้องปรับแต่ง args csplit -k -f=outfile infile "/-\|/+1" "{3}"บิต คุณสมบัติที่ดูเหมือนจะใช้งานไม่ได้คือ"{*}"ฉันต้องระบุจำนวนตัวคั่นและจำเป็นต้องเพิ่ม-kเพื่อหลีกเลี่ยงการลบไฟล์ที่ไม่ได้ทั้งหมดหากไม่พบตัวคั่นสุดท้าย นอกจากนี้หากคุณต้องการ--digitsคุณต้องใช้-nแทน
Pebbl

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

คำอธิบาย (แก้ไข):

RSเป็นตัวคั่นเร็กคอร์ดและโซลูชันนี้ใช้ส่วนขยาย gnu awk ซึ่งอนุญาตให้มีมากกว่าหนึ่งอักขระ NRคือหมายเลขบันทึก

คำสั่งพิมพ์จะพิมพ์บันทึกตามด้วย" -|"ลงในไฟล์ที่มีหมายเลขบันทึกในชื่อ


1
RSเป็นตัวคั่นเร็กคอร์ดและโซลูชันนี้ใช้ส่วนขยาย gnu awk ซึ่งอนุญาตให้มีมากกว่าหนึ่งอักขระ NR คือหมายเลขบันทึก คำสั่งพิมพ์จะพิมพ์บันทึกตามด้วย "- |" ลงในไฟล์ที่มีหมายเลขบันทึกในชื่อ
William Pursell

1
@rzetterbeg สิ่งนี้ควรใช้ได้ดีกับไฟล์ขนาดใหญ่ awk ประมวลผลไฟล์ทีละเร็กคอร์ดดังนั้นจึงอ่านได้มากเท่าที่จำเป็นเท่านั้น หากตัวคั่นเร็กคอร์ดปรากฏขึ้นครั้งแรกในไฟล์ช้ามากอาจเป็นปัญหาหน่วยความจำเนื่องจากระเบียนทั้งหมดต้องพอดีกับหน่วยความจำ นอกจากนี้โปรดทราบว่าการใช้อักขระมากกว่าหนึ่งตัวใน RS ไม่ใช่ awk มาตรฐาน แต่จะใช้ได้ใน gnu awk
William Pursell

4
สำหรับฉันมันแบ่ง 3.3 GB ใน 31.728 วินาที
Cleankod

3
@ccf ชื่อไฟล์เป็นเพียงสตริงทางด้านขวาของไฟล์>ดังนั้นคุณสามารถสร้างได้ตามต้องการ เช่นprint $0 "-|" > "file" NR ".txt"
William Pursell

1
@AGrush นั่นขึ้นอยู่กับรุ่น คุณสามารถทำได้awk '{f="file" NR; print $0 " -|" > f}'
William Pursell

7

Debian มีcsplitแต่ฉันไม่รู้ว่าเป็นเรื่องปกติสำหรับการแจกแจงทั้งหมด / ส่วนใหญ่ / อื่น ๆ หรือไม่ ถ้าไม่เป็นเช่นนั้นก็ไม่ควรจะยากเกินไปที่จะติดตามแหล่งที่มาและรวบรวม ...


1
ฉันเห็นด้วย. กล่อง Debian ของฉันบอกว่า csplit เป็นส่วนหนึ่งของ gnu coreutils ดังนั้นระบบปฏิบัติการ Gnu ใด ๆ เช่น Gnu / Linux distros ทั้งหมดจะมี Wikipedia ยังกล่าวถึง 'The Single UNIX® Specification, Issue 7' ในหน้า csplit ด้วยดังนั้นฉันจึงสงสัยว่าคุณเข้าใจแล้ว
ctrl-alt-delor

3
เนื่องจากcsplitอยู่ใน POSIX ฉันคาดว่าจะพร้อมใช้งานในระบบที่คล้าย Unix เป็นหลัก
Jonathan Leffler

1
แม้ว่า csplit จะเป็น POISX แต่ปัญหา (ดูเหมือนว่ากำลังทำการทดสอบกับระบบ Ubuntu ที่นั่งอยู่ข้างหน้าฉัน) คือไม่มีวิธีที่ชัดเจนในการทำให้มันใช้ไวยากรณ์ regex ที่ทันสมัยกว่า เปรียบเทียบ: VScsplit --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ห่วงสุดท้าย หากพบmffregex ที่จุดเริ่มต้นของบรรทัดจะใช้ส่วนที่เหลือของบรรทัดเป็นชื่อไฟล์เพื่อเปิดและเริ่มเขียน มันไม่เคยปิดอะไรเลยดังนั้นมันจะหมดที่จัดการไฟล์หลังจากไม่กี่โหล
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

aah พบวิธีการปิด: ; close(filename). ง่ายมาก แต่แก้ไขตัวอย่างข้างต้นได้อย่างแท้จริง
Jesper Rønn-Jensen

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


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"

สิ่งนี้จะอ่านไฟล์ทั้งหมดลงในหน่วยความจำซึ่งหมายความว่าจะไม่มีประสิทธิภาพหรือแม้กระทั่งล้มเหลวสำหรับไฟล์ขนาดใหญ่
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;
)

4
เช่นเคยจะไม่ได้ผล cat
tripleee

1
@Reishin เพจที่เชื่อมโยงจะอธิบายรายละเอียดเพิ่มเติมว่าคุณจะหลีกเลี่ยงcatไฟล์เดียวได้อย่างไรในทุกสถานการณ์ มีคำถาม Stack Overflow พร้อมการสนทนาเพิ่มเติม (แม้ว่าคำตอบที่ยอมรับจะปิด IMHO) stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

1
โดยทั่วไปแล้วเชลล์จะไม่มีประสิทธิภาพมากในสิ่งประเภทนี้อยู่ดี หากคุณไม่สามารถใช้csplitโซลูชัน Awk อาจเป็นที่นิยมสำหรับโซลูชันนี้มาก (แม้ว่าคุณจะแก้ไขปัญหาที่รายงานโดยshellcheck.netฯลฯ โปรดทราบว่าขณะนี้ยังไม่พบข้อบกพร่องทั้งหมดในนี้)
tripleee

@tripleee แต่ถ้างานคือการทำโดยไม่ต้อง awk, csplit และอื่น ๆ - ทุบตีเท่านั้น?
Reishin

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ยูทิลิตี้มาตรฐาน ดู@ คำตอบที่ริชาร์ด
tripleee

นี่เป็นทางออกที่ดีที่สุดของ imo ฉันต้องแยกการถ่ายโอนข้อมูล mysql 98G และ csplit ด้วยเหตุผลบางอย่างกิน RAM ทั้งหมดของฉันและถูกฆ่า แม้ว่าจะต้องจับคู่บรรทัดเดียวในขณะนั้นก็ตาม ไม่มีเหตุผล สคริปต์ python นี้ทำงานได้ดีขึ้นมากและไม่กิน ram ทั้งหมด
Stefan Midjich

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);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.