如何找出一个数字与文件中下一次出现的相同数字之间有多少行?

How can I find out how many lines are between a number and the next occurrence of the same number in a file?

我有很大的文件,每个文件都存储了非常长的计算结果。这是一个文件示例,其中有五个时间步长的结果; 第三、第四和第五 个时间步的输出存在问题。

(请注意,我一直很懒,在我的示例中使用了相同的数字来表示每个时间步的结果。实际上,每个时间步的数字都是唯一的。)

     3
 i =        1, time =        1.000, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709
  O        23.8223149199        7.2029442290        0.4030956770
     3
 i =        2, time =        1.500, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709
  O        23.8223149199        7.2029442290        0.4030956770
     3
 i =        3, time =        2.000, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709
  O        23.8223149199                                       (<--Problem: calculation stopped and some numbers are missing)
     3
 i =        4, time =        2.500, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709 (Problem: calculation stopped and entire row is missing below)
     3
 i =        5, time =        3.000, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709
  O        23.8223149199        7.2029442290        0.4030956770 sdffs (<--Problem: rarely, additional characters can be printed but I figured out how to identify the longest lines in the file and don't have this problem this time)

问题是计算可能会失败(然后需要重新启动),因为结果正在打印到文件中。这意味着当我尝试使用结果时,我遇到了问题。

我的问题是,我如何才能发现出现问题和结果文件被弄乱的情况?最常见的问题是没有“3”行结果(加上 header,这是 i = ... 所在的行)?如果我能找到问题行,我就可以删除那个时间步。

这是我在尝试使用 messed-up 文件时得到的错误输出示例:

Traceback (most recent call last):
  File "/mtn/storage/software/languages/anaconda/Anaconda3-2018.12/lib/python3.7/site-packages/aser/io/extxyz.py", line 593, in read_xyz
    nentss = int(line)
ValueError: invalid literal for int() with base 10: '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "pythonPostProcessingCode.py", line 25, in <module>
    path = read('%s%s' % (filename, fileext) , format='xyz', index=':')  # <--This line tells me that Python cannot read in a particular time step because the formatting is messed up.

我对 scripting/Awk 等没有经验,所以如果有人认为我没有使用适当的问题标签,欢迎 heads-up。谢谢。

将记录的匹配分布在 2 行中以尝试合并 i = ... 有点困难,但我认为您实际上不需要这样做。看起来一条新记录可以通过出现只有一列的行来区分。如果是这种情况,您可以执行以下操作:

awk -v c=330 -v p=1 'function pr(n) {
    if( n - p == c)  printf "%s", buf; 
    buf = ""} 
    NF == 1 { pr(NR); p = NR; c =  } 
    {buf = sprintf("%s%s\n", buf, [=10=])} 
    END {pr(NR+1)}' input-file

在上面,每当看到一行只有一条记录时,期望的是下一条记录中会有很多行。如果该号码不匹配,则不打印记录。为避免该逻辑,只需删除第 4 行末尾附近的 c = 。您需要 -v c=330 的唯一原因是启用该分配的删除;如果要单列行作为记录的行数,可以省略-v c=330.

header 加上 330 表示 331 行文本等

awk 'BEGIN { RS="i =" } { split([=10=],bits,"\n");if (length(bits)-1==331) { print RS[=10=] } }' file > newfile

解释:

awk 'BEGIN { 
             RS="i =" 
           } 
           { 
             split([=11=],bits,"\n");
             if (length(bits)-1==331) { 
                print RS[=11=] 
             } 
           }' file > newfile

在处理名为 file 的文件中的任何行之前,将记录分隔符设置为“i =”。然后,对于每条记录,使用 split 将记录 ($0) 拆分成一个数组 bits,以换行为分隔符。其中数组位的长度,减 1 是 331 打印记录分隔符加上记录,将输出重定向到一个名为 newfile

的新文件

如果允许写入中间文件,您可以使用csplit在单独的文件中抓取每条记录。

csplit -k infile '/i = /' {*}

然后可以看到哪些记录是完整的,哪些没有使用wc -l xx*(注意:xx是分割文件的默认前缀)

然后您可以对这些记录做任何您想做的事情,包括恰好有 331 行的列表文件:

wc -l xx* | sed -n 's/^ *331 \(xx.*\)//p'

如果您想用所有 有效 记录构建一个新文件,只需将它们连接起来:

wc -l xx* | sed -n 's/^ *331 \(xx.*\)//p' | xargs cat > newfile

除其他用途外,您还可以存档失败的记录:

wc -l xx* | sed -e '$d' -e '/^ *331 \(xx.*\)/d' | xargs cat > failures

听起来这就是你想要的:

$ cat tst.awk
/^ i =/  {
    prt()
    expNumLines = prev + 1
    actNumLines = 2
    rec = prev RS [=10=]
    next
}
NF == 4 {
    rec = rec RS [=10=]
    actNumLines++
}
{ prev = [=10=] }
END { prt() }

function prt() {
    if ( (actNumLines == expNumLines) && (rec != "") ) {
        print "-------------"
        print rec
    }
}

$ awk -f tst.awk file
-------------
     3
 i =        3, time =        2.000, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709
-------------
     3
 i =        5, time =        3.000, E =     1234567
  Mg       22.9985897185        6.9311166109        0.7603733573
  O        23.0438129644        6.4358253659        1.5992513709

只需更改 prt() 函数即可对有效记录执行您想执行的任何操作。

这个答案实际上与 bash 无关,但如果性能是个问题,您可能会感兴趣,因为您似乎要处理非常大的文件。

考虑到您可以编译一些非常基本的 C 程序,您可以构建此代码:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Constants are hardcoded to make the program more readable
// But they could be passed as program argument
const char separator[]="i =";
const unsigned int requiredlines=331;

int main(void) {
    char* buffer[331] = { NULL, };
    ssize_t buffersizes[331] = { 0, };
    size_t n = requiredlines+1; // Ignore lines until the separator is found
    char* line = NULL;
    size_t len = 0;
    ssize_t nbread;
    size_t i;

    // Iterate through all lines
    while ((nbread = getline(&line, &len, stdin)) != -1) {

        // If the separator is found:
        // - print the record (if valid)
        // - reset the record (always)
        if (strstr(line, separator)) {
            if (n == requiredlines) {
                for (i = 0 ; i < requiredlines ; ++i) printf("%s", buffer[i]);
            }
            n = 0;
        }

        // Add the line to the buffer, unless too many lines have been read
        // (in which case we may discard lines until the separator is found again)
        if (n < requiredlines) {
            if (buffersizes[n] > nbread) {
                strncpy(buffer[n], line, nbread);
                buffer[n][nbread] = '[=10=]';
            } else {
                free(buffer[n]);
                buffer[n] = line;
                buffersizes[n] = nbread+1;
                line = NULL;
                len = 0;
            }
        }
        ++n;
    }

    // Don't forget about the last record, if valid
    if (n == requiredlines) {
        for (i = 0 ; i < requiredlines ; ++i) printf("%s", buffer[i]);
    }

    free(line);
    for (i = 0 ; i < requiredlines ; ++i) free(buffer[i]);

    return 0;
}

程序可以这样编译:

gcc -c prog.c && gcc -o prog prog.o

那么可以这样执行:

./prog < infile > outfile

为了简化代码,它从 stdin 读取并输出到 stdout,但是在 Bash 考虑到您可以使用的重定向流的所有选项,这已经足够了。如果需要,代码可以直接适应 read/write from/to 个文件。

我已经在一个包含 1000 万行的生成文件上对其进行了测试,并将其与基于 awk 的解决方案进行了比较。

(time awk 'BEGIN { RS="i =" } { split([=13=],bits,"\n");if (length(bits)-1==331) { printf "%s",RS[=13=] } }' infile) > outfile

real    0m24.655s
user    0m24.357s
sys     0m0.279s

(time ./prog < infile) > outfile

real    0m1.414s
user    0m1.291s
sys     0m0.121s

在此示例中,它的运行速度比 awk 解决方案快大约 18 倍。你的里程可能会有所不同(不同的数据,不同的硬件)但我想它应该总是快得多。

我应该提到 awk 解决方案的速度非常快(对于脚本解决方案而言)。我首先尝试用 C++ 编写解决方案,它的性能与 awk 相似,有时甚至比 awk 慢。