我有一个很大的文件 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
虽然awk
和split
方法更短,但其他工具(如 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 ;