有没有办法就地修改文件?


54

我有一个相当大的文件(35Gb),我想就地过滤该文件(即我没有足够的磁盘空间来存放另一个文件),特别是我想grep并忽略某些模式-有没有办法这样做而无需使用其他文件?

假设我要过滤掉所有包含foo:例如...的行


3
@Tshepang:我认为他想写回同一文件。
Faheem Mitha

5
“就地”是拉丁语短语,意为“就地”。从字面上看,“就位”。
Faheem Mitha

3
在那种情况下,问题应该更清楚一些,例如是否有一种就地修改文件的方法
tshepang 2011年

5
@Tshepang,“原位”是一个相当普遍的短语,在英语中用它来准确地描述这一点-我认为标题是很容易解释的……@Gilles,我想了那么多,更容易等待更多的磁盘空间!;)
Nim11

2
@Nim:嗯,我想就地比更常见原位
tshepang 2011年

Answers:


41

在系统调用级别,这应该是可能的。程序可以在不截断的情况下打开要写入的目标文件,然后开始写入从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

我主要是将其发布给其他人,然后再尝试使用。也许其他人知道某个程序执行的功能经过更多测试。


我想看看我是否可以不写任何东西而逃脱!:)我想这可以解决问题!谢谢!
Nim

2
+1代表C;的确可以正常工作,但是我看到一个潜在的问题:当右侧文件写入同一文件时,正在从左侧读取文件,除非您协调两个进程,否则可能会覆盖同一文件块。对于文件完整性来说,使用较小的块大小可能会更好,因为大多数核心工具可能会使用8192。这可能会减慢程序速度,避免发生冲突(但不能保证)。也许将较大的部分读入内存(不是全部)并以较小的块写入。也可以添加nanosleep(2)/ usleep(3)。
Arcege 2011年

4
@Arcege:编写不是分块进行的。如果您的读取过程读取了2个字节,而您的写入过程写入了1个字节,则只有第一个字节会发生变化,并且读取过程可以继续读取字节3,而原始内容在该点不变。由于grep不会输出比读取更多的数据,因此写入位置应始终位于读取位置之后。即使您以与阅读相同的速度书写,也可以。尝试使用rot13而不是grep,然后再次尝试。md5sum的前后,您将看到相同的内容。
camh 2011年

6
真好 这可能是Joey Hess的moreutils的宝贵补充。您可以使用dd,但这很麻烦。
吉尔(Gilles)'所以

'grep模式bigfile | 覆盖bigfile”-我的工作没有错误,但我不明白的是-是否不需要用其他文本替换模式中的内容?所以不应该像这样:'grep pattern bigfile | 覆盖/替换文本/大文件”
亚历山大·米尔斯,

20

您可以用来sed在适当位置编辑文件(但这确实会创建一个中间临时文件):

删除包含foo以下内容的所有行:

sed -i '/foo/d' myfile

要保留所有包含foo以下内容的行:

sed -i '/foo/!d' myfile

有趣的是,此临时文件是否需要与原始文件大小相同?
Nim,

3
是的,所以可能不好。
pjc50 2011年

17
这不是OP所要求的,因为它创建了第二个文件。
Arcege 2011年

1
此解决方案将在只读文件系统上失败,其中“只读”表示您$HOME 是可写的,但/tmp将是只读的(默认情况下)。例如,如果您具有Ubuntu,并且已启动到故障恢复控制台,则通常是这种情况。同样,here-document运算符<<<也不会在那里工作,因为它需要/tmpr / w,因为它也会在其中写入一个临时文件。(strace
请参见此

是的,这对我也不起作用,我尝试过的所有sed命令都将用新文件替换当前文件(尽管使用--in-place标志)。
亚历山大·米尔斯

19

我假设您的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

16

使用任何类似伯恩的外壳:

{
  cat < bigfile | grep -v to-exclude
  perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile

由于某些原因,似乎人们往往会忘记40岁的¹和标准的读写读写重定向操作符。

我们打开bigfile在读+写模式和(大多数在这里重要的),而上截断stdoutbigfile打开(单独)上catstdin。后grep已经终止,如果它已经删除了一些行,stdout现在指向内的某个地方bigfile,我们需要摆脱的东西超出了这一点。因此,该perl命令会truncate STDOUT在当前位置(由返回tell STDOUT)截断文件()。

cat对于GNU来说grep,如果stdin和stdout指向同一文件,它会发出抱怨)。


¹好吧,虽然<>从70年代末开始就一直在Bourne的外壳中工作,但最初并没有记录,也没有正确实施。它不是ash1989年的原始实现,虽然它是POSIX sh重定向运算符(因为POSIX sh一直基于90年代初,所以一直到2000 ksh88年才添加到FreeBSD sh中,所以大约15年)旧的可能更准确。还要注意,<>所有外壳程序中都没有指定默认文件描述符,除了ksh93在2010年ksh93t +中将其从0更改为1(破坏了向后兼容性和POSIX兼容性)


2
你能解释一下perl -e 'truncate STDOUT, tell STDOUT'吗?它对我有用,但不包括在内。在不使用Perl的情况下有什么方法可以实现相同目的的?
亚伦·布伦库什

1
@AaronBlenkush,请参阅编辑。
斯特凡Chazelas

1
绝对精彩-谢谢。那时我在那儿,但是不记得了。...“ 36岁”标准的参考很有趣,因为en.wikipedia.org/wiki/Bourne_shell上没有提到它。它是用来干什么的?我看到了对SunOS 5.6中的错误修复的引用:redirection "<>" fixed and documented (used in /etc/inittab f.i.). 这是一个提示。
nealmcb

2
@nealmcb,请参阅编辑。
斯特凡Chazelas

@StéphaneChazelas您的解决方案与这个答案相比如何?它显然做同样的事情,但是看起来更简单。
akhan

9

尽管这是一个古老的问题,但在我看来,这是一个长期存在的问题,并且比目前建议的解决方案更通用,更清晰。归功于应归功于的信誉:我不确定如果不考虑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实用程序将任意文件描述符截断为任意长度,默认为标准输出和当前位置。

上面的命令(第一个例子)

  • 打开文件描述符4 T进行更新。与open(2)一样,以这种方式打开文件会将当前偏移量设置为0。
  • 然后grepT正常处理,shell将其输出重定向到T描述符4。
  • ftruncate在描述符4上调用ftruncate(2),将长度设置为当前偏移量的值(恰好是grep离开的位置)。

然后,该子外壳程序退出,关闭描述符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


5

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.
Peter.O 2011年

@fred,如果您暗示保存更改不会影响命名文件,那是不正确的。我解释说帖说,你的更改不会反映,直到您保存它们。我确实承认,这ed不是用于编辑35GB文件的解决方案,因为该文件已读入缓冲区。
glenn jackman 2011年

2
我以为这意味着完整的文件将被加载到缓冲区中..但是也许只有它需要的部分才被加载到缓冲区中..我一直对ed感到好奇...我认为可以进行原位编辑...我只需要尝试一个文件...如果可行,这是一个合理的解决方案,但是在我撰写本文时,我开始认为这可能是sed的灵感来源(摆脱了处理大数据块的麻烦……我注意到,“ ed”实际上可以接受脚本(以前缀!)的流输入,因此可能会有更多有趣的窍门
Peter.O

我非常确定in的写操作会ed截断文件并将其重写。因此,这不会像OP所希望的那样就地更改磁盘上的数据。另外,如果文件太大而无法加载到内存中,则无法使用。
尼克·马特奥

5

您可以使用bash的读/写文件描述符打开文件(覆盖它原位),然后sedtruncate...但当然,永远不要让数据读取到目前为止所做的更改比量大。

这是脚本(用途: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

3

我会对该文件进行内存映射,使用指向裸内存的char *指针就地执行所有操作,然后取消映射文件并截断​​它。


3
+1,但这仅是因为64位CPU和OS的广泛可用性使得现在可以使用35 GB的文件来实现。那些仍在32位系统上的用户(我怀疑,即使是该网站的访问者中的绝大多数,我也无法使用)。
沃伦·杨

2

并非完全原位,而是-在类似情况下可能会有用。
如果磁盘空间有问题,请先压缩文件(因为它是文本,这将大大减少文件大小),然后在解压缩/压缩管道中间以常规方式使用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

2
但可以肯定的是,gzip在将压缩版本替换为磁盘之前将压缩版本写入磁盘,因此,与其他选项不同,您至少需要那么多的额外空间。但是,如果您有空间(我没有...),它会更安全
nealmcb

这是一个聪明的解决方案,可以进一步优化以仅执行一次压缩而不是执行两次压缩:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen

0

为了使任何使用此问题的人都受益,正确的答案是停止寻找晦涩的shell功能,这些功能可能会损坏您的文件,从而导致性能提升可忽略不计,而应使用此模式的一些变体:

grep "foo" file > file.new && mv file.new file

仅在由于某种原因这是不可行的少数情况下,您才应该认真考虑此页面上的其他答案(尽管它们肯定很有趣)。我将承认,OP没有足够的磁盘空间来创建第二个文件的难题就是这种情况。即使到那时,也有其他选项可用,例如@Ed Randall和@Basile Starynkevitch提供的选项。


1
我可能会误会,但与OP最初的要求无关。aka大文件的内联编辑,没有足够的磁盘空间来存储临时文件。
Kiwy,

@Kiwy这是针对此问题的其他观众的一个答案(到目前为止,该观众已接近15,000)。问题“是否有一种就地修改文件的方法?” 与OP的特定用例相比具有更广泛的相关性。
Todd Owen

-3

echo -e "$(grep pattern bigfile)" >bigfile


3
如果文件很大并且grepped数据超过命令行允许的长度,则此方法不起作用。然后破坏了数据
Anthon 2013年
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.