有没有更快的方法从文件中删除行(给定行号)?

有没有更快的方法从文件中删除行(给定行号)?

一个相关的问题是这里

我经常需要通过删除中间的几行来编辑一个大文件。我知道我想要删除哪些行,我通常会执行以下操作:

sed "linenum1,linenum2 d" input.txt > input.temp

或通过添加 -i 选项进行内嵌。由于我知道行号,是否有命令可以避免流编辑并仅删除特定行? input.txt 可以大到 50 GB。

答案1

为了避免写入文件的副本,您可以做的就是将文件写入自身,如下所示:

{
  sed "$l1,$l2 d" < file
  perl -le 'truncate STDOUT, tell STDOUT'
} 1<> file

危险,因为你没有备份副本。

或者避免sed,窃取 manatwork 的部分想法:

{
  head -n "$(($l1 - 1))"
  head -n "$(($l2 - $l1 + 1))" > /dev/null
  cat
  perl -le 'truncate STDOUT, tell STDOUT'
} < file 1<> file

这仍然可以改进,因为你正在覆盖第一个l1-1虽然你不需要这样做,但避免它意味着更多地涉及编程,例如做所有perl可能最终效率较低的事情:

perl -ne 'BEGIN{($l1,$l2) = ($ENV{"l1"}, $ENV{"l2"})}
    if ($. == $l1) {$s = tell(STDIN) - length; next}
    if ($. == $l2) {seek STDOUT, $s, 0; $/ = \32768; next}
    if ($. > $l2) {print}
    END {truncate STDOUT, tell STDOUT}' < file 1<> file

从 的输出中删除第 1000000 到 1000050 行的一些时序seq 1e7

  • sed -i "$l1,$l2 d" file:16.2秒
  • 第一个解决方案:1.25s
  • 第二解:0.057s
  • 第三解:0.48s

< file它们都遵循相同的原理:我们为文件打开两个文件描述符,一个使用缩写 for处于只读模式 (0) ,另一个使用( will be )0< file处于读写模式 (1) 。这些文件描述符指向两个1<> file<> file0<> file打开文件描述每个都有一个电流光标位置在与它们关联的文件中。

例如,在第二个解决方案中,第一个解决方案将从 fd 0head -n "$(($l1 - 1))"读取$l1 - 1行数据并将该数据写入 fd 1。因此,在该命令结束时,光标在两个打开文件描述与 fds 0 和 1 关联的将位于第 3 行的开头$l1

然后, in head -n "$(($l2 - $l1 + 1))" > /dev/null,将从相同的行中head读取行$l2 - $l1 + 1打开文件描述通过仍然与其关联的 fd 0,因此 fd 0 上的光标将移动到该行之后的行首$l2

但它的 fd 1 已经被重定向到/dev/null,所以写入 fd 1 后,它不会移动光标在打开文件描述{...}由的 fd 1指向。

因此,启动后cat,光标位于打开文件描述fd 0 指向的位置将位于下一行的开头$l2,而 fd 1 上的光标仍将位于$l1第 3 行的开头。或者说,那一秒head将跳过这些行以在输入上删除,但不会在输出上删除。现在cat$l1用后面的下一行覆盖第 th 行$l2,依此类推。

cat当到达 fd 0 上的文件末尾时将返回。但是 fd 1 将指向文件中尚未被覆盖的某个位置。该部分必须消失,它对应于现在移至文件末尾的已删除行所占用的空间。我们需要的是在 fd 1 现在指向的确切位置截断文件。

这是通过ftruncate系统调用完成的。不幸的是,没有标准的 Unix 实用程序可以做到这一点,因此我们求助于perl.tell STDOUT给我们与 fd 1 关联的当前光标位置。我们使用 perl 的系统调用接口在该偏移处截断文件ftruncatetruncate

head在第三种解决方案中,我们用一个系统调用替换第一个命令对 fd 1 的写入lseek

答案2

使用sed是一个很好的方法:很明显,它流式传输文件(长文件没有问题),并且可以轻松推广以执行更多操作。但如果你想要一个简单的编辑文件的方法到位,最简单的事情是使用edor ex

(echo 10,31d; echo wq) | ed input.txt

一种更好的方法,保证可以处理无限大小的文件(并且只要 RAM 允许,就可以处理行)是以下perl单行代码,它可以就地编辑文件:

perl -n -i -e 'print if $. < 10 || $. > 31' input.txt

解释:

-n:将脚本应用到每一行。不产生其他输出。
-i:就地编辑文件(用于-i.bck进行备份)。
-e ...:打印除第 10 至 31 行之外的每一行。

答案3

如果你需要读写50GiB,那么将要花很长时间,无论你做什么。除非行的长度是固定的,或者您有其他方法可以知道要删除的行在哪里,否则无法读取文件直到要删除的最后一行。也许一个只计算换行符并稍后复制完整块的自定义程序比 快一点sed(1),但我相信这不是您的瓶颈。尝试使用time(1)来了解时间是如何分配的。

答案4

如果您想就地编辑文件,大多数 shell 工具都帮不了您,因为当您打开文件进行写入时,您只能选择截断它 ( >) 或追加它 ( >>),而不能覆盖现有内容。dd是一个值得注意的例外。看有没有办法就地修改文件?

export LC_ALL=C
lines_to_keep=$((linenum1 - 1))
lines_to_skip=$((linenum2 - linenum1 + 1))
deleted_bytes=$({ { head -n "$lines_to_keep"
                    head -n "$lines_to_skip" >&3;
                    cat
                  } <big_file | dd of=big_file conv=notrunc;
                } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek="$(($(wc -c <big_file) - $deleted_bytes))"

(警告:未经测试!)

相关内容