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


16

我对大文件和文件有一个奇怪的问题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中发现了bug(似乎不太可能缝制),或者我做错了什么?

更新

读取文件的2G后会发生此问题。它的接缝read和重定向不喜欢比2G大的文件。但仍在寻找更精确的解释。

Update2

绝对看起来像个错误。可以用以下方式复制它:

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正如Gilles早先指出的那样,受影响的文件:

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。As lseek返回从文件开头开始的偏移量,当偏移量超过2GB时,该int值为负,这会导致测试if (r >= 0)在应该成功的地方失败。


1
您可以使用较小的输入数据集来复制问题吗?总是相同的输入线会导致这些问题吗?
larsk's

@larks:好问题。问题总是始于第13.520.918行(实际上是我进行的测试的两倍)。该行之前的文件大小为2.147.487.726。它缝接这里有一个32位的限制,但不完全是我们略高于2 ^ 31(2.147.483.648),而是恰好是4K缓冲区限制(2 ^ 31 + 4K = 2.147.487.744)。上一行和下一行通常是100到200个字符的行。
jfg956

在第二个文件(大约相同的大小)上进行了测试:问题始于行#13.522.712,该文件在该行之前大2.147.498.679字节。它接缝指向readbash中语句限制的方向。
jfg956

Answers:


13

您已经在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

我没有详细阅读源代码,但是我猜想,当绝对偏移为负数时,过渡点不会顺利进行。因此,bash在超过2GB标记后重新填充缓冲区时,最终会读取错误的偏移量。

如果我的结论是错误的,并且您的bash实际上是在64位平台上运行或使用大文件支持进行编译,则肯定是一个错误。请报告给您的发行商或上游

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


1
Merci Gilles。很好的答案:完整,具有足够的信息,即使对于没有较强CS背景(32位...)的人也可以理解该问题。(镜像也有助于查询行号,应该予以确认。)此后,我也遇到了32位问题并下载了源代码,但还没有达到此水平。Merci Encore等人。
jfg956

4

我不知道错是什么,但肯定是令人费解的。如果您的输入行如下所示:

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您的日志文件匹配的行,并标记不以stdout上的时间戳开头的行。


我的文件中没有假行:cut -c 1-10 file.log | uniq -c给了我预期的结果。我正在使用,${line:0:4}-${line:5:2}-${line:8:2}因为我将文件放在目录中${line:0:4}/${line:5:2}/${line:8:2},并且简化了问题(我将更新问题说明)。我知道awk可以在这里为我提供帮助,但是我在使用它时遇到了其他问题。我想要的是了解问题bash,而不是找到替代解决方案。
jfg956 2012年

如您所说...如果“简化”问题,您可能不会获得想要的答案。我仍然认为,使用bash解决此问题并不是处理此类数据的正确方法,但是没有理由不起作用。
larsk's

简化的问题给出了我在问题中提出的意外结果,因此我认为这不是过于简化。此外,简化的问题给出了与有效cut语句相似的结果。因为我想将苹果与苹果而不是桔子进行比较,所以我需要使事情尽可能相似。
jfg956'3

1
我给您留了一个问题,可能会帮助您弄清楚问题出在哪里...
larsks 2012年

2

听起来您想要做的是:

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填满保持打开文件表。


感谢您的awk解决方案。我已经有了类似的东西。我的问题是要了解bash的限制,而不是找到其他解决方案。
jfg956
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.