まったく同じ構造の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)
)までを削除し、末尾の.xml
(sub(".xml$", "", FILENAME)
)と結果を出力します(printf FILENAME
)/double/{printf " %s", $3}
行に「double」(/double/
)が含まれている場合は、スペースに続けて3番目のフィールド(printf " %s", $3
)を出力します。 <
と>
を区切り文字として使用すると、これが数値になります(最初のフィールドは最初の<
の前にあり、2番目のフィールドはdouble
です)。必要に応じて、ここで数値をフォーマットできます。たとえば、%8.3f
の代わりに%s
を使用すると、任意の数値が小数点以下3桁で、全長(ドットと小数点以下を含む)が少なくとも8で出力されます。$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_xml
:find
に$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
ここで、BEGINFILE
、ENDFILE
は、ファイルを変更するときに呼び出されます(awkがサポートしている場合)。
将来のメンテナンスプログラマーとシステム管理者に代わって、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")
ファイルごとにこの単一のライナーを試すことができます。 awkの複数の区切り文字は効率的な分割を行い、trはディスクではなくメモリ内のすべての行を連結します。
for f in `ls *.xml` ;
do
echo $f,`grep double $f | awk -F '[<>]' '{print $3}' | tr '\n' ','`;
done
私はこれを私の最後にプロファイリングすることはできません-私は同じデータを持っていないので、しかし私の予感はそれがより速いはずだということです。
それとは別に、これは分割統治が最も簡単な問題です。複数のマシンまたはファームにアクセスできる場合は、タスク全体を複数のマシンに分割し、最終的にすべての出力を1つのファイルに連結できます。このようにして、コマンドラインの制限とメモリも管理できます。