我有一个相当大的文件(35Gb),我想就地过滤该文件(即我没有足够的磁盘空间来存放另一个文件),特别是我想grep并忽略某些模式-有没有办法这样做而无需使用其他文件?
假设我要过滤掉所有包含foo:
例如...的行
我有一个相当大的文件(35Gb),我想就地过滤该文件(即我没有足够的磁盘空间来存放另一个文件),特别是我想grep并忽略某些模式-有没有办法这样做而无需使用其他文件?
假设我要过滤掉所有包含foo:
例如...的行
Answers:
在系统调用级别,这应该是可能的。程序可以在不截断的情况下打开要写入的目标文件,然后开始写入从stdin中读取的内容。读取EOF时,输出文件可以被截断。
由于您要过滤输入中的行,因此输出文件的写入位置应始终小于读取位置。这意味着您不应使用新输出破坏输入。
但是,找到执行此操作的程序就是问题。dd(1)
具有conv=notrunc
不在打开时截断输出文件的选项,但也不会在最后截断,而是将原始文件内容保留在grep内容之后(使用类似的命令grep pattern bigfile | dd of=bigfile conv=notrunc
)
由于从系统调用的角度来看非常简单,因此我编写了一个小程序,并在一个小型(1MiB)完整回送文件系统上进行了测试。它完成了您想要的操作,但是您真的想首先使用其他文件进行测试。覆盖文件总是有风险的。
覆盖
/* This code is placed in the public domain by camh */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char **argv)
{
int outfd;
char buf[1024];
int nread;
off_t file_length;
if (argc != 2) {
fprintf(stderr, "usage: %s <output_file>\n", argv[0]);
exit(1);
}
if ((outfd = open(argv[1], O_WRONLY)) == -1) {
perror("Could not open output file");
exit(2);
}
while ((nread = read(0, buf, sizeof(buf))) > 0) {
if (write(outfd, buf, nread) == -1) {
perror("Could not write to output file");
exit(4);
}
}
if (nread == -1) {
perror("Could not read from stdin");
exit(3);
}
if ((file_length = lseek(outfd, 0, SEEK_CUR)) == (off_t)-1) {
perror("Could not get file position");
exit(5);
}
if (ftruncate(outfd, file_length) == -1) {
perror("Could not truncate file");
exit(6);
}
close(outfd);
exit(0);
}
您可以将其用作:
grep pattern bigfile | overwrite bigfile
我主要是将其发布给其他人,然后再尝试使用。也许其他人知道某个程序执行的功能经过更多测试。
grep
不会输出比读取更多的数据,因此写入位置应始终位于读取位置之后。即使您以与阅读相同的速度书写,也可以。尝试使用rot13而不是grep,然后再次尝试。md5sum的前后,您将看到相同的内容。
您可以用来sed
在适当位置编辑文件(但这确实会创建一个中间临时文件):
删除包含foo
以下内容的所有行:
sed -i '/foo/d' myfile
要保留所有包含foo
以下内容的行:
sed -i '/foo/!d' myfile
$HOME
将是可写的,但/tmp
将是只读的(默认情况下)。例如,如果您具有Ubuntu,并且已启动到故障恢复控制台,则通常是这种情况。同样,here-document运算符<<<
也不会在那里工作,因为它需要/tmp
是r / w,因为它也会在其中写入一个临时文件。(strace
我假设您的filter命令就是我所说的前缀收缩过滤器,它具有以下属性:在读取至少N个字节之前,从不写入输出中的字节N。grep
具有此属性(只要它只是过滤而不做其他事情(例如为匹配项添加行号))。使用这种过滤器,您可以在进行过程中覆盖输入。当然,您需要确保不要犯任何错误,因为文件开头的被覆盖部分将永远丢失。
大多数Unix工具仅提供附加到文件或将其截断的选择,而无法覆盖它。标准工具箱中的一个例外是dd
,可以告诉它不要截断其输出文件。因此,计划是将命令过滤到中dd conv=notrunc
。这不会更改文件的大小,因此,我们还获取了新内容的长度,并将文件截断为该长度(再次使用dd
)。请注意,此任务本来就不那么严格-如果发生错误,您就自己一个人。
export LC_ALL=C
n=$({ grep -v foo <big_file |
tee /dev/fd/3 |
dd of=big_file conv=notrunc; } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek=$n
您可以编写难看的Perl。这是一个快速的实现方式,但并没有设法提高效率。当然,您可能还希望直接使用该语言进行初始过滤。
grep -v foo <big_file | perl -e '
close STDOUT;
open STDOUT, "+<", $ARGV[0] or die;
while (<STDIN>) {print}
truncate STDOUT, tell STDOUT or die
' big_file
使用任何类似伯恩的外壳:
{
cat < bigfile | grep -v to-exclude
perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile
由于某些原因,似乎人们往往会忘记40岁的¹和标准的读写读写重定向操作符。
我们打开bigfile
在读+写模式和(大多数在这里重要的),而上截断stdout
而bigfile
打开(单独)上cat
的stdin
。后grep
已经终止,如果它已经删除了一些行,stdout
现在指向内的某个地方bigfile
,我们需要摆脱的东西超出了这一点。因此,该perl
命令会truncate STDOUT
在当前位置(由返回tell STDOUT
)截断文件()。
(cat
对于GNU来说grep
,如果stdin和stdout指向同一文件,它会发出抱怨)。
¹好吧,虽然<>
从70年代末开始就一直在Bourne的外壳中工作,但最初并没有记录,也没有正确实施。它不是ash
1989年的原始实现,虽然它是POSIX sh
重定向运算符(因为POSIX sh
一直基于90年代初,所以一直到2000 ksh88
年才添加到FreeBSD sh
中,所以大约15年)旧的可能更准确。还要注意,<>
所有外壳程序中都没有指定默认文件描述符,除了ksh93
在2010年ksh93t +中将其从0更改为1(破坏了向后兼容性和POSIX兼容性)
perl -e 'truncate STDOUT, tell STDOUT'
吗?它对我有用,但不包括在内。在不使用Perl的情况下有什么方法可以实现相同目的的?
redirection "<>" fixed and documented (used in /etc/inittab f.i.).
这是一个提示。
尽管这是一个古老的问题,但在我看来,这是一个长期存在的问题,并且比目前建议的解决方案更通用,更清晰。归功于应归功于的信誉:我不确定如果不考虑StéphaneChazelas提及<>
更新操作员的问题,我会提出建议。
在Bourne Shell中打开文件进行更新的功能有限。Shell使您无法查找文件,也无法设置其新长度(如果比旧文件短)。但这很容易补救,所以很容易使我惊讶的是它不在的标准实用程序之列/usr/bin
。
这有效:
$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T;
1 foo
这样做(给斯特凡的帽子小费):
$ { grep foo T && ftruncate; } 1<>T && nl T;
1 foo
(我正在使用GNU grep。自从他写出答案以来,也许情况有所改变。)
除了,您没有/ usr / bin / ftruncate。对于几十行C,您可以参见下文。该ftruncate实用程序将任意文件描述符截断为任意长度,默认为标准输出和当前位置。
上面的命令(第一个例子)
T
进行更新。与open(2)一样,以这种方式打开文件会将当前偏移量设置为0。 T
正常处理,shell将其输出重定向到T
描述符4。然后,该子外壳程序退出,关闭描述符4。这是ftruncate:
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main( int argc, char *argv[] ) {
off_t i, fd=1, len=0;
off_t *addrs[2] = { &fd, &len };
for( i=0; i < argc-1; i++ ) {
if( sscanf(argv[i+1], "%lu", addrs[i]) < 1 ) {
err(EXIT_FAILURE, "could not parse %s as number", argv[i+1]);
}
}
if( argc < 3 && (len = lseek(fd, 0, SEEK_CUR)) == -1 ) {
err(EXIT_FAILURE, "could not ftell fd %d as number", (int)fd);
}
if( 0 != ftruncate((int)fd, len) ) {
err(EXIT_FAILURE, argc > 1? argv[1] : "stdout");
}
return EXIT_SUCCESS;
}
注意,以这种方式使用时,ftruncate(2)是不可移植的。为了绝对通用,请读取最后写入的字节,重新打开文件O_WRONLY,查找,写入字节并关闭。
考虑到这个问题已有5年的历史,我将说这种解决方案不是显而易见的。它利用exec打开一个新的描述符,并且<>
运算符都是奥秘的。我想不出一个通过文件描述符操纵索引节点的标准实用程序。(语法可能是ftruncate >&4
,但是我不确定是否有改进。)它比camh的胜任的探索性回答要短得多。除非您比我更喜欢Perl,否则它比IMOStéphane的要清晰一些。我希望有人觉得它有用。
执行相同操作的另一种方式是报告当前偏移量的可执行版本的lseek(2)。输出可以用于某些Linuxi提供的/ usr / bin / truncate。
ed
是就地编辑文件的正确选择:
ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q
END_OF_ED_COMMANDS
ed
版本的行为有所不同.....来自man ed
(GNU Ed 1.4)...If invoked with a file argument, then a copy of file is read into the editor's buffer. Changes are made to this copy and not directly to file itself.
ed
不是用于编辑35GB文件的解决方案,因为该文件已读入缓冲区。
!
)的流输入,因此可能会有更多有趣的窍门
ed
截断文件并将其重写。因此,这不会像OP所希望的那样就地更改磁盘上的数据。另外,如果文件太大而无法加载到内存中,则无法使用。
您可以使用bash的读/写文件描述符打开文件(覆盖它原位),然后sed
和truncate
...但当然,永远不要让数据读取到目前为止所做的更改比量大。
这是脚本(用途:bash变量$ BASHPID)
# Create a test file
echo "going abc" >junk
echo "going def" >>junk
echo "# ORIGINAL file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )
#
# Assign file to fd 3, and open it r/w
exec 3<> junk
#
# Choose a unique filename to hold the new file size and the pid
# of the semi-asynchrounous process to which 'tee' streams the new file..
[[ ! -d "/tmp/$USER" ]] && mkdir "/tmp/$USER"
f_pid_size="/tmp/$USER/pid_size.$(date '+%N')" # %N is a GNU extension: nanoseconds
[[ -f "$f_pid_size" ]] && { echo "ERROR: Work file already exists: '$f_pid_size'" ;exit 1 ; }
#
# run 'sed' output to 'tee' ...
# to modify the file in-situ, and to count the bytes
<junk sed -e "s/going //" |tee >(echo -n "$BASHPID " >"$f_pid_size" ;wc -c >>"$f_pid_size") >&3
#
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The byte-counting process is not a child-process,
# so 'wait' doesn't work... but wait we must...
pid_size=($(cat "$f_pid_size")) ;pid=${pid_size[0]}
# $f_pid_size may initially contain only the pid...
# get the size when pid termination is assured
while [[ "$pid" != "" ]] ; do
if ! kill -0 "$pid" 2>/dev/null; then
pid="" # pid has terminated. get the byte count
pid_size=($(cat "$f_pid_size")) ;size=${pid_size[1]}
fi
done
rm "$f_pid_size"
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#
exec 3>&- # close fd 3.
newsize=$(cat newsize)
echo "# MODIFIED file (before truncating)";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo ) cat junk
#
truncate -s $newsize junk
echo "# NEW (truncated) file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo ) cat junk
#
exit
这是测试输出
# ORIGINAL file
going abc
going def
# 2 lines, 20 bytes
# MODIFIED file (before truncating)
abc
def
c
going def
# 4 lines, 20 bytes
# NEW (truncated) file
abc
def
# 2 lines, 8 bytes
并非完全原位,而是-在类似情况下可能会有用。
如果磁盘空间有问题,请先压缩文件(因为它是文本,这将大大减少文件大小),然后在解压缩/压缩管道中间以常规方式使用sed(或grep或其他命令)。
# Reduce size from ~35Gb to ~6Gb
$ gzip MyFile
# Edit file, creating another ~6Gb file
$ gzip -dc <MyFile.gz | sed -e '/foo/d' | gzip -c >MyEditedFile.gz
sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
为了使任何使用此问题的人都受益,正确的答案是停止寻找晦涩的shell功能,这些功能可能会损坏您的文件,从而导致性能提升可忽略不计,而应使用此模式的一些变体:
grep "foo" file > file.new && mv file.new file
仅在由于某种原因这是不可行的极少数情况下,您才应该认真考虑此页面上的其他答案(尽管它们肯定很有趣)。我将承认,OP没有足够的磁盘空间来创建第二个文件的难题就是这种情况。即使到那时,也有其他选项可用,例如@Ed Randall和@Basile Starynkevitch提供的选项。
echo -e "$(grep pattern bigfile)" >bigfile
grepped
数据超过命令行允许的长度,则此方法不起作用。然后破坏了数据