web-dev-qa-db-ja.com

複数のファイルから単一のCSVファイルへの効率的なデータ抽出

まったく同じ構造のXMLファイルの大規模なコレクションがあります。

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

ここで、各XMLファイル内のそのような<double>エントリの数は固定されており、既知です(私の特定の場合、168)。

これらすべてのXMLファイルの内容を次のように保存して単一のcsvファイルを作成する必要があります。

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の回答を読んでください。それは最速であり、はるかに最高にスケールアップするものです。

これでうまくいくはずです:

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:プログラムawkを使用し、GNU awk4.0.1でテストしました
  • -F '[<>]':フィールド区切り文字として<>を使用します
  • NR!=1 && FNR==1{printf "\n"}:全体の最初の行(NR!=1)ではなく、ファイルの最初の行(FNR==1)が改行を出力する場合
  • FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME}:ファイルの最初の行の場合は、ファイル名(FILENAME)の最後の/sub(".*/", "", FILENAME))までを削除し、末尾の.xmlsub(".xml$", "", FILENAME))と結果を出力します(printf FILENAME
  • /double/{printf " %s", $3}行に「double」(/double/)が含まれている場合は、スペースに続けて3番目のフィールド(printf " %s", $3)を出力します。 <>を区切り文字として使用すると、これが数値になります(最初のフィールドは最初の<の前にあり、2番目のフィールドはdoubleです)。必要に応じて、ここで数値をフォーマットできます。たとえば、%8.3fの代わりに%sを使用すると、任意の数値が小数点以下3桁で、全長(ドットと小数点以下を含む)が少なくとも8で出力されます。
  • END {printf "\ n"}:最後の行の後に追加の改行を出力します(これはオプションの場合があります)
  • $path_to_xml/*.xml:ファイルのリスト
  • > final_table.csv:出力をリダイレクトして結果をfinal_table.csvに入れます

「引数リストから長い」エラーの場合は、findをパラメーター-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を実行します。 +は、複数のファイルを一度に渡すことができることを示します。これにより、フォークが減少します。 \;の代わりに;+は引用符で囲む必要があり、そうでない場合はシェルによって解釈されます)を使用する場合、コマンドはファイルごとに個別に実行されます。

xargsfindと組み合わせて使用​​することもできます。

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

ここで、BEGINFILEENDFILEは、ファイルを変更するときに呼び出されます(awkがサポートしている場合)。

5
Adaephon

将来のメンテナンスプログラマーとシステム管理者に代わって、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")
2
Sobrique

ファイルごとにこの単一のライナーを試すことができます。 awkの複数の区切り文字は効率的な分割を行い、trはディスクではなくメモリ内のすべての行を連結します。

for f in `ls *.xml` ; 
do 
     echo $f,`grep double $f | awk  -F  '[<>]' '{print $3}' | tr '\n' ','`; 
done

私はこれを私の最後にプロファイリングすることはできません-私は同じデータを持っていないので、しかし私の予感はそれがより速いはずだということです。

それとは別に、これは分割統治が最も簡単な問題です。複数のマシンまたはファームにアクセスできる場合は、タスク全体を複数のマシンに分割し、最終的にすべての出力を1つのファイルに連結できます。このようにして、コマンドラインの制限とメモリも管理できます。

0
amisax