ファイルから行を削除するより高速な方法はありますか?

ファイルから行を削除するより高速な方法はありますか?

関連する質問はここ

大容量ファイルを編集してみると、途中で数行を削除する必要があることがよくあります。削除する行を知っており、通常は次のことを行います。

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

または、-iオプションを追加してインラインで実行することもできます。行番号を知っていますが、ストリームの編集を避け、特定の行だけを削除する命令がありますか? input.txtは最大50GBまで可能です。

答え1

ファイルのコピーを書き込まない場合は、次のようにファイル自体にファイルを書き込むことをお勧めします。

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

バックアップコピーがないため危険です。

または、これを防ぐために、sedmanatworkのアイデアの一部を盗んでください。

{
  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.25秒
  • 2番目のソリューション:0.057秒
  • 3番目のソリューション:0.48秒

< fileすべて同じ原則に従います。ファイルの2つのファイル記述子を開きます。 1つは読み取り専用モード(0)で略語forを使用し、もう1つは0< file読み取り/書き込みモード(1)で(will be)を使用します。このファイル記述子は2つを指します。1<> file<> file0<> fileファイル説明を開くそれぞれ電流があります。カーソル位置それらに関連するファイルから。

たとえば、2番目のソリューションでは、最初のソリューションはfd 0から行データをhead -n "$(($l1 - 1))"読み取り、そのデータをfd 1に書き込みます。$l1 - 1したがって、コマンドが終了すると、カーソルは2つのコマンドの間にあります。ファイル説明を開くfds 0と1に関連する項目は、3行目の先頭にあります$l1

次に、head -n "$(($l2 - $l1 + 1))" > /dev/null同じhead行を読みます。$l2 - $l1 + 1ファイル説明を開くまだ接続されているfd 0を介して、fd 0のカーソルはその行の次の行の先頭に移動します$l2

ただし、fd 1はにリダイレクトされているため、/dev/nullfd 1を作成した後はカーソルを次に移動しません。ファイル説明を開く{...}fd 1が指します。

したがって、起動時にcatカーソルは次の位置にあります。ファイル説明を開くfd 0が指す位置は次の行の先頭にあり$l2、fd 1のカーソルはまだ$l1行3の先頭にあります。つまり、対応する2head行目は入力では削除のためにスキップされますが、出力では削除されません。これで、最初の行は次の行で上書きされ、catこのように続行されます。$l1$l2

catfd 0のファイルの終わりに達すると返されます。ただし、fd 1はまだ上書きされていないファイルの場所を指します。このセクションは消える必要があり、ファイルの最後に移動された削除された行が占めるスペースに対応します。私たちに必要なのは、現在fd 1が指す正確な場所からファイルを切り取ることです。

これはftruncateシステムコールを介して行われます。残念ながら、これを実行できる標準のUnixユーティリティがないため、fd 1に関連する現在のカーソル位置をperl提供することに依存しています。私たちはPerlのシステムコールインターフェイスをtell STDOUT使ってこのオフセットからファイルを切り取りますftruncatetruncate

head3番目のソリューションでは、最初のコマンドのfd 1書き込みをシステムコールに置き換えますlseek

答え2

これは使いやすい方法ですsed。明らかに、ファイルをストリーミングし(長いファイルには問題ありません)、簡単に一般化してより多くの作業を実行できます。しかし、あなたが望むならシンプルファイルを編集する方法所定の位置に、最も簡単な方法は以下を使用することedですex

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

無制限のサイズ(およびRAMが許可する1行)のファイルを処理することを保証するより良いアプローチは、ファイルをperl内部で編集する次の1行のコードです。

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

説明する:

-n:各行にスクリプトを適用します。他の出力は生成しません。
-i:ファイルを所定の位置に編集します(-i.bckバックアップ目的で)。
-e ...:10〜31行を除くすべての行を印刷します。

答え3

50GiBを読み書きする必要がある場合〜する何をしても長い時間を投資してください。行の長さが固定されていない場合、または削除する行がどこにあるかを知る他の方法がない場合は、削除する最後の行までファイルを読み取ることができません。改行文字だけをカウントし、後でブロック全体をコピーするカスタムプログラムは少し速いかもしれませんが、sed(1)これはボトルネックではないと確信しています。time(1)時間がどのように割り当てられるかを理解するには、を使用してみてください。

答え4

ファイルをその場で編集したい場合、ほとんどのシェルツールは役に立ちません。これは、書き込み用にファイルを開くときに既存の内容を上書きせずに切り取り(>)または追加()のみを選択できるためです。注目すべき例外です。バラより>>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))"

(警告:テストされていません!)

関連情報