【问题标题】:Splitting large text file on every blank line在每个空白行上拆分大文本文件
【发布时间】:2016-01-22 13:20:36
【问题描述】:

我在将一个大的文本文件拆分成多个较小的文件时遇到了一些麻烦。我的文本文件的语法如下:

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 是很好的工具,但在语法有点令人费解之前从未使用过它们。

我发现这两个问题几乎与我的问题相对应,但未能修改语法以满足我的需要:

Split text file into multiple files & How can I split a text file into multiple text files?(在 Unix 和 Linux 上)

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

【问题讨论】:

  • 我敢打赌,在尝试使用它们解决问题之前,您需要先学习一下如何使用它们(awk、perl 或其他)。
  • 或者是否有您知道的可以尝试解决的语言?
  • 最好使用链接代码块中的代码块进行编辑以发布一些示例,包括输入和所需输出。
  • 选择一种语言,先从自己尝试。如果你有问题,那就来这里试试吧。

标签: bash perl awk


【解决方案1】:

因为是星期五,我觉得有点帮助...... :)

试试这个。如果文件像您暗示的那样小,最简单的方法是一次读取所有文件并在内存中工作。

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 文档可以解释您不理解的任何单个命令,但此时您可能也应该查看教程。

【讨论】:

  • 设置$/ 可能是更好的方法。
  • 是的,local 也不是必需的。习惯的力量。
  • 不过,这是一个好习惯,除此之外也无害 ;)
【解决方案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 &gt; file,而不是 print &gt;&gt; file - shell 和 awk 对于 &gt;&gt;&gt; 有不同的语义。
  • @EdMorton,你是对的。更新。感谢您的提示(shellawk 对于 &gt;&gt;&gt; 的语义不同)。
  • 使用 print &gt; ("filename"i".txt") 而不是 print &gt; "filename"i".txt",因为该语句的含义在 POSIX 中未定义,一些 awks 会将其视为 (print &gt; "filename") i".txt" 或其他不受欢迎的东西。
  • 另外加一行来关闭文件
【解决方案3】:
awk -v RS="\n\n" '{for (i=1;i<=NR;i++); print > i-1}' file.txt

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

【讨论】:

  • 对 RS 使用多个字符会使这个 gawk 变得特定,但无论如何你都应该使用RS=""。也总是在输出重定向的右侧加上括号,因为一些 awks 会将print i-1 解释为(print i) -i。最重要的是 - 逻辑是错误的,它会打印每条记录的 NR 次。
【解决方案4】:

也试试这个 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 循环。见unix.stackexchange.com/q/169716/133219
【解决方案5】:

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

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

RS: 这是 awk 的输入记录分隔符。它的默认值是一个包含单个换行符的字符串,这意味着输入记录由单行文本组成。 它也可以是空字符串,在这种情况下,记录由空行分隔,或正则表达式,在这种情况下,记录由输入文本中正则表达式的匹配项分隔。

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

【讨论】:

  • 我们如何将它保存在变量数组中?
  • 简单的解决方案,不错!如果您想将输出文件名模式作为变量传递,您可以选择以下内容:awk -v RS= -v PATTERN="whatever-%d.txt" '{FILE=sprintf(PATTERN, NR); print &gt; FILE}' $filename
  • 对于大文件,输入记录可能不适合内存(在我的情况下>20 GB)。因此,首选面向行的解决方案,请参阅@sat 的答案。我的最终解决方案是:awk -v PATTERN="whatever-%d.txt" 'BEGIN {n=1; FILE=sprintf(PATTERN, n)} !NF {n++; FILE=sprintf(PATTERN, n); next} {print &gt; FILE}' $filename
  • 请注意,您可能以这种方式打开了太多文件句柄。只有 gnu awk 会自动解决这个问题。更好的版本是:awk -v RS= '{f="whatever=" NR ".txt"; print &gt; f; close(f)}' file
  • 不错。我正在寻找易于设置的东西,因为我的输入文件不是太大,这正好符合要求,简短但清晰,我可以根据需要进行调整
【解决方案6】:

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

就这样。 &lt;&gt; 是“魔术”文件句柄,因为它读取管道数据或从命令行上指定的文件(打开并读取它们)。这类似于sedgrep 的工作方式。

这可以简化为一个衬里:

perl -00 -pe 'open ( $out, '>', "filename_".++$n ); select $out;'  yourfilename_here

【讨论】:

  • -00?嗯,这是新的东西。但我确实尽量避免使用一个衬里:)
  • 我一般都会这样做,但是当我们参加awk 比赛时,我会尝试将它们包括在内以进行比较。 (但尽可能一些说明更清楚的代码之后)。
  • 谢谢!就是这样!但是,在第一次运行此命令时,我遇到了与其他脚本相同的情况。原因显然是我的输入数据文件(每个文件有 4-8M 行长)有不正确的行分隔符或奇怪的东西。每当我在任何文本编辑器上打开它们时,它们看起来都很好。但是运行此命令会生成一个与输入文件相同的文件。但是在我将每个数据集复制粘贴(呃)到文本编辑器上的空白页并点击保存后,它们的文件大小会发生一些变化(比如 150MB 文件上的 1M),之后这个命令运行得很好。
【解决方案7】:

如果您收到“打开的文件过多”错误如下...

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

【讨论】:

    【解决方案8】:

    您可以使用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哲学)
    【解决方案9】:

    你也可以试试split -p "^$"

    【讨论】:

    • 这是 BSD 的 split (macos)