PAGE TOP

取り組み

印刷する

gawkの連想配列を工夫してみた

CSVデータの集計

さぼ郎
データベース

古いと言われている「awk」です。

繰り返しになりますが、この「awk」は、ストリームエディタと言って、1行ずつテキストデータを取り込んで、必要に応じた加工をして出力するという処理をします。

いたって簡単な記述で、結構なことができます。ネットで調べると、awkの使い手の人たちは、とても難しい記述で、とても難しい処理をしています。

ワタシは、いたって簡単なことしか書けないし、簡単な処理しかしません。そもそも、言語はコミュニケーション・ツールであるので、意思の疎通としては、極力簡単な伝え方しか覚えない主義です。

単純な集計を考えてみます。
データは、
福島支店,20160410,L,2900,63,男,福島県
福島支店,20160410,L,36000,56,男,福島県
福島支店,20160410,L,6000,80,男,福島県
福島支店,20160410,L,4000,67,男,福島県
福島支店,20160410,L,5300,67,男,福島県
このような感じで1千万件超あります。

データの意味は、
支店、購入年月日、購入商品、価格、年齢、性別、都道府県
このような感じ構成されています。

《count.awk
BEGIN{
FS=",";
OFS="t";
}
{
dataGetCC(1);
dataGetCC(2);
dataGetCC(3);
dataGetCC(4);
dataGetCC(5);
dataGetCC(6);
dataGetCC(7);
}
function dataGetCC(pos){
buff[pos,$pos]+=1;
cc[pos]+=1;
}
function dataOutCC(pos){
print "n
" pos "n";

for (i in buff) {
reg="^" pos ;
if(i~reg){
str=i;
asc=sprintf("%c",28); sub(asc,"t",str); print str,buff[i];
}
}
printf "n";
}
END{
dataOutCC(1);
dataOutCC(2);
dataOutCC(3);
dataOutCC(4);
dataOutCC(5);
dataOutCC(6);
dataOutCC(7);
}

BEGIN{
FS=",";
OFS="t";
}
BEGIN」は冒頭で1回だけ設定できます。
変数の初期化とか、このような約束変数の初期化をします。

FS」はセパレートを指定します。タブなら「t」。
OFS」は、出力する際のセパレータを指定します。
どちらも通常(デフォルト)なら半角スペースです。

function dataGetCC(pos){
buff[pos,$pos]+=1;
cc[pos]+=1;
}
関数を定義しています。
引数の「pos」で、データ列の位置を指定します。

buff[pos,$pos] は、なにをしているかというと、連想配列を2元で使っています。
つまり、データ列の位置「pos」と、その値「$pos」を配列の添字として「+=1」でインクリメントしています。

余談ですが、この「連想配列」という言葉は、奇妙です。配列で何を連想するのかが今ひとつスッキリしません。

英語では「associative array」というようで、associative に「連想」の意味があるからなのだと思います。

ようは、配列の添字に文字列が使えるというのが「連想配列」というわけです。

それを2元にして使おうとしています。

dataGetCC(1);
dataGetCC(2);
dataGetCC(3);
このように関数に、データ列の位置を渡しています。

function dataOutCC(pos){
print "n【" pos "】n";
for (i in buff) {
reg="^" pos ;
if(i~reg){
str=i;
asc=sprintf("%c",28);
sub(asc,"t",str);
print str,buff[i];
}
}
printf "n";
}
連想配列の場合、添字の数が不明なのと数値ではないのでループでしていることもできません。
そこで、一般的には、

for (i in buff)
print i,buff;

のような書き方をします。
出力は、期待するような順で出力されるわけではないので、後から並び替える必要があります。

この関数では、

buff[pos,$pos]+=1;

のように、スカラー型(posのこと)とデータ列の文字列を組み合わせていますので、連想配列の出力の仕方に依存します。

for (i in buff) { 連想配列の通常の出力の仕方
reg="^" pos ; 正規表現を変数にセットしている
if(i~reg){ 連想配列の添字と変数をマッチ
str=i; 変数に連想配列の添字をセット
asc=sprintf("%c",28); 制御コードをバイナリに
sub(asc,"t",str); バイナリコードをタブに返還
print str,buff[i]; 制御コードをタブに返還
}
}

連想配列にスカラー型の添字と文字列を組み合わせて2元配列にしていますが、実際にはアスキーコードの「0x1c(バイナリの28)」を挟んで多元配列風にしています

0x1c」は「FS(フォーム区切り)」だそうです。

なので、2元化した連想配列の添字は、実は、「0x1c」で繋いだ文字列を添字にしているようです。で、その0x1c」を、出力の際には「タブ」にしています。

csvファイルは16個あります。データ数が多いのはExcelの「1,408,576行」を超えているファイルもあります。

カウントは、

{
++cc;
}
END{
print cc;
}

書くのはたったの6行です。
「END」は1回だけ実行されます。

ストップウォッチ

1年間の発生件数は、「14,606,877レコード」あります。
1千460万行、カウントするのに要した時間は、「4.64秒!」でした。

>gawk -f scriptcount.awk *.csv>ttl.txt
では、集計してみましょう。

7列のデータを、それぞれの項目ごとの発生件数を集計してみると、所要時間は「57.42秒」でした。

ストップウォッチ

集計1軸、約8秒程度です。

レコード件数が14,606,877レコード」、
売上金額が「157,475,328,500円」。

実は、この集計を3年間ほど、Access+Excelで行っていました。Accessにはデータベースの最大サイズが2Gバイトという制約があり、それを超えるときには「ユニオンクエリ」というテーブル結合クエリを使って、半期ごとにデータベースに入れて集計していました。

CSVをAccessに取り込むのも結構時間がかかり、クロス集計は2時間位かかっていたと思いますが、gawkならば、出力後の編集が必要になりますが、集計そのものならそんなに時間がかかりそうもない気がしています。

約1600億円の集計が、フリーソフトの「gawk」で、たった39行で、こんなに簡単に、スピーディーにできてしまいます。

まとめ

このような処理をAccessでやると、いろいろな手口を駆使しないと運用できません。

Accessの弱点。
1.一つのデータベースに2Gバイトの壁があること。
2.テーブルの列幅が255しか無いこと
3.MACで動かないこと
ここがクリアできれば、もっと利用が増えるように思いますが、何か考えがあるのでしょう。

ネットで書かれているように「壊れる」という現象はほとんど起きません。「壊れる」のには、作り方に問題があるような気もします。

次回は、1400万件のクロス集計にチャレンジしてみようと思います。

キーワード