在每个空行处分割大型文本文件

28

我有些困难,需要将一个大文本文件分割成多个小文件。我的文本文件的语法如下:

dasdas #42319 blaablaa 50 50
content content
more content
content conclusion

asdasd #92012 blaablaa 30 70
content again
more of it
content conclusion

asdasd #299 yadayada 60 40
content
content
contend done
...and so on

我的文件中典型的信息表格通常有10至40行。

我希望将此文件分割为n个较小的文件,其中n是内容表格的数量。

也就是说

dasdas #42319 blaablaa 50 50
content content
more content
content conclusion

将成为它自己独立的文件,(whateverN.txt)

以及

asdasd #92012 blaablaa 30 70
content again
more of it
content conclusion

再次另存为一个单独的文件whateverN+1.txt 等等。

似乎awkPerl是解决这个问题很好用的工具,但由于以前从未使用过它们,语法有点令人困惑。

我找到了这两个几乎符合我的问题的问题,但未能修改语法以适应我的需求:

将文本文件分割成多个文件如何将文本文件拆分成多个文本文件?(在Unix和Linux上)

应该如何修改命令行输入,使其解决我的问题?


2
我敢打赌你需要先学一下如何使用awk、perl或其他工具,然后再试着用它们来解决你的问题。 - Lee Duhem
还是你会其他编程语言,可以尝试用另一种语言解决问题吗? - mwp
最好您编辑帖子并使用代码块来展示一些例子,就像您链接的那些例子一样,展示您的输入和期望输出。 - Nick P
选择一种语言,先自己尝试。如果仍然有问题,请带着您的尝试来这里。 - serenesat
9个回答

42

RS设置为null告诉awk使用一个或多个空行作为记录分隔符。然后您可以简单地使用NR来设置对应于每个新记录的文件名:

 awk -v RS= '{print > ("whatever-" NR ".txt")}' file.txt
RS: 这是awk的输入记录分隔符。它的默认值是一个只包含单个换行符的字符串,这意味着输入记录由单行文本组成。当其为null字符串时,记录由连续的空行分隔;当其为正则表达式时,记录由输入文本中正则表达式的匹配项分隔。
$ cat file.txt
dasdas #42319 blaablaa 50 50
content content
more content
content conclusion

asdasd #92012 blaablaa 30 70
content again
more of it
content conclusion

asdasd #299 yadayada 60 40
content
content
contend done

$ awk -v RS= '{print > ("whatever-" NR ".txt")}' file.txt

$ ls whatever-*.txt
whatever-1.txt  whatever-2.txt  whatever-3.txt

$ cat whatever-1.txt 
dasdas #42319 blaablaa 50 50
content content
more content
content conclusion

$ cat whatever-2.txt 
asdasd #92012 blaablaa 30 70
content again
more of it
content conclusion

$ cat whatever-3.txt 
asdasd #299 yadayada 60 40
content
content
contend done
$ 

3
我们如何将它保存在变量数组中? - Chand
2
简单的解决方案,不错!如果你想将输出文件名模式作为变量传递,可以选择以下方式:awk -v RS= -v PATTERN="whatever-%d.txt" '{FILE=sprintf(PATTERN, NR); print > FILE}' $filename - Erwin411
对于大文件,输入记录可能无法放入内存(在我的情况下>20 GB)。因此,最好采用基于行的解决方案,请参见@sat的答案。我的最终解决方案是:awk -v PATTERN="whatever-%d.txt" 'BEGIN {n=1; FILE=sprintf(PATTERN, n)} !NF {n++; FILE=sprintf(PATTERN, n); next} {print > FILE}' $filename - Erwin411
2
请注意,您可能以这种方式打开了太多文件句柄。只有GNU awk会自动解决此问题。更好的版本是:awk -v RS= '{f="whatever=" NR ".txt"; print > f; close(f)}' file - kvantour
不错。我正在寻找一些易于设置的东西,因为我的输入文件并不太大,这正好符合我的要求,短小精悍,方便我根据需要进行调整。 - Mr Redstoner
awk是一种用于处理文本的非常强大的编程语言,这是一个很好的例子。如果想了解更多有关awk的内容,我建议阅读Kernighan和Pike所著的《Unix程序设计环境》第4.4节以及Bentley所著的《编程珠玑》的相关内容。 - George Co

10
你可以使用csplit命令:
csplit \
    --quiet \
    --prefix=whatever \
    --suffix-format=%02d.txt \
    --suppress-matched \
    infile.txt /^$/ {*}

POSIX的csplit仅使用短选项,并不支持--suffix--suppress-matched,因此需要使用GNU的csplit

以下是这些选项的作用:

  • --quiet – 禁止输出文件大小
  • --prefix=whatever – 使用名为whatever而不是默认的xx文件名前缀
  • --suffix-format=%02d.txt – 在默认的两位数字后缀后添加.txt
  • --suppress-matched – 不包括与输入分割模式匹配的行
  • /^$/ {*} – 按照“空行”(/^$/)模式尽可能多地分割({*}

这应该是“标准”答案!一个专业的程序...(Unix哲学) - Daniel Bandeira

3

Perl有一个非常实用的特性叫做输入记录分隔符 $/

它是在读取文件时分隔记录的标记。

因此:

#!/usr/bin/env perl
use strict;
use warnings;

local $/ = "\n\n"; 
my $count = 0; 

while ( my $chunk = <> ) {
    open ( my $output, '>', "filename_".$count++ ) or die $!;
    print {$output} $chunk;
    close ( $output ); 
}

就像这样。这里的<>是一个“神奇”的文件句柄,它可以读取管道数据或从命令行指定的文件(打开并读取它们)。这与sedgrep的工作方式类似。
这可以简化为一行代码:
perl -00 -pe 'open ( $out, '>', "filename_".++$n ); select $out;'  yourfilename_here

-00?这是新鲜事。但我尽量避免使用一行代码 :) - Nick P
1
通常我会这样做,但是当我们在进行awk竞赛时,我会尽量在一些更清晰的代码之后再加入它们以供比较。 - Sobrique
谢谢!就是这样!然而,一开始运行此命令时,结果与其他脚本相同。显然原因是我的输入数据文件(每个文件有4-8M行)具有不正确的行分隔符或其他问题。无论我在任何文本编辑器中打开它们,它们看起来都很好。但运行此命令会导致一个文件,与输入文件相同。但是,在我将每个数据集复制粘贴到文本编辑器的空白页面上并保存后,它们的文件大小会稍微改变(例如150MB文件中的1M),之后此命令就可以正常运行了。 - tropical e

2
你可以使用这个 awk 命令,
awk 'BEGIN{file="content"++i".txt"} !NF{file="content"++i".txt";next} {print > file}' yourfile

(或)

awk 'BEGIN{i++} !NF{++i;next} {print > "filename"i".txt"}' yourfile

更易读的格式:

BEGIN {
        file="content"++i".txt"
}
!NF {
        file="content"++i".txt";
        next
}
{
        print > file
}

你可以使用/^$/或更常见的!NF,而不是$0 ~ /^$/。要使用print > file而不是print >> file - shell和awk对于>>>有不同的语义。 - Ed Morton
1
@EdMorton,你是正确的。已更新。感谢你的提示(shellawk对于>>>有不同的语义)。 - sat
使用print > ("filename"i".txt")代替print > "filename"i".txt",因为在POSIX中该语句的含义未定义,一些awk会将其视为(print > "filename") i".txt"或其他不良结果。 - Ed Morton
请将以下与编程相关的内容从英文翻译成中文。只返回翻译后的文本:同时添加一行来关闭文件。 - user1778602

1
如果您遇到以下“打开文件太多”的错误...
awk: whatever-18.txt makes too many open files
 input record number 18, file file.txt
 source line number 1

在创建新文件之前,您可能需要先关闭已创建的文件,方法如下。

awk -v RS= '{close("whatever-" i ".txt"); i++}{print > ("whatever-" i ".txt")}' file.txt

0

既然今天是星期五,我感觉有点想帮忙... :)

试试这个。如果文件像你所说的那样小,最简单的方法就是一次性将其全部读入内存中进行操作。

use strict;
use warnings;

# slurp file
local $/ = undef;
open my $fh, '<', 'test.txt' or die $!;
my $text = <$fh>;
close $fh;

# split on double new line
my @chunks = split(/\n\n/, $text);

# make new files from chunks
my $count = 1;
for my $chunk (@chunks) {
    open my $ofh, '>', "whatever$count.txt" or die $!;
    print $ofh $chunk, "\n";
    close $ofh;
    $count++;
}

Perl文档可以解释任何您不理解的单个命令,但此时您可能还应该查阅教程。


设置 $ / 可能是更好的方法。 - Sobrique
是的,而且“local”也不必要。这只是习惯问题。 - Nick P
这是一个好习惯,而且除此之外没有任何坏处 ;) - Sobrique

0
awk -v RS="\n\n" '{for (i=1;i<=NR;i++); print > i-1}' file.txt

将记录分隔符设置为空行,将每个记录作为单独的文件打印,编号为1、2、3等。最后一个文件(仅)以空行结束。


使用多个字符作为RS可能会让gawk特定,但你应该无论如何都要使用RS=""。同时,在输出重定向时始终将右侧括起来,因为某些awk会将print i-1解释为(print i) -i。最重要的是,这个逻辑是错误的,它会打印每个记录的NR次出现。 - Ed Morton

0

也可以尝试这个 Bash 脚本

#!/bin/bash
i=1
fileName="OutputFile_$i"
while read line ; do 
if [ "$line"  == ""  ] ; then
 ((++i))
 fileName="OutputFile_$i"
else
 echo $line >> "$fileName"
fi
done < InputFile.txt

这将破坏他输入文件的内容,并根据输入文件的内容以及您运行它的任何目录的内容产生不同的输出。请勿编写shell循环来操作文本。请参见http://unix.stackexchange.com/q/169716/133219。 - Ed Morton

0

您还可以尝试使用 split -p "^$"


这是BSD的split(macOS版本)。 - Nuno Silva

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接