今日からまた新コーナー「たまに書くならこんなPerl」をスタートします。
 コンピューター言語の入門書なんか読んでいて「理論は分かったけど、これどういう風に使えばいいの?」、「この人本当に実生活でプログラムを使ってるの?」と思ったことないですか。ぼくはしょっちゅうあります。書く方も結構辛くて、入門書は基本的な理論、機能を1個ずつ紹介するので、どうしても実験的な、教材丸出しのプログラムになりがちですね。特に最初の方は後で出てくる機能を使う機能を使えないという縛りがありますから、なかなか苦しいです。『かんたんPerl』は、そんな中でもせいぜい実用的な、現実社会をよく写したプログラムを書くように苦心したつもりですが、どうでしょうね。
 前置きが長くなりましたが、今日はぼくが現実社会で使っているプログラムをお見せして、こんな風にPerlが活かせるということを楽しんでいただけると幸いです。今日のお題は「PASMOを読み込んでマネーフォワードにアップロードする」です。
 交通系ICカード「PASMO」は、ネットから使用履歴を見ることが出来ません。FeLiCaというカードリーダーがSONYから出ているので、それをパソコンにつないで読み込みます。
IMG_3235
 読み込んだデータは、SFCard ViewerというソフトでCSVに変換できます。
20160202SFCV
 CSVはConma Seperated Values(カンマ区切り値)の略で、Excelなどのソフトで読める表計算ソフトの形式の一つです。エクセルで読み込むとこうなります。
20160202csv
 テキスト形式なので、エディターでも読み込めます。
20160202csv2
 自分のプライバシーに配慮して、ほとんどデータを消しててスミマセン。

 オンライン家計簿サービス「マネーフォワード」は、銀行や証券会社、クレジットカードとはよく連携して、手間無しで履歴を取ってくれます。現金は手入力しないといけないのは当たり前ですが、最近はレシートをスマホのカメラで認識する機能などを取り入れています。
20160202mnfw
 問題はプリペイドカードで、かなりショボい、いつまで経っても連携しないです。特に問題なのがPASMO(SUICAも)で、いちいちFeLiCaで読み込んでCSVに保存しなければいけない。
 それだけならしょうがないで済むのですが、このCSVがマネーフォワードとフィールドの順番が違っていて、これを直すのがめちゃめちゃ面倒です。

■PASMO側:
1行目:カードID=xxxxxx
2行目(見出し):利用年月日,定期,鉄道会社名,入場駅/事業者名,定期,鉄道会社名,出場駅/降車場所,利用額(円),残額(円),メモ
3行目以降明細

■マネーフォワード側:
1行目(見出し)計算対象,日付,内容,金額,保有金融機関,大項目,中項目,メモ
2行目以降明細

 見事に順番も項目数も違います。
 いちおうマネーフォワードにもウィザード的なものがあって、どのフィールドを捨ててどの順に並べ替えますかみたいなのができるんですが、私すぐギブアップしました。もうちょっと使いやすくして欲しいものだ。ていうかPASMOのフォーマットのテンプレートぐらい、持っていてほしいものです。あるいはSFCard Viewer側で「マネーフォワード形式でエクスポート」という機能があるとか。いまいちどっちもやる気が感じられません。

 Excelで手でやってもいいし、Excel VBAでやってもいいんでしょうけど、ぼくは長いサラリーマン生活で結局どっちも身につけられませんでした。csvはテキストファイルなので、Perlで加工しようと思います。

 「CPAN CSV」で検索すると、Text::CSV_XSというモジュールがあるので、これを使ってみます。オブジェクト指向のモジュールですが、解説を読むとそんなに難しくないようでした。
 うちの環境では、最初に「cpanm Text::CSV_XS」でインストールしないとモジュールが使えませんでした。

#!/usr/local/bin/perl
#
# ps2mf.pl -- PASMOのファイルをマネーフォワード形式に変換

use strict;
use warnings;
use 5.010;
use utf8;
use Text::CSV_XS;

binmode STDERR, ":encoding(UTF-8)";

my $ps = Text::CSV_XS->new({binary => 1});
my $mf = Text::CSV_XS->new({binary => 1});

open PS, "<:encoding (Shift_JIS)", "sfcv_history.csv" or die $!;
open MF, ">:encoding(Shift_JIS)", "money_forward.csv" or die $!;
select MF;

scalar(<PS>) for 1..2; # 見出しを空読み

say "計算対象,日付,内容,金額,保有金融機関,大項目,中項目,メモ";

while(<PS>){
 $ps->parse($_) or next;
 # 利用年月日,定期,鉄道会社名,入場駅/事業者名,定期,鉄道会社名,出場駅/降車場所,利用額(円),残額(円),メモ
 my ($date, undef, $incom, $insta, undef, $outcom, $outsta, $amount, undef, $memo)
  = $ps->fields;

 # 計算対象,日付,内容,金額,保有金融機関,大項目,中項目,メモ
 $mf->combine("1", $date,
  "PASMO_".$incom.$insta."=>".$outcom.$outsta,
  $amount, "PASMO", "交通費", "電車", $memo);

 say $mf->string();
}

close PS;
close MF;

 では解説します。

my $ps = Text::CSV_XS->new({binary => 1});
my $mf = Text::CSV_XS->new({binary => 1});

 これはCSVを読み込むオブジェクトを作っています。$psはPASMO、$mfはマネーフォワード用のオブジェクトです。日本語の場合は「{ binary => 1 }」という引数を渡します。

open PS, "<:encoding (Shift_JIS)", "sfcv_history.csv" or die $!;
open MF, ">:encoding(Shift_JIS)", "money_forward.csv" or die $!;
select MF;

 これは普通にCSVファイルを開いています。自分で使う用なので、ファイル名を固定でガッと書いてしまっています。

scalar(<PS>) for 1..2; # 見出しを空読み

 PASMOのファイルには、先頭2行に見出しが入っているのでこれを読み飛ばします。<PS>をスカラーコンテキストで2回評価するだけです。
 ループの中に入ります。

 $ps->parse($_) or next;

 parseメソッドで行を読み込みます。失敗したら偽が返るので、次の集会に回ります。

 my ($date, undef, $incom, $insta, undef, $outcom, $outsta, $amount, undef, $memo)
  = $ps->fields;

 読み込んだPASMOデータを解釈しています。使わないカラムはundef関数を左辺値に受けます。こうするとその値は虚空の彼方へ消えてしまいます。

 $mf->combine("1", $date,
  "PASMO_".$incom.$insta."=>".$outcom.$outsta,
  $amount, "PASMO", "交通費", "電車", $memo);

 こっちは値を組み立てています。固定値はリテラルで埋めています。「内容」というフィールドは「PASMO_乗車会社乗車駅=>下車会社下車駅」という文字列にしています。カンタンだなー!

 say $mf->string();

 最後に組み立てた文字列を出力して終わりです。

 では一瞬ぼくの個人情報を晒して実行してみます。

カードID=99999999
利用年月日,定期,鉄道会社名,入場駅/事業者名,定期,鉄道会社名,出場駅/降車場所,利用額(円),残額(円),メモ
2016/01/31,,メトロ,新宿三丁目,,東急,新丸子,195,2649,
2016/01/31,,メトロ,南阿佐ケ谷,,メトロ,新宿三丁目,195,2844,
2016/01/31,,メトロ,新宿三丁目,,メトロ,南阿佐ケ谷,30,3039,
2016/01/31,,東急,新丸子,,メトロ,新宿三丁目,360,3069,

 実行結果はこうなります。

計算対象,日付,内容,金額,保有金融機関,大項目,中項目,メモ
1,2016/01/31,"PASMO_メトロ新宿三丁目=>東急新丸子",195,PASMO,"交通費","電車",
1,2016/01/31,"PASMO_メトロ南阿佐ケ谷=>メトロ新宿三丁目",195,PASMO,"交通費","電車",
1,2016/01/31,"PASMO_メトロ新宿三丁目=>メトロ南阿佐ケ谷",30,PASMO,"交通費","電車",
1,2016/01/31,"PASMO_東急新丸子=>メトロ新宿三丁目",360,PASMO,"交通費","電車",

 これでこのままマネーフォワードにアップロードできます。
 PASMOは過去20日間ぐらいのデータをまるまる取っているので、行の削除だけは手動で、何らかのエディターでやってやる必要があります。別に日付を取得して今日のデータだけフィルタリングしたりするのも造作も無いことですが、午前様になって昨日のデータを処理したり、数日間家計簿をサボったりするときを考えて、そこは手動にしています。

 別にカンマ区切りのデータぐらいsplitすれば…と思われるかもしれませんが、CSVは「引用符で囲まれたカンマはデータにする」とかいろいろワナがあるので、モジュールを使うのがラクチンです。