ソートされた複数のCSVファイルを結合するスクリプト
問題
CSVファイルが複数存在する。各ファイルは異なる列データが保存されているが、どのファイルもCSV左端列でソートされている。これらを左端列でソートされた1つのCSVファイルにまとめたい。まとめるプログラムを作れ。
なお、CSVファイルは数万行を想定し、メモリ消費を考慮したプログラムを書くこと。
CSVデータ
data1.txt
#id,data11,data12 1,data111,data121 2,data112,data122 3,data113,data123 4,data114,data124 5,data115,data125 6,data116,data126 7,data117,data127
data2.txt
#id,data21,data22 0,data210,data220 1,data211,data221 4,data214,data224 5,data215,data225 6,data216,data226 7,data217,data227 8,data218,data228 9,data219,data229
data3.txt
#id,data31,data32 0,data310,data320 1,data311,data321 2,data312,data322 5,data315,data325 6,data316,data326 7,data317,data327 8,data318,data328 9,data319,data329
期待する出力例
※カンマだとわかりにくいので、タブ文字で出力してみた。
id data11 data12 data21 data22 data31 data32 0 data210 data220 data310 data320 1 data111 data121 data211 data221 data311 data321 2 data112 data122 data312 data322 3 data113 data123 4 data114 data124 data214 data224 5 data115 data125 data215 data225 data315 data325 6 data116 data126 data216 data226 data316 data326 7 data117 data127 data217 data227 data317 data327 8 data218 data228 data318 data328 9 data219 data229 data319 data329
回答例
解説
まず、ヘッダを処理する。その後、データ部を処理する。
各CSVファイルは歯かけでデータが入っているため、行を read しても、その行がすぐ出力できるとは限らない。出力する行の選択方法は、各CSVから1行読み込み、IDが最も小さいデータのみを探すことで実現した。その後、最小のIDのみを出力する。
この方法だと同一行に対する読込みが2回以上発生する。そこで、簡単な行バッファクラスBfhを実装してコード可読性を向上させている。
なお、各CSVファイルはソートされているので、行バッファの最大バッファ行数は1行で済む。
read-sorted-files.pl
#!perl package Bfh; use strict; use warnings; sub new { my ($class, %args) = @_; bless { fh => $args{fh}, buff_line => undef, } } sub readline { my $self = shift; my $line; if ( $self->{buff_line} ) { $line = $self->{buff_line}; $self->{buff_line} = ''; } else { my $fh = $self->{fh}; $line = <$fh>; chomp($line) if defined $line; } $line; } sub unreadline { my ($self, $line) = @_; $self->{buff_line} = $line; 1; } package __main__; use strict; use warnings; use Data::Dumper; my @all_files = ('data1.txt', 'data2.txt', 'data3.txt'); my $separator = ","; my @desc = map { {} } (1..@all_files); sub cmp_id { my ($x, $y) = @_; $x <=> $y } # open for (my $i = 0; $i < @all_files; $i++) { my $file = $all_files[$i]; open(my $fh, '<', $file) or die $!; my $bfh = Bfh->new( fh => $fh ); $desc[$i]{bfh} = $bfh; } # read header for my $d (@desc) { my $header = $d->{bfh}->readline; my ($id, @cols) = split(/,/, $header); $d->{cols} = \@cols; } # output header my @all_cols; for my $d (@desc) { push @all_cols, @{ $d->{cols} }; } print join($separator, 'id', @all_cols),"\n"; # output data while (1) { # pass1: pick up min id my $min_id; my $is_bfh_open; for my $d (@desc) { my $line = $d->{bfh}->readline; next unless defined($line); $is_bfh_open = 1; my ($id) = split(/,/, $line); $min_id ||= $id; $min_id = $id if cmp_id($id, $min_id) < 0; $d->{bfh}->unreadline($line); } last unless $is_bfh_open; # pass2: pick up data my %record; for my $d (@desc) { my $line = $d->{bfh}->readline; next unless defined($line); my ($id, @data) = split(/,/, $line); if ($id != $min_id) { $d->{bfh}->unreadline($line); next; } $record{id} ||= $id; @record{ @{ $d->{cols} } } = @data; } # output data map { $record{$_} ||= '' } @all_cols; # warning eater print join($separator, $min_id, @record{ @all_cols }),"\n"; } __END__