Awk 编程:根据模式将大文件拆分为较小的文件

Awk 编程:根据模式将大文件拆分为较小的文件

我有一个很大的文件 input.dat,如下所示。

kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000
 kpoint2 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

我需要将文件拆分成两个较小的文件,如下所示

kpoint1.dat

kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

kpoint2.dat

kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

我写了一个小脚本来实现这个功能。脚本如下所示。

for j in {1..2} 
do
    awk '$1=="kpoint'$j'" {for(i=1; i<=3; i++){getline; print}}' tmp7 >kpoint'$j'.dat
done

该脚本会创建具有所需名称的输出文件。但所有文件都是空的。有人能帮我解决这个问题吗?

答案1

这可以完全在以下方式完成awk

$ awk '$1 ~ /kpoint[0-9]/ { file = $1 ".dat" } {print > file}' file
$ head kpoint*
==> kpoint1.dat <==
kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

==> kpoint2.dat <==
 kpoint2 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

Awk 还支持> file重定向,但有一些细微的差别(请参阅GNU awk 手册了解更多信息)。

答案2

尽管muru 的回答是最简单的,还有其他几种无需使用 awk 的方法。

Perl

使用 awk 的方法基本上是,我们写入特定文件名,并且当且仅当我们在行首遇到 kpoint 时才更改该文件名。Perl 也可以采用相同的方法:

$ perl -ane '$p=$F[0] if $F[0] =~ /kpoint/;open($f,">>",$p . ".dat"); print $f $_' input.txt

工作原理如下:

  • -a标志允许我们使用@F从输入文件的每一行自动拆分的特殊单词数组。因此$F[0]指的是第一个单词,就像$1在 awk 中一样
  • $p=$F[0] if $F[0] =~ /kpoint/意味着当且仅当在行中时才会改变$p(即前缀变量) 。该模式匹配的改进可能是kpoint/^ *kpoint/
  • 每次迭代我们都会打开追加一个文件,其名称由字符串$p组成.dat;请注意,附加部分很重要。如果您想要清晰运行,您可能想要删除旧kpoint文件。如果我们希望文件始终是新创建的并被覆盖,那么我们可以重新请求原始命令:

    $ perl -ane 'if ($F[0] =~ /kpoint/){$p=$F[0]; open($f,">",$p . ".dat")}; print $f $_' input.txt
    
  • 最后print $f $_只打印我们打开的文件名。

分裂

从您的示例中可以看出,每个条目由 5 行组成。如果这是恒定的,我们可以用这种方式拆分文件,而不依赖于模式匹配split。具体来说是这个命令:

$ split --additional-suffix=".dat" --numeric-suffixes=1 -l 5 input.txt  kpoint

该命令中的选项如下:

  • --additional-suffix=".dat".dat是将添加到每个创建的文件的静态后缀
  • --numeric-suffixes=1将允许我们向每个文件名添加从 1 开始的变化数字
  • -l 5将允许每 5 行分割输入文件
  • input.txt是我们要拆分的文件
  • kpoint将是静态文件名前缀

以下是实际操作方式:

$ split --additional-suffix=".dat" --numeric-suffixes=1 -l 5 input.txt  kpoint                                                                        
$ cat kpoint01.dat                                                                                                                                    
kpoint1 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000
$ cat kpoint02.dat                                                                                                                                    
 kpoint2 :       0.0000    0.0000    0.0000
  band No.  band energies     occupation 
      1     -52.8287      2.00000
      2     -52.7981      2.00000
      3     -52.7981      2.00000

或者,我们也可以添加--suffix-length=1以保持每个数字后缀的长度更短,而kpoint1不是kpoint01,但如果您有大量的 s,这可能会成为问题kpoint

替代 awk

这个类似于muru 的回答,除了这里我们使用不同的模式匹配以及通过不同的方法创建文件名变量sprintf()

$ awk '/^\ *kpoint/{f=sprintf("%s.dat",$1)};{print > f}' input.txt

Python

虽然awksplit方法更短,但其他工具(如 Python)非常适合文本处理,我们可以使用这些工具来实现更详细但有效的解决方案。

下面的脚本正是这样做的,它按照向后查看我们保存的行列表的思路运行。脚本不断保存行,直到遇到kpoint行的开头,这意味着我们到达了新条目,也意味着我们需要将前一个条目写入其各自的文件。

#!/usr/bin/env python3
import sys

def write_entry(pref,line_list):
    # this function writes the actual file for each entry
    with open(".".join([pref,"dat"]),"w") as entry_file:
        entry_file.write("".join(line_list))

def main():
    prefix = ""
    old_prefix = ""
    entry=[]
    with open(sys.argv[1]) as fd:
        for line in fd:
            # if we encounter kpoint string, that's a signal
            # that we need to write out the list of things 
            if line.strip().startswith('kpoint'):
                prefix=line.strip().split()[0]
                # This if statement counters special case
                # when we just started reading the file
                if not old_prefix:
                    old_prefix = prefix
                    entry.append(line)
                    continue
                write_entry(old_prefix,entry)
                old_prefix = prefix
                entry=[]
            # Keep storing lines. This works nicely after old 
            # entry has been cleared out. 
            entry.append(line)
    # since we're looking backwards, we need one last call
    # to write last entry when input file has been closed
    write_entry(old_prefix,entry)

if __name__ == '__main__': main()

纯 Bash

几乎与 Perl 方法的想法相同 - 我们不断地将所有内容写入特定的文件名,并且仅在其中找到行时才更改文件名kpoint

#!/usr/bin/env bash

while IFS= read -r line;
do
    case "$line" in
        # We found next entry. Use word-splitting to get
        # filename into fname variable, and truncate that filename
        *kpoint[0-9]*) read fname trash <<< $line  && 
                       echo "$line" > "$fname".dat ;;
        # That's just a line within entry. Append to 
        # current working file
        *) echo "$line" >> "$fname".dat ;;
    esac
done < "$1"

# Just in case there are trailing lines that weren't processed
# in while loop, append them to last filename
[ -n "$line" ] && echo "$line" >> "$fname".dat ;

相关内容