さて、前回の続き、今回もPerlによる超手抜き家計簿の実装法を紹介する。



前回は、

 2012-03-12 09:00「残高設定 5000円」
 2012-03-12 12:00「ココイチ カレー 900円」
 2012-03-12 10:00「ドトール コーヒー 150円」
 2012-03-12 13:00「使途不明 100円」

というツイート列を、

 日付    時刻  店舗   品目   金額  残高
 2012-03-12 13:00 ?    -     100円 3850円
 2012-03-12 12:00 ココイチ カレー   900円 3950円
 2012-03-12 10:00 ドトール コーヒー  150円 4850円
 2012-03-12 09:00 残高設定 -    5000円 5000円

という家計簿に組み立てる仕様を検討した。

今回はこの仕様を実装する。
ではここで、プログラムをドバーンと公開してみる。
前回との違いは
 ・ツイートを読み込んで家計簿形式に組み立てている
 ・美しい表組みにしている
 ・メールで送り返している
というところである。
#!/usr/bin/perl -w
# mailHTML -- ツイッター家計簿アルファ版

use strict;
use Encode qw(from_to encode);
use Mail::Mailer;

use lib '/home/????????/local/lib';
use lib '/home/????????/local/lib/perl5';
use lib '/home/????????/local/lib/perl5/site_perl';

use Net::Twitter;
use utf8;
use DateTime::Format::Strptime;
use HTML::Template;

my $handle = Net::Twitter->new({
traits => [qw/OAuth API::REST API::Search/],
consumer_key => "????????",
consumer_secret  => "????????",
access_token  => "????????",
access_token_secret => "????????",
});

my $dt = DateTime->now( time_zone => 'Asia/Tokyo' );
$dt->subtract( days => 1 );

my $screen_name = '????????'; # アカウントのscreen name
my $statuses =
$handle->user_timeline({ id => $screen_name, count => 200, since => $dt });

my @out = ();

for my $status ( @$statuses ) {
my ($date, $time, $shop, $item, $cash);
my @tw = split / /, $status->{text};
for (@tw) {
 if (/(\d\d\d\d-\d\d-\d\d)/) {
  $date = $1;
 } elsif (/(\d\d:\d\d)/) {
  $time = $1.':00';
 } elsif (/(\d+)円$/) {
  $cash = $1;
 } elsif (defined $shop) {
  $item = $_;
 } else {
  $shop = $_;
 }
}

next unless defined $cash;

my ($nowdate, $nowtime) = &tzChange($status->{created_at});
$date = $nowdate unless defined $date;
$time = $nowtime unless defined $time;

$item = "-" unless defined $item;
$shop = "?" unless defined $shop;

push @out, { date=> $date, time => $time, shop => $shop, item => $item, cash => $cash };
}

my $zan;
my @sort;
for my $out (sort {$a->{date}.$a->{time} cmp $b->{date}.$b->{time}} @out) {
if ($out->{shop} eq '残高設定') {
 $zan = $out->{cash};
} elsif (defined $zan) {
 $zan -= $out->{cash};
} else {
 next;
}
$out->{hand} = $zan;
push @sort, $out;
}

@sort = reverse @sort;

my $tmpl_file = "kakeibo.tmpl";

my $tmpl;
open (my $template, "<:utf8", $tmpl_file );
$tmpl = HTML::Template->new(filehandle => *$template);
close $template;

$tmpl->param( { Table => \@sort } );

my $from = 'from@example.com';
my $to = '?????@example.com';
my $subject = "家計簿";

my $mailer = Mail::Mailer->new("sendmail");
$mailer->open({ From => $from,
  To => $to,
  Subject => $subject,
  'MIME-Version' => '1.0',
  'Content-Type' => "text/html; charset=UTF-8",
  'Content-Transfer-Encoding' => '8bit',
}) or die "Can't open $!\n";

print $mailer $tmpl->output;
$mailer->close();

sub tzChange ($) {
my $utc = shift;

#Sat Feb 11 03:54:53 +0000 2012
my $strp = DateTime::Format::Strptime->new(
 pattern => '%a %b %d %H:%M:%S %z %Y'
);

my $dt = $strp->parse_datetime($utc);
$dt->set_time_zone('Asia/Tokyo');

return ($dt->strftime("%Y-%m-%d"), $dt->strftime("%H:%M:%S"));
}
では部分ごとに順を追って紹介する。
use strict;
use Encode qw(from_to encode);
use Mail::Mailer;

use lib '/home/????????/local/lib';
use lib '/home/????????/local/lib/perl5';
use lib '/home/????????/local/lib/perl5/site_perl';

use Net::Twitter;
use utf8;
use DateTime::Format::Strptime;
use HTML::Template;
まず必要なモジュールをuseする。

Encodeは、日本語コードの変換に使うモジュールだ。
Mail::Mailerはメールを送信するのに使うモジュールである。

DateTime::Format::Strptimeは日付の計算に使う。
今回はTwitterの標準であるUTC(協定世界時)と日本時間の変換、および「2日前からのツイートを取得」という計算に使う。

HTML::TemplateはHTMLをPerlで簡単に生成するのに使う。
特に表組などの繰り返し要素を使うには必携だ。
my $handle = Net::Twitter->new({
traits => [qw/OAuth API::REST API::Search/],
consumer_key => "????????",
consumer_secret  => "????????",
access_token  => "????????",
access_token_secret => "????????",
});
my $dt = DateTime->now( time_zone => 'Asia/Tokyo' );
$dt->subtract( days => 1 );

my $screen_name = '????????'; # アカウントのscreen name
my $statuses =
$handle->user_timeline({ id => $screen_name, count => 200, since => $dt });
ここまではすでに紹介した。
過去1日分のツイートについて$statusesというリファレンスに取得している。
これは配列リファレンスで、各要素には1ツイートごとのハッシュリファレンスが入っている。
特に難しいことを言っていないのだが、わけが分からない方は拙著「すぐわかるオブジェクト指向Perl」をお読みください。
(ステマかよ!)


my @out = ();
@outは各ツイートごとによって変化する家計の状況を入れる配列である。
配列は空リスト()で初期化する。
for my $status ( @$statuses ) {
これは、$statusesが指し示す無名配列をデリファレンスした配列@@$statusesの各要素を、$statusに入れて周回するループである。
foreachとも書けるが通っぽくはforと書く。
my ($date, $time, $shop, $item, $cash);
ここで各明細の日付、時刻、店舗、品目、使用金額を入れる変数を作る。
my @tw = split / /, $status->{text};
さて、$statusは各ツイートの状態を表す無名ハッシュリファレンスであるが、デリファレンスして各要素を取り出す。
ハッシュキーがtextの$status->{text}にはツイートしたテキストが入っているが、今回はスペース区切りで日付、金額、店舗、品目を与えることになっているので、スペースでsplitする。
splitした結果を配列@twに入れる。
for (@tw) {
また配列@twに関してfor(foreach)する。
各配列要素は変数を省略すると例のアレ$_に入る。
例のアレが分からない人は拙著「すぐわかるPerl」を読めばいいのではないだろうか、
(ああステマ・・・)



ここからは@twの各要素について、ある条件を満たしていればある明細が決まる、という処理である。
 if (/(\d\d\d\d-\d\d-\d\d)/) {
  $date = $1;
要素が2012-03-19のように、数字4桁-2桁-2桁になっていれば日付と考えられる。
よって$dateを埋める。
 } elsif (/(\d\d:\d\d)/) {
  $time = $1.':00';
同様に数字2桁:2桁にあんっていれば時刻と考えられるので$dateを埋める。
入力は時:分で入れて、秒は00固定とする。
 } elsif (/(\d+)円$/) {
  $cash = $1;
「数字+円」の形式の文字列であったら、それを$cashに入れている。
 } elsif (defined $shop) {
  $item = $_;
日付、時刻、金額以外の文字列は店舗か品目になる。
もしすでに店舗が決まっていれば、文字列は品目になるので$itemに入れる。
 } else {
  $shop = $_;
ここに到達するのは、何らかの文字列が$_にあるが、日付でも、時刻でも、金額でもなく、変数$shopがまだ決まっていない場合である。
これは店舗であるので、$shopに入れる。
 }
}
ここで1ツイートの解釈が終わった。
next unless defined $cash;
「円」という文字列が入っていないツイートは金額が決まらない。
それは家計簿の明細ではないとみなして、次のツイートまで飛ばしている。
my ($nowdate, $nowtime) = &tzChange($status->{created_at});
さて、$status->{created_at}というハッシュ要素には、ツイートされた時刻が入っている。
これを、日本時間の日付、時刻に変換するサブルーチンtzChangeを読んで、戻り値を$nowdate、$nowtimeに入れている。
なうの日付、時刻である。

ちょっと下のほうに移ってサブルーチンの中身を研究する。
sub tzChange ($$) {
my $utc = shift;
サブルーチンtzChangeはスカラー変数1個を受ける。
引数(ツイートのcreated_at要素、つまり現在時刻)を変数$utcに入れてある。
#Sat Feb 11 03:54:53 +0000 2012
my $strp = DateTime::Format::Strptime->new(
 pattern => '%a %b %d %H:%M:%S %z %Y'
);

my $dt = $strp->parse_datetime($utc);
まずDateTime::Format::Strptimeオブジェクトを作って$strpに入れる。
これは日付を解釈するテンプレートのようなものだ。
次に、このテンプレートにしたがって$utcを変換して$dtに入れる。
$dtは日付オブジェクトである。
$dt->set_time_zone('Asia/Tokyo');
これで$dtのタイムゾーンを日本にする。
return ($dt->strftime("%Y-%m-%d"), $dt->strftime("%H:%M:%S"));
最後は日付を「YYYY-MM-DD」形式、時刻を「HH:MM:SS」形式にしてサブルーチンを抜ける。

メインルーチンに戻る。
$date = $nowdate unless defined $date;
$time = $nowtime unless defined $time;
これは、もし日付$dateや時刻$timeから手動で与えられていなければ(それらの変数はundefであるので)ツイートの日付、時刻に合わせるということだ。
これで日付、時刻を省略可能にする。
ほとんど買い物した直後に起票するので、省略値を与えたのである。
$item = "-" unless defined $item;
$shop = "?" unless defined $shop;
同様に、品目$itemが決まっていなければ「-」を、店舗$shopが決まっていなければ「?」をセットする。
push @out, { date=> $date, time => $time, shop => $shop, item => $item, cash => $cash };
ここで@outという配列には、日付、時刻、店舗、品目、金額を入れた無名ハッシュを配列要素として追加する。
}
これで全ツイートの処理が終わった。

いま、配列@outには、

 2012-03-12 09:00「残高設定 5000円」
 2012-03-12 12:00「ココイチ カレー 900円」
 2012-03-12 10:00「ドトール コーヒー 150円」
 2012-03-12 13:00「使途不明 100円」

というツイートを受けて、

 2012-03-12 09:00 残高設定 -    5000
 2012-03-12 12:00 ココイチ カレー   900
 2012-03-12 10:00 ドトール コーヒー  150
 2012-03-12 13:00 ?    -     100

というハッシュ配列が入っているはずだ。
すみませんここで一週間休んでいいですか。

Subscribe with livedoor Reader
Add to Google
RSS
このエントリーをはてなブックマークに追加