複数ファイルのデータを単一のCSVファイルに効率的に抽出

複数ファイルのデータを単一のCSVファイルに効率的に抽出

同じ構造を持つXMLファイルがたくさんあります。

$ cat file_<ID>.xml
... 
 ... 
   ...
      <double>1.2342</double>
      <double>2.3456</double>
      ...
   ...
 ... 
... 

<double>ここで、各 XML ファイルの対応するエントリの数は固定されていることがわかります (私の特別な場合は 168)。

csv次のように、すべてのXMLファイルの内容を格納するファイルを作成する必要があります。

file_0001 1.2342 2.3456 ... 
file_0002 1.2342 2.3456 ... 

など。

これをどのように効率的に実行できますか?


私が思いついた最高は次のとおりです。

#!/usr/bin/env zsh

for x in $path_to_xmls/*.xml; do 

    # 1) Get the doubles ignoring everything else
    # 2) Remove line breaks within the same file
    # 3) Add a new line at the end to construct the CSV file
    # 4) Join the columns together

    cat $x | grep -F '<double>' | \ 
    sed -r 's/.*>([0-9]+\.*[0-9]*).*?/\1/' | \
    tr '\n' ' ' | sed -e '$a\'  |  >> table_numbers.csv

    echo ${x:t} >> file_IDs.csv
done
    
paste file_IDs table_numbers.csv > final_table.csv

〜10K XMLファイルを含むフォルダに上記のスクリプトを配置すると、次の結果が表示されます。

./from_xml_to_csv.sh  100.45s user 94.84s system 239% cpu 1:21.48 total

悪くはありませんが、100倍または1000倍以上のファイルを処理できることを願っています。どうすればこの処理をより効率的にすることができますか?

また、上記のソリューションを使用すると、数百万のファイルを処理するなど、グローバル拡張が限界に達する状況に直面するでしょうか? (典型的な"too many args"質問)。

修正する

この質問に興味がある人は@mikeserveの答えを読んでください。これまでに最も高速でスケーラビリティに優れた製品です。

答え1

これにより、トリックを実行できます。

awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' $path_to_xml/*.xml > final_table.csv

説明する:

  • awk:このプログラムを使用してawkGNU awk 4.0.1でテストしました。
  • -F '[<>]'<: と>フィールド区切り文字として使用
  • NR!=1 && FNR==1{printf "\n"}:全体の最初の行()ではなく、NR!=1ファイルの最初の行(FNR==1)の場合は改行文字を出力します。
  • FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME}:ファイルの最初の行の場合は、/ファイル名()から最後()の前をすべて削除し、後の()を削除して結果を印刷()します。sub(".*/", "", FILENAME)FILENAME.xmlsub(".xml$", "", FILENAME)printf FILENAME
  • /double/{printf " %s", $3}行に「double」(/double/)が含まれている場合は、スペースが印刷され、その後に3番目のフィールド(printf " %s", $3)が表示されます。数字になる区切り文字としてと<を使用します(最初のフィールドは最初のフィールドの前にあるもので、2番目のフィールドはです)。必要に応じて、ここで数値書式を指定できます。たとえば、任意の数字の代わりに使用すると、小数点以下の3桁が出力され、全長(スコアと小数点以下の桁数を含む)は少なくとも8桁になります。><double%8.3f%s
  • END{printf "\n"}: 最後の行の後に追加の改行を印刷します (オプションである可能性があります)。
  • $path_to_xml/*.xml: ファイルリスト
  • > final_table.csvfinal_table.csv:結果を入れてください。

「引数リストが長くなる」エラーが発生した場合は、直接渡すのではなく、findwith引数を使用してファイルリストを生成できます。-exec

find $path_to_xml -maxdepth 1 -type f -name '*.xml' -exec awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' {} + > final_table.csv

説明する:

  • find $path_to_xmlfindファイルを一覧表示するように指示します。$path_to_xml
  • -maxdepth 1: サブフォルダーを入力しないでください$path_to_xml
  • -type f:一般ファイルのみを一覧表示します。 (これも$path_to_xml自分を除きます。)
  • -name '*.xml': only list files that match the pattern*.xml`、引用する必要があります。それ以外の場合、シェルは拡張モードを試みます。
  • -exec COMMAND {} +COMMAND代わりに一致するファイルをパラメータとして使用します{}+複数のファイルを一度に転送できるため、フォークが少なくなります。各ファイルに対して個別にコマンドを実行する代わりに使用される場合\;;引用符が必要、それ以外の場合はシェルで解釈されます)。+

xargs以下と組み合わせて使用​​することもできますfind

find $path_to_xml -maxdepth 1 -type f -name '*.xml' -print0 |
 xargs -0 awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' > final_table.csv

説明する

  • -print0:ヌル文字で区切られたファイルのリストを出力します。
  • |(パイプ):標準出力をfind標準入力にリダイレクトします。xargs
  • xargs:標準入力からコマンドをビルドして実行します。つまり、渡された各引数(この場合はファイル名)に対してコマンドを実行します。
  • -0:xargs引数がヌル文字で区切られていると仮定します。

awk -F '[<>]' '      
      BEGINFILE {sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      ENDFILE {printf "\n"}
    ' $path_to_xml/*.xml > final_table.csv

whichBEGINFILEENDFILEファイルが変更されたときに呼び出されます(awkがサポートしている場合)。

答え2

可能なグローバル拡張制限を超えました - はい、いいえ。シェルはすでに実行中なので停止しません。ただし、グローバルな配列全体を単一のコマンドの引数として渡したい場合は可能です。この問題を解決するための移植可能で強力な方法は次のとおりですfind

find . \! -name . -prune -name pattern -type f -exec cat {} + | ...

cat... 現在のディレクトリで名前が一致する通常のファイルだけを探します。patterncatただし、これを超えないように必要なだけ呼び出されますARG_MAX

しかし、実際にGNUがあるので、sed私たちはできます。ほぼsedただ1つのスクリプトですべてを実行してくださいfind

cd /path/to/xmls
find . \! -name . -prune -name \*.xml -type f -exec  \
    sed -sne'1F;$x;/\n*\( \)*<\/*double>/!d' \
        -e  '$s//\1/gp;H' {} + | paste -d\\0 - -

私は別の方法を考えた。これは〜になります非常に.高速ですが、ファイルごとに正確に168の一致があり、ファイル名に1つのドットしかありません。

(   export LC_ALL=C; set '' - -
    while [ "$#" -lt 168 ]; do set "$@$@"; done
    shift "$((${#}-168))"
    find . \! -name . -prune -name \*.xml -type f      \
              -exec  grep -F '<double>' /dev/null {} + |
    tr \<: '>>' | cut -d\> -f1,4 | paste -d\  "$@"     |
    sed 'h;s|./[^>]*>||g;x;s|\.x.*||;s|..||;G;s|\n| |'
)

要求に応じてコマンドが動作する方法の詳細な説明は次のとおりです。

  1. ( ... )

    • まず、小さなスクリプト全体が独自のサブシェルで実行されます。なぜなら、実行中に一部のグローバル環境属性を変更して、操作が完了すると、変更したすべての属性が元の値に復元されるからです。
  2. export LC_ALL=C; set '' - -
    • 現在のロケールをに設定すると、Cフィルタ操作を大幅に減らすことができます。 UTF-8ロケールでは、すべての文字を1つ以上のバイトで表すことができ、見つかったすべての文字は何千もの可能な文字グループから選択する必要があります。 C言語環境では、各文字はバイトで、その数は128文字のみです。全体的に、文字マッチングが速くなります。
    • このsetステートメントはシェルの位置パラメーターを変更します。実行set '' - -設定は、および$1です。\0$2$3-
  3. while ... set "$@$@"; done; shift ...
    • デフォルトでは、この問い合わせのポイントは168個のダッシュ配列を取得することです。後でpaste168番目の改行を維持しながら、連続した167の改行セットを空白に置き換えます。最も簡単な方法はstdinに168のパラメータ参照を提供し-、それらをすべて一緒に貼り付けるように指示することです。
  4. find ... -exec grep -F '<double>' /dev/null' ...
    • このfind部分は、以前に議論されていますが、固定文字列と一致する可能grep性がある行のみを印刷します。最初のパラメータを作成すると、次のパラメータを取得できます。-F<double>grep/dev/nullいいえ文字列一致 -grep各呼び出しが常に2つ以上のファイルパラメータを取得していることを確認します。 2つ以上の名前付き検索ファイルを呼び出すと、ファイル名は常に各出力行の先頭にgrep印刷されます。file_000.xml:
  5. tr \<: '>>'
    • ここでは、の出力でまたは文字grepの各発生を変換します。:<>
    • この時点でのサンプルマッチラインは次のとおりです./file_000.xml> >double>0.0000>/double>
  6. cut -d\> -f1,4
    • cut最初または4番目の文字ごとのフィールドにない入力は、その出力から削除されます>
    • この時点でのサンプルマッチラインは次のとおりです./file_000.xml>0.0000
  7. paste -d\ "$@"
    • すでに議論していますが、ここではpasteバッチ入力行として168を使用します。
    • このとき、以下のように168本の一致する行が同時に表示されます。./file_000.xml>0.000 .../file_000.xml>0.167
  8. sed 'h;s|./[^>]*>||g;x;s|\.xml.*||;s|..||;G;s|\n| |'
    • 今、より速くて小さなユーティリティがほとんどの作業を行いました。マルチコアシステムでは同時に実行することもできます。そしてそのユーティリティ -特に cutそしてpaste。しかし、私はこれを行うことができると想像できるだけにし、上訴しなければなりません。sedawksed
    • まずh、すべての入力行のコピーを作成し、パターンスペースからgすべてのパターン発生をグローバルに削除しました./[^>]*>。つまり、すべてのファイル名の発生を削除しました。この時のパターン空間はsed次のようになります。0.000 0.0001...0.167
    • 次に、古いスペースとパターンスペースをx変更してすべてを削除します。したがって、保存された行コピーの最初のファイル名から始まるすべてのコンテンツが削除されます。その後、最初の2文字を削除または削除すると、パターンスペースは次のようになります。h\.xml.*./file_000
    • 今残っているのは、それらを互いに貼り付けるだけです。 ewline文字の後のパターンスペースに前のスペースのGコピーを追加し、ewlineをスペースに置き換えます。h\ns///\n
    • だから最終的にパターン空間はfile_000 0.000...0.167。これはsed、各ファイル書き込み出力findがに渡されることですgrep

答え3

将来のメンテナンスプログラマとシステム管理者の代わりにXMLを解析するために正規表現を使用しないでください。 XMLは、正規表現の解析に適していない構造化データ型です。プレーンテキストのように偽装して「偽」にすることができますが、XMLには異なる方法で解析される意味で同じ項目がたくさんあります。たとえば、改行を含み、単項タグを持つことができます。

だから - パーサーを使って - XMLが無効であるため、いくつかのソースデータを模擬しました。もう少し完全なサンプルをお寄せください。

基本レベルでは、double次のようにノードを抽出します。

#!/usr/bin/env perl

use strict;
use warnings;
use XML::Twig;

my $twig = XML::Twig -> new;
$twig -> parse ( \*DATA ); 

foreach my $double ( $twig -> get_xpath('//double') ) {
   print $double -> trimmed_text,"\n";
}

__DATA__
<root> 
 <subnode> 
   <another_node>
      <double>1.2342</double>
      <double>2.3456</double>
      <some_other_tag>fish</some_other_tag>
   </another_node>
 </subnode>
</root> 

これは以下を印刷します:

1.2342
2.3456

それでは拡張してみましょう。

#!/usr/bin/env perl

use strict;
use warnings;
use XML::Twig;
use Text::CSV;

my $twig = XML::Twig->new;
my $csv  = Text::CSV->new;

#open our results file
open( my $output, ">", "results.csv" ) or die $!;
#iterate each XML File. 
foreach my $filename ( glob("/path/to/xml/*.xml") ) {
    #parse it
    $twig->parsefile($filename);
    #extract all the text of all the 'double' elements. 
    my @doubles = map { $_->trimmed_text } $twig->get_xpath('//double');
    #print it as comma separated. 
    $csv->print( $output, [ $filename, @doubles ] );

}
close($output);

私はこれがトリックを実行する必要があると思います(サンプルデータがなければ確かに言うことはできません)。しかし注意してください。 XMLパーサーを使用すると(XML仕様に従って)、完全に効率的に実行できるXML形式の再指定は発生しません。 CSVパーサーを使用すると、コンマや改行を含むフィールドにはまったく入りません。

より具体的なノードを探している場合は、より詳細なパスを指定できます。実際、上記のコードはただルックアップのインスタンスですdouble。ただし、次のものを使用できます。

get_xpath("/root/subnode/another_node/double")

答え4

各ファイルを2回書き込みます。おそらくこれは最も高価な部分です。代わりに、エントリ全体をメモリ(おそらく配列)に保存したいと思います。それでは最後に一度書いてください。

ulimitメモリ制限に達し始めたことを確認してください。このワークロードを10〜100倍に増やすと、10〜100 GBのメモリが必要になることがあります。繰り返しごとに数千回実行されるループで一括処理できます。これが反復可能なプロセスでなければならないかどうかはわかりませんが、より速くより強力でなければならない場合は、より洗練されなければなりません。それ以外の場合、配置は手で縫い付けられます。

また、各ファイルと各パイプに対して複数のプロセスを作成します。単一のプロセスを使用して完全な解析/修正(grep/sed/tr)を実行できます。 grepの後、Zshを展開して他の翻訳を処理できます(参考文献を参照man zshexpn)。あるいは、sed複数の式を使用して、1回の呼び出しですべての単一行操作を実行できます。 (拡張正規表現)を避け、貪欲ではない場合はsedより速くなります。複数のファイルから一致する行を一度に抽出し、中間の一時ファイルに書き込むことができます-rgrepただし、ボトルネックを理解し、未解決の問題を解決しないでください。

関連情報