Bash 脚本和大文件(错误):使用重定向中的 read 内置输入进行输入会产生意外结果

Bash 脚本和大文件(错误):使用重定向中的 read 内置输入进行输入会产生意外结果

我对大文件和bash.这是上下文:

  • 我有一个大文件:75G 和​​ 400,000,000 多行(这是一个日志文件,我的错,我让它增长)。
  • 每行的前 10 个字符是格式为 YYYY-MM-DD 的时间戳。
  • 我想拆分该文件:每天一个文件。

我尝试使用以下脚本但不起作用。 我的问题是这个脚本不起作用,而不是替代解决方案

while read line; do
  new_file=${line:0:10}_file.log
  echo "$line" >> $new_file
done < file.log

经过调试,发现问题出在new_file变量上。这个脚本:

while read line; do
  new_file=${line:0:10}_file.log
  echo $new_file
done < file.log | uniq -c

给出以下结果(我输入xes 是为了保密数据,其他字符是真实的)。注意dh和 较短的字符串:

...
  27402 2011-xx-x4
  27262 2011-xx-x5
  22514 2011-xx-x6
  17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
      1 2011-xx-x2
      3 2011-xx-x1
...
     12 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1
      1 208--
      1 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1    
...

我的文件格式不是问题。该脚本cut -c 1-10 file.log | uniq -c仅提供有效的时间戳。有趣的是,上述输出的一部分变为cut ... | uniq -c

3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1

我们可以看到,在 uniq count 之后4474604,我的初始脚本失败了。

我是否达到了 bash 中我不知道的限制,我是否在 bash 中发现了错误(看起来不太可能),或者我做错了什么?

更新:

读取2G文件后出现问题。它接缝read和重定向不喜欢大于2G的文件。但仍在寻找更准确的解释。

更新2:

它看起来确实像一个错误。它可以通过以下方式复制:

yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c

但这作为一种解决方法效果很好(看来我发现了 的有用用途cat):

cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c 

一个错误已提交给 GNU 和 Debian。受影响的版本是bashDebian Squeeze 6.0.2 和 6.0.4 上的 4.1.5。

echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu

更新3:

感谢 Andreas Schwab 对我的错误报告做出了快速反应,这个补丁可以解决此不当行为。受影响的文件是lib/sh/zread.c正如吉尔斯早些时候指出的那样:

diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
      int fd; {   off_t off;
-  int r;
+  off_t r;

  off = lused - lind;   r = 0;

r变量用于保存 的返回值lseek。 aslseek返回距离文件开头的偏移量,当超过2GB时,该int值为负数,这会导致测试if (r >= 0)在应该成功的地方失败。

答案1

您在 bash 中发现了某种错误。这是一个已知的错误,并且已修复。

程序将文件中的偏移量表示为某种具有有限大小的整数类型的变量。在过去,每个人都使用int几乎所有东西,并且int类型限制为 32 位,包括符号位,因此它可以存储从 -2147483648 到 2147483647 的值。现在有不同的为不同的事物输入名称,包括off_t文件中的偏移量。

默认情况下,off_t在 32 位平台上是 32 位类型(最大允许 2GB),在 64 位平台上是 64 位类型(最大允许 8EB)。然而,使用 LARGEFILE 选项编译程序很常见,该选项将类型切换off_t为 64 位宽,并使程序调用合适的函数实现,例如lseek

您似乎在 32 位平台上运行 bash,并且您的 bash 二进制文件未使用大文件支持进行编译。现在,当您从常规文件中读取一行时,bash 使用内部缓冲区来批量读取字符以提高性能(有关更多详细信息,请参阅中的源代码)builtins/read.def)。当该行完成时,bash 调用lseek将文件偏移量倒回到该行末尾的位置,以防其他程序关心该文件中的位置。的调用lseek发生在zsyncfc函数中lib/sh/zread.c

我还没有详细阅读源代码,但我推测当绝对偏移量为负时,在过渡点某些事情不会顺利发生。因此,在超过 2GB 标记后,当 bash 重新填充缓冲区时,它最终会读取错误的偏移量。

如果我的结论是错误的,并且您的 bash 实际上运行在 64 位平台上或使用大文件支持进行编译,那么这绝对是一个错误。请将其报告给您的发行版或上游

无论如何,shell 并不是处理如此大文件的正确工具。会很慢。如果可能的话使用 sed,否则使用 awk。

答案2

我不知道什么是错,但它确实很复杂。如果您的输入行如下所示:

YYYY-MM-DD some text ...

那么这真的没有理由:

new_file=${line:0:4}-${line:5:2}-${line:8:2}_file.log

您正在做大量的子字符串工作,以最终得到看起来...与文件中已经看起来完全一样的东西。这个怎么样?

while read line; do
  new_file="${line:0:10}_file.log"
  echo "$line" >> $new_file
done

这只是抓取该行的前 10 个字符。您也可以完全放弃bash并仅使用awk

awk '{print > ($1 "_file.log")}' < file.log

$1这会获取(每行中第一个空格分隔的列)中的日期并使用它来生成文件名。

请注意,您的文件中可能存在一些虚假的日志行。也就是说,问题可能出在输入上,而不是您的脚本上。您可以扩展awk脚本来标记虚假行,如下所示:

awk '
$1 ~ /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
    print > ($1 "_file.log")
    next
}

{
    print "INVALID:", $0
}
'

这会写入YYYY-MM-DD与日志文件匹配的行,并在标准输出上标记不以时间戳开头的行。

答案3

听起来你想做的是:

awk '
{  filename = substr($0, 0, 10) "_file.log";  # input format same as output format
   if (filename != lastfile) {
       close(lastfile);
       print 'finished writing to', lastfile;
   }
   print >> filename;
   lastfile=filename;
}' file.log

close可以防止打开的文件表被填满。

相关内容