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

Nim*_*Nim 62 grep large-files text-processing

我有一个相当大的文件(35Gb),我想就地过滤这个文件(即我没有足够的磁盘空间来存放另一个文件),特别是我想 grep 并忽略一些模式 - 有没有办法在不使用其他文件的情况下执行此操作?

假设我想过滤掉所有包含foo:例如...

cam*_*amh 42

在系统调用级别,这应该是可能的。程序可以打开您的目标文件进行写入而无需截断它并开始写入它从标准输入读取的内容。读取EOF时,可以截断输出文件。

由于您是从输入中过滤行,因此输出文件的写入位置应始终小于读取位置。这意味着您不应使用新输出破坏您的输入。

但是,找到执行此操作的程序是个问题。dd(1)具有conv=notrunc在打开时不截断输出文件的选项,但它也不会在最后截断,在 grep 内容之后保留原始文件内容(使用类似命令grep pattern bigfile | dd of=bigfile conv=notrunc

由于从系统调用的角度来看它非常简单,因此我编写了一个小程序并在一个小型 (1MiB) 全环回文件系统上对其进行了测试。它做了你想要的,但你真的想先用其他一些文件来测试它。覆盖文件总是有风险的。

覆盖.c

/* 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);
}
Run Code Online (Sandbox Code Playgroud)

您可以将其用作:

grep pattern bigfile | overwrite bigfile
Run Code Online (Sandbox Code Playgroud)

在您尝试之前,我主要发布此内容供其他人评论。也许其他人知道一个程序可以执行类似的操作,并且经过更多测试。

  • 好的。这可能是 Joey Hess 的 [moreutils](http://kitenet.net/~joey/code/moreutils/) 的宝贵补充。你[可以使用`dd`](http://unix.stackexchange.com/questions/11067/is-there-a-way-to-filter-a-file-in-place/11114#11114),但它是麻烦。 (6认同)
  • @Arcege:写作不是分块完成的。如果你的读进程读了 2 个字节,你的写进程写了 1 个字节,那么只有第一个字节会改变,读进程可以在第 3 字节继续读取,此时原始内容不变。由于 grep 输出的数据不会多于读取的数据,因此写入位置应始终位于读取位置之后。即使您以与阅读相同的速度写作,它仍然可以。用这个代替 grep 试试 rot13,然后再试一次。md5sum 之前和之后,你会看到它是一样的。 (4认同)
  • +1 为 C;似乎确实有效,但我看到了一个潜在的问题:当右侧正在写入同一个文件时,正在从左侧读取文件,除非您协调这两个进程,否则您可能会在同一进程中出现覆盖问题块。使用较小的块大小对文件完整性可能更好,因为大多数核心工具可能会使用 8192。这可能会降低程序的速度以避免冲突(但不能保证)。也许将较大的部分读入内存(不是全部)并写入较小的块。还可以添加 nanosleep(2)/usleep(3)。 (2认同)

Sté*_*las 23

使用任何类似 Bourne 的 shell:

{
  cat < bigfile | grep -v to-exclude
  perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile
Run Code Online (Sandbox Code Playgroud)

出于某种原因,人们似乎倾向于忘记 40 岁¹ 和标准的读写重定向操作符。

我们bigfile以读+写模式打开,并且(这里最重要的是)在stdoutwhile上没有截断,而bigfilecat's上打开(单独)stdin。后grep已经终止,如果它已经删除了一些行,stdout现在指向内的某个地方bigfile,我们需要摆脱的东西超出了这一点。因此,在当前位置(由 返回)perl截断文件 ( truncate STDOUT)的命令tell STDOUT

(这cat是针对 GNU 的grep,否则会在 stdin 和 stdout 指向同一个文件时抱怨)。


¹ 好吧,虽然<>从 70 年代后期开始就在 Bourne shell 中,但它最初没有记录并且没有正确实施。它不是ash1989 年的原始实现,虽然它是一个 POSIXsh重定向操作符(从 90 年代初开始,因为 POSIXsh一直基于ksh88它),它sh直到 2000 年才被添加到 FreeBSD ,因此可移植15 年old可能更准确。另请注意,未指定时的默认文件描述符在所有 shell 中都是 0,除了ksh93在 2010 年的 ksh93t+ 中它从 0 更改为 1(破坏向后兼容性和 POSIX 合规性)

  • 你能解释一下`perl -e 'truncate STDOUT, tell STDOUT'`吗?它对我有用,但不包括在内。有什么方法可以在不使用 Perl 的情况下实现相同的目标? (2认同)
  • @nealmcb,见编辑。 (2认同)
  • @akhan,[其他方法](//serverfault.com/a/557566) 用新文件替换该文件,它不会就地覆盖同一文件。特别是,原始文件的权限所有权和其他元数据将丢失。 (2认同)

dog*_*ane 20

您可以使用sed就地编辑文件(但这确实会创建一个中间临时文件):

删除所有包含 的行foo

sed -i '/foo/d' myfile
Run Code Online (Sandbox Code Playgroud)

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

sed -i '/foo/!d' myfile
Run Code Online (Sandbox Code Playgroud)

  • 这不是 OP 所要求的,因为它创建了第二个文件。 (21认同)
  • 是的,所以这可能不好。 (3认同)

Gil*_*il' 20

我假设您的过滤器命令就是我所说的前缀收缩过滤器,它具有在读取至少 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
Run Code Online (Sandbox Code Playgroud)

您可以编写粗略等效的 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
Run Code Online (Sandbox Code Playgroud)


Jam*_*den 10

尽管这是一个老问题,但在我看来,这是一个长期存在的问题,并且比迄今为止所建议的更通用、更清晰的解决方案可用。信用到期的信用:如果不考虑 Stéphane Chazelas 提到的<>更新运算符,我不确定我是否会提出它。

在 Bourne shell 中打开文件进行更新的效用有限。shell 使您无法搜索文件,也无法设置其新长度(如果比旧长度短)。但这很容易补救,所以很容易让我感到惊讶它不在/usr/bin.

这有效:

$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T; 
     1  foo
Run Code Online (Sandbox Code Playgroud)

就像这样(给 Stéphane 的帽子提示):

$ { grep foo T && ftruncate; } 1<>T  && nl T; 
     1  foo
Run Code Online (Sandbox Code Playgroud)

(我正在使用 GNU grep。自从他写下他的答案以来,也许有些事情发生了变化。)

除了,你没有/usr/bin/ftruncate。对于几十行 C,你可以,见下文。这个ftruncate实用程序将任意文件描述符截断为任意长度,默认为标准输出和当前位置。

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

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

然后子shell退出,关闭描述符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;
}
Run Code Online (Sandbox Code Playgroud)

注意, ftruncate(2) 以这种方式使用时是不可移植的。为绝对通用,读取最后写入的字节,重新打开文件 O_WRONLY,查找、写入字节并关闭。

鉴于这个问题已经有 5 年历史了,我想说这个解决方案并不明显。它利用exec打开一个新的描述符,以及<>操作符,这两者都是神秘的。我想不出一个通过文件描述符操作 inode 的标准实用程序。(语法可能是ftruncate >&4,但我不确定是否有改进。)它比 camh 称职的探索性答案要短得多。除非你比我更喜欢 Perl,否则它比 Stéphane 的更清晰一点,IMO。我希望有人觉得它有用。

做同样事情的另一种方法是报告当前偏移量的 lseek(2) 的可执行版本;输出可用于某些 Linuxi 提供的/usr/bin/truncate


gle*_*man 5

ed 就地编辑文件可能是正确的选择:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS
Run Code Online (Sandbox Code Playgroud)

  • 我在想这意味着 *full* 文件将被加载到缓冲区中......但也许只有它需要的部分被加载到缓冲区中......我一直对 ed 感到好奇......我认为它*可以*进行原位编辑......我只需要尝试一个*大*文件......如果它有效,它是一个合理的解决方案,但在我写的时候,我开始认为这可能是*sed*的灵感(从处理大数据块中解放出来......我注意到'ed'实际上可以接受来自脚本的流输入(以`!`为前缀),所以它可能有一些更有趣的袖手旁观。 (2认同)

Pet*_*r.O 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
Run Code Online (Sandbox Code Playgroud)

这是测试输出

# 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
Run Code Online (Sandbox Code Playgroud)


Tod*_*wen 5

为了让任何人在谷歌上搜索“如何就地修改文件?”这个问题,通常情况下的正确答案是停止寻找晦涩的 shell 功能,这些功能可能会损坏您的文件,而性能增益可以忽略不计,而应使用一些变体这种模式的:

grep "foo" file > file.new && mv file.new file
Run Code Online (Sandbox Code Playgroud)

只有在极其罕见的情况下,由于某种原因这是不可行的,您才应该认真考虑本页上的任何其他答案(尽管它们确实读起来很有趣)。我承认OP没有磁盘空间来创建第二个文件的难题正是这种情况。尽管如此,仍有其他可用选项,例如@Ed Randall 和@Basile Starynkevitch 提供的选项。

  • 我可能会误解,但与OP最初提出的问题无关。又名大文件内联编辑,没有足够的磁盘空间用于临时文件。 (2认同)