かなり大きなファイル(35Gb)があり、このファイルをその場でフィルター処理したい(つまり、別のファイル用に十分なディスク容量がない)、特にgrepを行い、いくつかのパターンを無視したい-方法はありますか別のファイルを使用せずにこれを行いますか?
foo:
たとえば、以下を含むすべての行を除外したいとしましょう...
かなり大きなファイル(35Gb)があり、このファイルをその場でフィルター処理したい(つまり、別のファイル用に十分なディスク容量がない)、特にgrepを行い、いくつかのパターンを無視したい-方法はありますか別のファイルを使用せずにこれを行いますか?
foo:
たとえば、以下を含むすべての行を除外したいとしましょう...
回答:
システムコールレベルでこれが可能になります。プログラムは、ターゲットファイルを切り捨てずに書き込み用に開き、stdinから読み取ったものの書き込みを開始できます。EOFを読み取るとき、出力ファイルは切り捨てられる場合があります。
入力から行をフィルタリングしているため、出力ファイルの書き込み位置は常に読み取り位置より小さくする必要があります。これは、新しい出力で入力を破損しないことを意味します。
ただし、これを行うプログラムを見つけることが問題です。開くときに出力ファイルを切り捨てないdd(1)
オプションがありますconv=notrunc
が、grepの内容の後に元のファイルの内容を残して、最後に切り捨てもしません(などのコマンドを使用grep pattern bigfile | dd of=bigfile conv=notrunc
)
システムコールの観点からは非常に単純なので、小さなプログラムを作成し、小さな(1MiB)フルループバックファイルシステムでテストしました。それはあなたが望むことをしましたが、あなたは本当に最初にいくつかの他のファイルでこれをテストしたいです。ファイルを上書きすることは常に危険を伴います。
overwrite.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);
}
次のように使用します。
grep pattern bigfile | overwrite bigfile
私は主に、あなたがそれを試す前に他の人がコメントするためにこれを投稿しています。おそらく他の誰かが、よりテストされた同様のことをするプログラムを知っているでしょう。
grep
読み取るよりも多くのデータを出力しないため、書き込み位置は常に読み取り位置の後ろにある必要があります。読書と同じ速さで書いていても、大丈夫です。grepの代わりにrot13を試してから、もう一度試してください。md5sumの前後で同じことがわかります。
dd
が、それは面倒です。
sed
ファイルをその場で編集するために使用できます(ただし、これは中間の一時ファイルを作成します):
を含むすべての行を削除するにはfoo
:
sed -i '/foo/d' myfile
を含むすべての行を保持するにはfoo
:
sed -i '/foo/!d' myfile
$HOME
ます書き込み可能ではなく、/tmp
される読み取り専用(デフォルト)。たとえば、Ubuntuがあり、回復コンソールを起動した場合、これは一般的なケースです。また、ヒアドキュメント演算子<<<
も一時ファイルを書き込むためr / wである必要がある/tmp
ため、そこでも動作しません。(「d出力を含むこの質問」を参照)strace
あなたのフィルターコマンドは、少なくともNバイトの入力を読み取る前に出力のバイトNが決して書き込まれないという特性を持つ、私がプレフィックス縮小フィルターと呼ぶものであると仮定します。grep
このプロパティがあります(フィルタリングのためだけであり、一致する行番号を追加するような他のことをしない限り)。このようなフィルターを使用すると、入力に合わせて上書きできます。もちろん、ファイルの先頭の上書きされた部分は永久に失われるため、間違いを犯さないようにする必要があります。
ほとんどのUNIXツールは、ファイルに追加するか切り捨てるかを選択するだけで、上書きする可能性はありません。標準ツールボックスの例外の1つは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
Bourneのようなシェルの場合:
{
cat < bigfile | grep -v to-exclude
perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile
何らかの理由で、人々はその40歳の¹と標準の読み取り+書き込みリダイレクト演算子を忘れがちであるように思われます。
私たちは、オープンbigfile
に切り捨てなし(ほとんどここで重要なもの)、読み取り+書き込みモードにしてstdout
いる間bigfile
に(別途)が開いているcat
のstdin
。終了後grep
、いくつかの行を削除した場合、のいずれstdout
かの場所を指すbigfile
ようになり、このポイントを超えたものを取り除く必要があります。したがって、現在の位置でperl
ファイル(truncate STDOUT
)を切り捨てるコマンド(によって返されるtell STDOUT
)。
(これcat
は、grep
標準入力と標準出力が同じファイルを指している場合に文句を言うGNU用です)。
¹まあ、<>
70年代後半に最初からBourneシェルに入っていましたが、最初は文書化されておらず、適切に実装されていませんでした。ash
1989年の元の実装ではなく、POSIX sh
リダイレクション演算子(POSIX sh
がksh88
常に持っていた90年代前半であるため)は、sh
たとえば2000年までFreeBSD に追加されなかったため、移植性が15年でした。 oldはおそらくより正確です。また、指定されていない場合のデフォルトのファイル記述子は<>
すべてのシェルにありますがksh93
、2010年にksh93t +で0から1に変更されたことを除きます(後方互換性とPOSIX準拠に違反します)
perl -e 'truncate STDOUT, tell STDOUT'
か?それを含めずに私のために動作します。Perlを使用せずに同じことを達成する方法はありますか?
redirection "<>" fixed and documented (used in /etc/inittab f.i.).
これは1つのヒントです。
これは古い質問ですが、私には永遠の質問のようです。これまで提案されてきたよりも一般的で明確な解決策が利用可能です。クレジットが支払われるべきクレジット:StéphaneChazelasの<>
更新オペレーターについての言及を考慮せずにそれを思いつくかどうかはわかりません。
Bourneシェルで更新のためにファイルを開くことは、限られたユーティリティです。シェルでは、ファイルを検索する方法も、新しい長さを設定する方法もありません(古いものより短い場合)。しかし、それは簡単に修正できるので、それがの標準ユーティリティに含まれていないことに簡単に驚きます/usr/bin
。
これは動作します:
$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T;
1 foo
これも同様です(Stéphaneへのヒント):
$ { grep foo T && ftruncate; } 1<>T && nl T;
1 foo
(私はGNU grepを使用しています。おそらく彼が答えを書いてから何かが変わったのでしょう。)
ただし、/ usr / bin / ftruncateはありません。Cの数十行については、以下を参照してください。このftruncateユーティリティは、任意のファイル記述子を任意の長さに切り捨てます。デフォルトは標準出力と現在の位置です。
上記のコマンド(最初の例)
T
更新のためにファイル記述子4を開きます。open(2)と同様に、この方法でファイルを開くと、現在のオフセットが0に配置されます。 T
正常に処理され、シェルはT
記述子4 を介して出力をリダイレクトします。次に、サブシェルが終了し、記述子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;
}
NB、この方法で使用すると、ftruncate(2)は移植できません。絶対的な一般性のために、最後に書き込まれたバイトを読み取り、ファイルO_WRONLYを再度開き、シークし、バイトを書き込み、閉じます。
質問が5年前であることを考えると、この解決策は自明ではないと言います。execを利用して、新しい記述子と<>
演算子を開きます。どちらも難解です。ファイル記述子によってiノードを操作する標準ユーティリティは考えられません。(構文はになる可能性がありますがftruncate >&4
、改善されるかどうかはわかりません。)camhの有能で探索的な答えよりもかなり短いです。あなたが私よりもPerlを好まない限り、それはStéphaneのIMOよりも少し明確です。誰かがそれを役に立つと思うことを願っています。
同じことを行う別の方法は、現在のオフセットを報告するlseek(2)の実行可能バージョンです。出力は、一部のLinuxiが提供する/ usr / bin / truncateに使用できます。
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.
ed
ファイルはバッファに読み込まれるため、35GBファイルを編集するためのgoolソリューションではありません。
!
)ので、その袖をさらにいくつかの興味深いトリックを持っていることがあります。
ed
ファイルが切り捨てられ、書き換えられると確信しています。そのため、OPが望むようにディスク上のデータをインプレースで変更することはありません。また、ファイルが大きすぎてメモリにロードできない場合は機能しません。
bashの読み取り/書き込みファイル記述子を使用してファイルを開き(in-situで上書きする)、その後... sed
およびtruncate
もちろん、変更がこれまでに読み取られたデータ量より大きくなることを許可しないでください。 。
スクリプトは次のとおりです(使用: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
厳密にはその場ではありませんが、これは同様の状況で役立つ可能性があります。
ディスク容量に問題がある場合は、最初にファイルを圧縮し(テキストであるため、大幅に削減されます)、解凍/圧縮パイプラインの途中で通常の方法で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
sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
この質問をグーグルする人の利益のために、正しい答えは、ごくわずかなパフォーマンス向上のためにファイルを破損する危険性のある不明瞭なシェル機能の検索を停止し、代わりにこのパターンのいくつかのバリエーションを使用することです:
grep "foo" file > file.new && mv file.new file
これが何らかの理由で実行不可能であるという非常にまれな状況でのみ、このページの他の回答を真剣に検討する必要があります(確かに読むのは面白いですが)。私は、2番目のファイルを作成するためのディスクスペースがないというOPの難題がまさにそのような状況であることを認めます。それでも、@ Ed Randallや@Basile Starynkevitchによって提供されるような他のオプションが利用可能です。