私はPerlで大きなCSV形式のファイルを解析するプロジェクトに取り組んでおり、物事をより効率的にすることを目指しています。
私のアプローチは、最初に行ごとにsplit()
、次にフィールドを取得するためにカンマで各行をsplit()
します。ただし、データのパスを少なくとも2回必要とするため、これは最適ではありません。 (一度行ごとに分割してから、行ごとにもう一度)。これは非常に大きなファイルであるため、半分にカットする処理は、アプリケーション全体の大幅な改善になります。
私の質問は、組み込みツールのみを使用して大きなCSVファイルを解析する最も時間効率の良い方法は何ですか?
注:各行にはさまざまな数のトークンがあるため、行を無視してカンマのみで分割することはできません。また、フィールドには英数字のASCIIデータのみが含まれると想定できます(特殊文字やその他のトリックは含まれません)。また、効果的に機能する可能性はありますが、並列処理を行いたくありません。
編集
Perl 5.8に同梱されている組み込みツールのみを使用できます。官僚的な理由で、サードパーティのモジュールを使用できません(cpanでホストされている場合でも)
別の編集
ソリューションがファイルデータを完全にメモリにロードした後にのみ処理できると仮定しましょう。
まだ別の編集
私はこの質問がどれほど愚かかを把握しました。時間を無駄にしてすみません。終了する投票。
それを行う正しい方法は、桁違いに Text :: CSV_XS を使用することです。それはあなたが自分で行う可能性が高いものよりもはるかに高速で堅牢です。コア機能のみを使用する場合、速度と堅牢性に応じていくつかのオプションがあります。
Pure-Perlで得られる最速の速度は、ファイルを1行ずつ読み取り、データを単純に分割することです。
my $file = 'somefile.csv';
my @data;
open(my $fh, '<', $file) or die "Can't read file '$file' [$!]\n";
while (my $line = <$fh>) {
chomp $line;
my @fields = split(/,/, $line);
Push @data, \@fields;
}
フィールドに埋め込みコンマが含まれている場合、これは失敗します。より堅牢な(ただし遅い)アプローチは、Text :: ParseWordsを使用することです。これを行うには、split
を次のように置き換えます。
my @fields = Text::ParseWords::parse_line(',', 0, $line);
こちらも引用符を尊重するバージョンです(例:foo,bar,"baz,quux",123 -> "foo", "bar", "baz,quux", "123"
)。
sub csvsplit {
my $line = shift;
my $sep = (shift or ',');
return () unless $line;
my @cells;
$line =~ s/\r?\n$//;
my $re = qr/(?:^|$sep)(?:"([^"]*)"|([^$sep]*))/;
while($line =~ /$re/g) {
my $value = defined $1 ? $1 : $2;
Push @cells, (defined $value ? $value : '');
}
return @cells;
}
次のように使用します。
while(my $line = <FILE>) {
my @cells = csvsplit($line); # or csvsplit($line, $my_custom_seperator)
}
他の人が言ったように、これを行う正しい方法は、 Text :: CSV 、およびText::CSV_XS
バックエンド(最速の読み取り用)またはText::CSV_PP
バックエンド(XS
モジュールをコンパイルできない場合)。
余分なコードをローカルで取得することが許可されている場合(たとえば、独自の個人モジュール)Text::CSV_PP
とローカルのどこかに配置してから、use lib
回避策:
use lib '/path/to/my/perllib';
use Text::CSV_PP;
さらに、ファイル全体をメモリに読み込んでスカラーに格納する(と思われる)代替方法がない場合でも、スカラーへのハンドルを開くことで、ファイルハンドルのように読み込むことができます。
my $data = stupid_required_interface_that_reads_the_entire_giant_file();
open my $text_handle, '<', \$data
or die "Failed to open the handle: $!";
次に、Text :: CSVインターフェイスを介して読み取ります。
my $csv = Text::CSV->new ( { binary => 1 } )
or die "Cannot use CSV: ".Text::CSV->error_diag ();
while (my $row = $csv->getline($text_handle)) {
...
}
またはコンマの次善の分割:
while (my $line = <$text_handle>) {
my @csv = split /,/, $line;
... # regular work as before.
}
この方法では、データはスカラーから一度に1ビットだけコピーされます。
ファイルを1行ずつ読み取る場合、1回のパスで実行できます。一度にすべてをメモリに読み込む必要はありません。
#(no error handling here!)
open FILE, $filename
while (<FILE>) {
@csv = split /,/
# now parse the csv however you want.
}
しかし、これが大幅に効率的かどうかは確かではありませんが、Perlは文字列処理がかなり速いです。
インポートをベンチマークする必要があります減速の原因を確認します。たとえば、85%の時間がかかるdb挿入を実行している場合、この最適化は機能しません。
編集
これはコードゴルフのように感じられますが、一般的なアルゴリズムはファイル全体またはファイルの一部をバッファに読み込むことです。
Csvデリミターまたは新しい行が見つかるまで、バッファーを1バイトずつ繰り返します。
それでおしまい。しかし、大きなファイルをメモリに読み込むのは本当に最善の方法ではありません。これが行われる通常の方法については、元の答えを参照してください。
CSVファイルを$csv
変数にロードし、正常に解析した後、この変数にテキストが必要ないと仮定します。
my $result=[[]];
while($csv=~s/(.*?)([,\n]|$)//s) {
Push @{$result->[-1]}, $1;
Push @$result, [] if $2 eq "\n";
last unless $2;
}
$csv
をそのままにする必要がある場合:
local $_;
my $result=[[]];
foreach($csv=~/(?:(?<=[,\n])|^)(.*?)(?:,|(\n)|$)/gs) {
next unless defined $_;
if($_ eq "\n") {
Push @$result, []; }
else {
Push @{$result->[-1]}, $_; }
}
質問によって課せられた制約内で答える場合、入力ファイルをスカラーではなく配列に丸みすることで、最初の分割を切り取ることができます。
open(my $fh, '<', $input_file_path) or die;
my @all_lines = <$fh>;
for my $line (@all_lines) {
chomp $line;
my @fields = split ',', $line;
process_fields(@fields);
}
そして、インストールできない場合でも(の純粋なPerlバージョン)Text::CSV
、ソースコードをCPANにプルアップし、コードをプロジェクトにコピー/貼り付けすることで回避できる場合があります...