web-dev-qa-db-ja.com

ディレクトリ内の1,000万を超えるファイルでsedを実行するにはどうすればよいですか?

10144911ファイルを含むディレクトリがあります。これまで私は以下を試しました:

  • for f in ls; do sed -i -e 's/blah/blee/g' $f; done

シェルがクラッシュしました。lsはチルダに入っていますが、作成方法がわかりません。

  • ls | xargs -0 sed -i -e 's/blah/blee/g'

sedの引数が多すぎます

  • find . -name "*.txt" -exec sed -i -e 's/blah/blee/g' {} \;

これ以上のメモリをフォークできませんでした

この種類のコマンドを作成する方法に関する他のアイデアはありますか?ファイルは相互に通信する必要はありません。 ls | wc -lは機能しているようです(非常に遅い)ので、可能である必要があります。

16
Sandro

これを試してみてください:

find -name '*.txt' -print0 | xargs -0 -I {} -P 0 sed -i -e 's/blah/blee/g' {}

sedの呼び出しごとに1つのファイル名のみをフィードします。これにより、「sedの引数が多すぎる」問題が解決されます。 -Pオプションでは、複数のプロセスを同時にフォークできるようにする必要があります。 0が機能しない場合(可能な限り多く実行されるはずです)、他の数(10?100?お持ちのコアの数?)を試して数を制限してください。

私はこのメソッド(および他のすべてのメソッド)を10million(空の)ファイル。名前は「hello00000001」から「hello10000000」(名前ごとに14バイト)。

UPDATE:quad-core実行を含めました'find |xargs'メソッド(まだ「sed」がなく、エコー>/dev/nullのみ)。

# Step 1. Build an array for 10 million files
#   * RAM usage approx:  1.5 GiB 
#   * Elapsed Time:  2 min 29 sec 
  names=( hello\ * )

# Step 2. Process the array.
#   * Elapsed Time:  7 min 43 sec
  for (( ix=0, cnt=${#names[@]} ; ix<$cnt; ix++ )) ; do echo "${names[ix]}" >/dev/null ; done  

これは、上記のテストデータに対して実行したときに提供された回答がどのように機能したかをまとめたものです。これらの結果には、基本的なオーバーヘッドのみが含まれます。つまり、「sed」は呼び出されませんでした。 sedプロセスはほぼ間違いなく最も時間がかかりますが、裸の方法を比較する方法を見るのは興味深いと思いました。

デニスの'find |xargs'メソッドは、シングルコアを使用し、bash array実行時のno sedメソッドよりも* 4時間21分**長くかかりました...ただし、マルチコアの利点は'find'は、ファイルを処理するためにsedが呼び出されているときに表示される時間差を上回る必要があります。

           | Time    | RAM GiB | Per loop action(s). / The command line. / Notes
-----------+---------+---------+----------------------------------------------------- 
Dennis     | 271 min | 1.7 GiB | * echo FILENAME >/dev/null
Williamson   cores: 1x2.66 MHz | $ time find -name 'hello *' -print0 | xargs -0 -I {} echo >/dev/null {}
                               | Note: I'm very surprised at how long this took to run the 10 million file gauntlet
                               |       It started processing almost immediately (because of xargs I suppose),  
                               |       but it runs **significantly slower** than the only other working answer  
                               |       (again, probably because of xargs) , but if the multi-core feature works  
                               |       and I would think that it does, then it could make up the defecit in a 'sed' run.   
           |  76 min | 1.7 GiB | * echo FILENAME >/dev/null
             cores: 4x2.66 MHz | $ time find -name 'hello *' -print0 | xargs -0 -I {} -P 0 echo >/dev/null {}
                               |  
-----------+---------+---------+----------------------------------------------------- 
fred.bear  | 10m 12s | 1.5 GiB | * echo FILENAME >/dev/null
                               | $ time names=( hello\ * ) ; time for (( ix=0, cnt=${#names[@]} ; ix<$cnt; ix++ )) ; do echo "${names[ix]}" >/dev/null ; done
-----------+---------+---------+----------------------------------------------------- 
l0b0       | ?@#!!#  | 1.7 GiB | * echo FILENAME >/dev/null 
                               | $ time  while IFS= read -rd $'\0' path ; do echo "$path" >/dev/null ; done < <( find "$HOME/junkd" -type f -print0 )
                               | Note: It started processing filenames after 7 minutes.. at this point it  
                               |       started lots of disk thrashing.  'find' was using a lot of memory, 
                               |       but in its basic form, there was no obvious advantage... 
                               |       I pulled the plug after 20 minutes.. (my poor disk drive :(
-----------+---------+---------+----------------------------------------------------- 
intuited   | ?@#!!#  |         | * print line (to see when it actually starts processing, but it never got there!)
                               | $ ls -f hello * | xargs python -c '
                               |   import fileinput
                               |   for line in fileinput.input(inplace=True):
                               |       print line ' 
                               | Note: It failed at 11 min and approx 0.9 Gib
                               |       ERROR message: bash: /bin/ls: Argument list too long  
-----------+---------+---------+----------------------------------------------------- 
Reuben L.  | ?@#!!#  |         | * One var assignment per file
                               | $ ls | while read file; do x="$file" ; done 
                               | Note: It bombed out after 6min 44sec and approx 0.8 GiB
                               |       ERROR message: ls: memory exhausted
-----------+---------+---------+----------------------------------------------------- 
7
Peter.O

完全に安全な検索 のもう1つの機会:

while IFS= read -rd $'\0' path
do
    file_path="$(readlink -fn -- "$path"; echo x)"
    file_path="${file_path%x}"
    sed -i -e 's/blah/blee/g' -- "$file_path"
done < <( find "$absolute_dir_path" -type f -print0 )
2
l0b0

これはほとんどトピックから外れていますが、使用できます

find -maxdepth 1 -type f -name '*.txt' | xargs python -c '
import fileinput
for line in fileinput.input(inplace=True):
    print line.replace("blah", "blee"),
'

ここでの主な利点(... xargs ... -I {} ... sed ...以上)は速度です。sedを1,000万回呼び出す必要がありません。 Python(pythonは比較的遅いので))の使用を避けることができれば、さらに高速になるため、このタスクにはPerlの方が適している可能性があります。 。Perlで同等のことを便利に行う方法がわかりません。

これが機能する方法は、xargsが単一のコマンドラインに収まる限り多くの引数を使用してPythonを呼び出し、引数(ls -f *.txtによって提供されている)がなくなるまでそれを継続することです。 )各呼び出しの引数の数は、ファイル名の長さやその他の要素によって異なります。fileinput.input関数は、各呼び出しの引数で指定されたファイルから連続する行を生成し、inplaceオプションは魔法のように「キャッチ」するように指示します。 "出力し、それを使用して各行を置き換えます。

Pythonの文字列replaceメソッドは正規表現を使用しないことに注意してください。それらが必要な場合は、import reして、print re.sub(line, "blah", "blee")を使用する必要があります。それらはPerl互換の正規表現であり、sed -rで取得するものの一種の非常に強化されたバージョンです。

編集

アキラがコメントで述べているように、グロブはシェル(find)自体によって処理されるため、bashコマンドの代わりにグロブ(ls -f *.txt)を使用した元のバージョンは機能しません。これは、コマンドが実行される前に、1,000万のファイル名がコマンドラインに置き換えられることを意味します。これは、コマンドの引数リストの最大サイズを超えることがほぼ保証されています。これに関するシステム固有の情報には、xargs --show-limitsを使用できます。

引数リストの最大サイズもxargsによって考慮されます。これにより、pythonの各呼び出しに渡される引数の数がその制限に従って制限されます。xargsは引き続き=を呼び出す必要があるためpythonかなりの回数、ファイルリストを取得するためにos.path.walkを使用するというakiraの提案は、おそらく時間を節約するでしょう。

1
intuited

試してください:

ls | while read file; do (something to $file); done
0
Reuben L.