一个相关的问题是这里。
我经常需要通过删除中间的几行来编辑一个大文件。我知道我想要删除哪些行,我通常会执行以下操作:
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
<> file
0<> 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 的系统调用接口在该偏移处截断文件ftruncate
:truncate
。
head
在第三种解决方案中,我们用一个系统调用替换第一个命令对 fd 1 的写入lseek
。
答案2
使用sed
是一个很好的方法:很明显,它流式传输文件(长文件没有问题),并且可以轻松推广以执行更多操作。但如果你想要一个简单的编辑文件的方法到位,最简单的事情是使用ed
or 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))"
(警告:未经测试!)