我对大文件和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
给出以下结果(我输入x
es 是为了保密数据,其他字符是真实的)。注意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。受影响的版本是bash
Debian 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
可以防止打开的文件表被填满。