プロが教える店舗&オフィスのセキュリティ対策術

perl初心者です。いつもありがとうございます。

perlでcsvファイル(1行のカラム数は200)、総行数は約3万行のファイルを37番目のカラム(-25以上25未満の数値データ)で降順ソートしその値によって行数がだいたい均等になるよう3分割し、2番目のカラムに文字でも数字でもよいのですがその4つのグループごとにフラグ(例えば1,2,3)を入れたいと思ってます。グループ化については境目の37番カラムの値は重複している場合が多いと思うのですがその場合は下(別に上でもかまいません)に入れるものとします。

ソートロジックは過去の質問を参照して理解しましたがグループ化しフラグを入れるルーチンがうまく作れません。下記のように作ったのですがこの先同じことを何度もやらなくてはならないので先に進めません。どなたかお助けください。最終的にやりたいことはカラム37でグループ化→カラム2にフラグを立てる、次にカラム2とカラム38(-25から0までの数値)でソートし同様に同じ行数になるようにグループ化→カラム3にフラグを立てる、さらにカラム2とカラム3とカラム39(-25以上25未満の数値データ)でソートし・・・同様に繰り返し最終的に1グループが100件(行)~150件(行)になるようにしたいのです。つまり約3万件のデータを3*4*2*4*2=192分割(5列の値で分類)したい、そしてどのような範囲で分割したかという情報も得たいのです。

use strict;
use warnings;

use utf8;
use Encode;
binmode STDOUT, ':encoding(utf-8)';

my $dir = './data'; # 処理するディレクトリ
my $motoFile = 'customer.txt'; # もとファイル

open my $fh, '<:encoding(cp932)', "$dir/$motoFile" or die 'ファイルが開けません。',"$!";

my %sorted;

while (my $line = <$fh>) {
my $key = (split /,/, $line)[37];
push @{$sorted{$key}}, $line;
if (@{$sorted{$key}} == 1000) {
open OUT, '>>:encoding(cp932)', "$dir/$key.tmp" or die "Can't open: $!";
print OUT @{$sorted{$key}};
close OUT;
@{$sorted{$key}} = ();
}
}

open OUT, '>:encoding(cp932)', "$dir/out.txt" or die "Can't open: $!";
foreach my $key (sort { $b <=> $a } keys %sorted) {
if (-e "$key.tmp") {
open IN, '<:encoding(cp932)', "$dir/$key.tmp" or die "Can't open: $!";
print OUT while <IN>;
close IN;
}
print OUT @{$sorted{$key}} if @{$sorted{$key}};
}
close OUT;

#↓↓↓↓ここからフラグを作成するルーチン
# 行数を調べ3つに分けるルーチン
my @colum37;
open IN, '<:encoding(cp932)', "$dir/out.txt" or die 'ファイルが開けません。',"$!";
my @in = <IN>;
close IN;
my $gyousuu = scalar(@in);
my $amari = $gyousuu % 3;

if ($amari == 0) {
my $groupGyousuu = ($gyousuu-$amari)/3;
print "総行数は$gyousuu","で、1グループの行数は$groupGyousuu","ほど、余りは$amari\n";

# あまりが0の時、group1は@inの0行 ~$groupGyousuu-1行まで
#         group2は@inの$groupGyousuu行 ~$groupGyousuu*2-1行まで
#         group3は@inの$groupGyousuu*2行~$groupGyousuu*3-1行まで
foreach my $num (1..2) {
push @colum37, (split /,/, $in[$groupGyousuu*$num])[37]; # これは境目の先頭の37番目
}
print "@colum37\n"; #これでここまでは完成、分けるべき値がこの配列に入っている。

open OUT, '>:encoding(cp932)', "$dir/out.txt" or die "Can't open: $!";
foreach my $line (@in) {
my @line = split /,/,$line;
if ($line[37]>=$colum37[0]) {
$line[1] = 1;
}elsif ($line[37]>=$colum37[1] and $line[37]<$colum37[0]) {
$line[1] = 2;
}elsif ($line[37]<$colum37[1]) {
$line[1] = 3;
}
$line = join (',',@line);
print OUT $line;
}
close OUT;
}
elsif ($amari == 1) { この後未作成

A 回答 (4件)

## 面白そうだったので、自分流に書いてみました。


## 適当に書いたので、バグがあったらごめんなさいです。
## ← ##で始まる行は解説用コメントです

use strict;
use warnings;
use utf8;

binmode(STDOUT, ':utf8');
binmode(STDERR, ':utf8'); ## エラー出力も考慮しましょう

my $dir = './data'; # 処理するディレクトリ
my $motoFile = 'customer.txt'; # もとファイル

# データファイル読み込み
my @array;
open(my $fh, '<:encoding(cp932)', "$dir/$motoFile") or die 'ファイルが開けません。',"$!";
while(my $line = <$fh>) {
chomp $line;
## 「配列リファレンスの配列」を作る。複数カラムでのソートが楽になります
push(@array, [ split(/,/, $line) ]);
}
close($fh);

# 配列リファレンス内の指定カラム値を比較して配列をソート
## どこを使ってソートするかわからなかったので
## とりあえず[37]~[41]でソート
my @sorted = sort {
$b->[37] <=> $a->[37] ||
$b->[38] <=> $a->[38] ||
$b->[39] <=> $a->[39] ||
$b->[40] <=> $a->[40] ||
$b->[41] <=> $a->[41]
} @array;

# ソート済みデータファイル出力
## 分類用フラグは後付けで計算
open(my $fh_out, '>:encoding(cp932)', "$dir/out.txt") or die "Can't open: $!";
for(my $i=0; $i<192; $i++) {
# 192等分したブロックの先頭と終了のインデックスを求める
## 余りの分を先頭に近いブロックに寄せる方法については宿題としておきます(難しくないです)
my $start = int(@sorted * $i / 192);
my $end = int(@sorted * ($i+1) / 192) - 1;

# 分類用フラグの計算
my @flag_value = (
int($i / (4 * 2 * 4 * 2)) % 3 + 1,
int($i / (2 * 4 * 2)) % 4 + 1,
int($i / (4 * 2)) % 2 + 1,
int($i / 2) % 4 + 1,
$i % 2 + 1,
);

# 出力範囲の表示
## とりあえず標準出力に。
## データ形式に合わせて見やすいように整形するといいでしょう
printf(
"[%d, %d, %d, %d, %d] (%s, %s, %s, %s, %s) ~ (%s, %s, %s, %s, %s)\n",
@flag_value,
@{$sorted[$start]}[37..41],
@{$sorted[$end]}[37..41],
);

# ブロック毎にフラグを上書きしながら出力
for(my $j=$start; $j<=$end; $j++) {
## 分類用フラグをどこに立てるかわからなかったので
## とりあえず[2]~[6]に立てる
@{$sorted[$j]}[2..6] = @flag_value;

# 出力
print $fh_out join(',', @{$sorted[$j]}),"\n";
}
}
close($fh_out);

## 具体的な用途が不明なので以下は独り言ですが、
## 正直なところ、カラム内にインデックスフラグ立てるメリットは
## ほとんどない気がします・・。
## 結果を別ファイルに分けるならわかるのですが、それならなおさら
## カラム内にフラグを持たせる必要はないように思えます。

この回答への補足

動作結果です。
$ ./mainBunkatu3.pl
Malformed UTF-8 character (unexpected continuation byte 0x81, with no preceding start byte) at ./mainBunkatu3.pl line 64.
[1, 1, 1, 1, 1] (18.90, 0.00, 0.00, 0.00, 0.00) ` (8.70, 0.00, 0.00, 0.00, 0.00)
[1, 1, 1, 1, 2] (8.70, 0.00, 0.00, 0.00, 0.00) ` (7.40, 0.00, 0.00, 0.00, 0.00)
~中略~
[1, 4, 2, 4, 1] (-0.50, 0.00, 0.00, 0.00, 0.00) ` (-0.59, 0.00, 0.00, 0.00, 0.00)
[1, 4, 2, 4, 2] (-0.59, 0.00, 0.00, 0.00, 0.00) ` (-0.59, 1.00, 1.00, 0.00, 1.00)
[2, 1, 1, 1, 1] (-0.59, 0.00, 0.00, 0.00, 1.00) ` (-0.69, 3.00, 4.00, 1.00, 3.00)
[2, 1, 1, 1, 2] (-0.69, 1.00, 0.00, 0.00, 1.00) ` (-0.70, 0.00, 0.00, 0.00, 0.00)
~中略~
[2, 4, 2, 4, 1] (-3.90, 0.00, 1.00, 0.00, 2.00) ` (-4.00, 0.00, 0.00, 0.00, 0.00)
[2, 4, 2, 4, 2] (-4.00, 1.00, 1.00, 0.00, 0.00) ` (-4.00, 0.00, 0.00, 0.00, 0.00)
[3, 1, 1, 1, 1] (-4.00, 0.00, 0.00, 0.00, 0.00) ` (-4.10, 0.00, 0.00, 0.00, 1.00)
[3, 1, 1, 1, 2] (-4.10, 0.00, 0.00, 0.00, 0.00) ` (-4.20, 0.00, 0.00, 0.00, 0.00)
~中略~
[3, 4, 2, 3, 2] (-11.70, 0.00, 0.00, 0.00, 0.00) ` (-12.30, 0.00, 0.00, 0.00, 0.00)
[3, 4, 2, 4, 1] (-12.30, 0.00, 0.00, 0.00, 0.00) ` (-13.70, 0.00, 0.00, 0.00, 0.00)
[3, 4, 2, 4, 2] (-13.70, 4.00, 3.00, 0.00, 0.00) ` (-22.50, 0.00, 1.00, 0.00, 0.00)
実は境目の-0.59(一つめのフラグ)はどちらかに入れたいのです。同様に-4の人たちもです。

補足日時:2012/06/19 11:25
    • good
    • 0
この回答へのお礼

すごいです。動きました。ありがとうございます。私には読むのも時間がかかります。こんなに短い時間で書けるとは!ただ、ただ驚きです。私には読むだけでも2日~3日かかりますのでとりあえずお礼させていただきます。

お礼日時:2012/06/19 11:24

各段階で等分するのなら、分割しながらソートしてもソートしてから分割しても


同じ事なので、本当は値の範囲ごとに区分したいんだと思いますが。
Unix系ならシェルスクリプトで手軽にできますよ。

#!/bin/sh
line=$(( $(sed -n '$=' customer.txt) / (3 * 4 * 2 * 4 * 2) + 1 ))
sort -s -t, -k38nr -k39nr -k40nr -k41nr -k42nr | split -a 3 -d -l ${line}
mkdir -p ./dir
mv x[0-9][0-9][0-9] ./dir

この回答への補足

皆さん、なかなかやりたいことが伝えられず申し訳ありません。下のANo.3の方にはもっと補足を書こうと思ったのですが間違えてクリックし確定してしまいました。

補足日時:2012/06/19 12:29
    • good
    • 0
この回答へのお礼

大変ありがとうございます。私には想像もつかない方法をご存じで、世の中にはすごい人がいるものだなぁとただただ感じいっております。
試してみます。これも解読するのには3日以上かかるかと思われますのでお礼を先にさせていただきます。

お礼日時:2012/06/19 11:33

前回の回答で $count をクリアする文を入れるのを忘れていました。

すみませんが、修正願います。

if ($count == $groupGyousuu + ($idx_amari < $amari ? 1 : 0)) {
close OUT;
$flag = next_flag($flag);
$idx_amari++;
$count = 0; # この文を追加
open OUT '>:encoding(cp932)', "$dir/out$flag.txt" or die "Can't open: $!";
}
    • good
    • 0
この回答へのお礼

ありがとうございます。大変参考になります。お礼が遅くなり申し訳ありません。試したり読んだりしているのですが初心者の私にはとても敷居が高くて、まだまだ時間がかかってしまいそうなのでお礼させていただきます。

お礼日時:2012/06/19 11:07

out.txt を作成するまでは、よくできているいるように思います。

その後の細分化しながらのグループ分けは、あまり有効的なやり方ではありません。カラム37番目 (コード中も 37 になっているので、0 から数えた?) が整数であるとすると、最大 50 種類で平均しても1種類で 600 行にもなってしまうからです。ソート対象は37番目のカラムのみのようですので、ソート済みの out.txt の行を順に 11111, 11112, 11121, ..., 34232, 34241, 34242 に割り当てたほうが簡単に済むように思います。

while (my $line = <$fh>) {
...
if (eof) { $groupGyousuu = int($. / 192); $amari = $. % 192; }
}

最初の while ループの最終行でグループ行数と余りの行を算出しておきます。次のコードでは、フラグをファイ名に含めてあります。行に入れる場合は、コメントアウトしてある行を参考にしてみてください。(なお、余りの行がない場合、余分な空ファイルが作られてしまいますが、繁雑になるので除外のコードを入れてありません。)

my ($count, $idx_amari, $flag) = (0, 0, '11111');
open IN, '<:encoding(cp932)', "$dir/out.txt" or die "Can't open: $!";
open OUT, '>:encoding(cp932)', "$dir/out$flag.txt" or die "Can't open: $!";
while (my $line = <IN>) {
# substr($line, index($line, ','), 0) = ',' . join(',', split //, $flag);
print OUT $line;
$count++;
if ($count == $groupGyousuu + ($idx_amari < $amari ? 1 : 0)) {
close OUT;
$flag = next_flag($flag);
$idx_amari++;
open OUT '>:encoding(cp932)', "$dir/out$flag.txt" or die "Can't open: $!";
}
}
close OUT;

sub next_flag {
my @temp = split //, $_[0];
my @limit = (3, 4, 2, 4, 2);
foreach my $i (reverse 0 .. 4) {
if ($temp[$i] < $limit[$i]) {
$temp[$i]++;
return join('', @temp);
} else {
$temp[$i] = 1;
}
}
}
    • good
    • 0

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!