2016/02/18(木)Google Calendar のデータをXMLに加工して monthly.js に 流し込む

所属している草野球チームのHP制作を担当していて、「スケジュールを載せたい」という要望が来ました。数週間ぐらい前に「そういえば何かカレンダーをキレイに表示させるJSライブラリがあったよなぁ」と「monthly.js」思い出して、Googleカレンダーとこれを使って表示させることにしました。

練習場所まで全世界に公開するのも気持ちが悪いので、Googleのカレンダーは「非公開」の設定のままで、どうにか情報を取得するすべはないものかと調べると、OAuth2なGoogle Calendar API というAPIを使えばいいと載っていました。( OAuth2じゃないAPIもあるっぽいですが、実装が複雑になるのでOAuth2の方式がいいよみたいな情報が公式ドキュメントに書かれていた。うろ覚え。)

しかし、肝心のPerlの解説がない。(仕事ではRubyがメインですが、草野球のホームページぐらいの規模ならPerlで書きたい。)ライブラリ自体は「Google::API::Client」という名前でCPANに登録されているのでこれを使うことにしました。

client_secret.json みたいなファイルをGoogle APIのページから取得すると(詳細は「celient_secret.json google」でググッて)あとは、以下のコードで、初回時のみ手動で認証してリフレッシュトークンを取得すると、それ以降はよしなにしてくれます。(ずっとアクセスを待ち受ける必要があるWebアプリにも組み込めます。)

#!/usr/bin/env perl

use strict;
use warnings;
use utf8;
use feature qw/say/;
use open qw/:encoding(utf-8) :std/;
use JSON qw//;
use Time::Moment qw//;
use Google::API::Client;
use Google::API::OAuth2::Client;
use XML::Simple ();
use Cache::FileCache;
use DDP; # cpanm Data::Printer してね

my $myemail = 'xxx@gmail.com';
my $client_secrets_file = 'client_secret.json';
my $client = Google::API::Client->new;
my $gcal = $client->build('calendar', 'v3');
my $auth_driver = Google::API::OAuth2::Client->new_from_client_secrets($client_secrets_file, $gcal->{auth_doc});

my $dat_file = 'token.dat';
get_or_restore_token($dat_file, $auth_driver);

my $my_calender_id      = 'primary';
my $holiday_calendar_id = 'ja.japanese#holiday@group.v.calendar.google.com';

my $cache = Cache::FileCache->new({
    cache_root => '/tmp',
    namespace  => 'calendar',
});

my %cache_duration_of = (
    $my_calender_id      => '5s',
    $holiday_calendar_id => '7d',
);

my $now = Time::Moment->now;

my $first_day_of_month = Time::Moment->new(
    year   => $now->year,
    month  => $now->month,
    day    => 1,
    offset => 540, # +09:00(540分)
);

my $req_body = {
    timeMax => $first_day_of_month->plus_months(6)->to_string,
    timeMin => $first_day_of_month->minus_months(6)->to_string,
};

my $schedule = get_or_cache_event_items($my_calender_id);
my $holidays = get_or_cache_event_items($holiday_calendar_id);

my @events = (@{$schedule->{items}}, @{$holidays->{items}});

print make_xml(\@events);

exit;

sub get_or_restore_token
{
    my ($file, $auth_driver) = @_;

    if (-f $file)
    {
        open (my $fh, '<', $file) or die $!;
        local $/;
        my $access_token = JSON::from_json(<$fh>);
        close($fh);

        $auth_driver->token_obj($access_token);
    }
    else
    {
        my $auth_url = $auth_driver->authorize_uri;
        say 'Go to the following link in your browser:';
        say $auth_url;

        say 'Enter verification code:';
        chomp(my $code = <>);
        $auth_driver->exchange($code);
        store_token($file, $auth_driver);
    }
}

sub store_token
{
    my ($file, $auth_driver) = @_;

    my $access_token = $auth_driver->token_obj;

    open (my $fh, '>', $file) or die $!;
    print {$fh} JSON::to_json($access_token);
    close($fh);
}

sub get_or_cache_event_items
{
    my $calendar_id = shift;
    my $items = $cache->get($calendar_id);

    if ( ! $items )
    {
        $items = $gcal->events->list(calendarId => $calendar_id, body => $req_body)->execute({ auth_driver => $auth_driver });
        $cache->set($calendar_id => $items, $cache_duration_of{$calendar_id}) if $items;
    }

    return $items;
}

sub get_date_using_datetime { local $_ = $_[0]; s/(.+)T.+/$1/; $_; }
sub get_time_using_datetime { local $_ = $_[0]; s/.+T([0-9][0-9]:[0-9][0-9]):[0-9][0-9]\+.+/$1/; $_; }

sub make_xml
{
    my $items = shift;

    my $data = {};
    $data->{event} = ();

    my $i = 0;

    for my $item (@{$items})
    {
        my $event = {};

        my $startdate = $item->{start}{date} ? $item->{start}{date} : get_date_using_datetime($item->{start}{dateTime});
        my $enddate   = $item->{end}{date}   ? $item->{end}{date}   : get_date_using_datetime($item->{end}{dateTime});

        $event->{id}        = ++$i;
        $event->{name}      = $item->{summary};
        $event->{startdate} = $startdate;
        $event->{enddate}   = $enddate;

        $event->{color} = '#ffb128';
        $event->{color} = '#ddeeff' if $item->{organizer}{email} ne $myemail;

        $event->{starttime} = get_time_using_datetime($item->{start}{dateTime}) if $item->{start}{dateTime};
        $event->{endtime}   = get_time_using_datetime($item->{end}{dateTime})   if $item->{end}{dateTime};

        push(@{$data->{event}}, $event);
    }

    return XML::Simple::XMLout($data, RootName => 'monthly', XMLDecl => '<?xml version="1.0"?>', NoAttr => 1);
}

以下のようにXMLが出力されます。

<?xml version="1.0"?>
<monthly>
  <event>
    <name>練習(雨天中止)</name>
    <color>#ffb128</color>
    <enddate>2016-02-14</enddate>
    <endtime>15:30</endtime>
    <id>1</id>
    <startdate>2016-02-14</startdate>
    <starttime>11:00</starttime>
  </event>
  <event>
    <name>試合!</name>
    <color>#ffb128</color>
    <enddate>2016-03-26</enddate>
    <endtime>16:00</endtime>
    <id>2</id>
    <startdate>2016-03-26</startdate>
    <starttime>14:00</starttime>
  </event>
  <event>
    <name>グランド練習</name>
    <color>#ffb128</color>
    <enddate>2016-02-20</enddate>
    <endtime>16:00</endtime>
    <id>3</id>
    <startdate>2016-02-20</startdate>
    <starttime>14:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-02-28</enddate>
    <endtime>15:00</endtime>
    <id>4</id>
    <startdate>2016-02-28</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-03-06</enddate>
    <endtime>15:00</endtime>
    <id>5</id>
    <startdate>2016-03-06</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-03-13</enddate>
    <endtime>15:00</endtime>
    <id>6</id>
    <startdate>2016-03-13</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-04-03</enddate>
    <endtime>15:00</endtime>
    <id>7</id>
    <startdate>2016-04-03</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-03-20</enddate>
    <endtime>15:00</endtime>
    <id>8</id>
    <startdate>2016-03-20</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>敬老の日</name>
    <color>#ddeeff</color>
    <enddate>2015-09-22</enddate>
    <id>9</id>
    <startdate>2015-09-21</startdate>
  </event>
  <event>
    <name>国民の休日</name>
    <color>#ddeeff</color>
    <enddate>2015-09-23</enddate>
    <id>10</id>
    <startdate>2015-09-22</startdate>
  </event>
  <event>
    <name>秋分の日</name>
    <color>#ddeeff</color>
    <enddate>2015-09-24</enddate>
    <id>11</id>
    <startdate>2015-09-23</startdate>
  </event>
  <event>
    <name>文化の日</name>
    <color>#ddeeff</color>
    <enddate>2015-11-04</enddate>
    <id>12</id>
    <startdate>2015-11-03</startdate>
  </event>
  <event>
    <name>元日</name>
    <color>#ddeeff</color>
    <enddate>2016-01-02</enddate>
    <id>13</id>
    <startdate>2016-01-01</startdate>
  </event>
  <event>
    <name>昭和の日</name>
    <color>#ddeeff</color>
    <enddate>2016-04-30</enddate>
    <id>14</id>
    <startdate>2016-04-29</startdate>
  </event>
  <event>
    <name>憲法記念日</name>
    <color>#ddeeff</color>
    <enddate>2016-05-04</enddate>
    <id>15</id>
    <startdate>2016-05-03</startdate>
  </event>
  <event>
    <name>体育の日</name>
    <color>#ddeeff</color>
    <enddate>2015-10-13</enddate>
    <id>16</id>
    <startdate>2015-10-12</startdate>
  </event>
  <event>
    <name>みどりの日</name>
    <color>#ddeeff</color>
    <enddate>2016-05-05</enddate>
    <id>17</id>
    <startdate>2016-05-04</startdate>
  </event>
  <event>
    <name>海の日</name>
    <color>#ddeeff</color>
    <enddate>2016-07-19</enddate>
    <id>18</id>
    <startdate>2016-07-18</startdate>
  </event>
  <event>
    <name>勤労感謝の日</name>
    <color>#ddeeff</color>
    <enddate>2015-11-24</enddate>
    <id>19</id>
    <startdate>2015-11-23</startdate>
  </event>
  <event>
    <name>天皇誕生日</name>
    <color>#ddeeff</color>
    <enddate>2015-12-24</enddate>
    <id>20</id>
    <startdate>2015-12-23</startdate>
  </event>
  <event>
    <name>春分の日</name>
    <color>#ddeeff</color>
    <enddate>2016-03-21</enddate>
    <id>21</id>
    <startdate>2016-03-20</startdate>
  </event>
  <event>
    <name>春分の日 振替休日</name>
    <color>#ddeeff</color>
    <enddate>2016-03-22</enddate>
    <id>22</id>
    <startdate>2016-03-21</startdate>
  </event>
  <event>
    <name>こどもの日</name>
    <color>#ddeeff</color>
    <enddate>2016-05-06</enddate>
    <id>23</id>
    <startdate>2016-05-05</startdate>
  </event>
  <event>
    <name>成人の日</name>
    <color>#ddeeff</color>
    <enddate>2016-01-12</enddate>
    <id>24</id>
    <startdate>2016-01-11</startdate>
  </event>
  <event>
    <name>建国記念の日</name>
    <color>#ddeeff</color>>
    <enddate>2016-02-12</enddate>
    <id>25</id>
    <startdate>2016-02-11</startdate>
  </event>
</monthly>

Googleカレンダーが終了時刻それ自体はイベントの期間に含めない(不等号に「=(イコール)」が付くかどうかの違いのようなもの) のに対して、monthly.js は終了時刻それ自体をイベントの期間に含めるという違いがあるため、endtime(enddateじゃなくて)がない場合は、enddate を一日早めるという面倒な処理が追加で必要になります。(上記のコードではそれが追加されていないので注意。)

上記の面倒な処理を終えて「monthly.js」のマニュアルを読んでWebアプリに組み込むと、以下のように美しく表示できるようになります。ちなみに、高速で「月」を切り替えるとイベントが移動したり増殖したりする「monthly.js」のバグを作り終えてから発見しました。(ぎりぎりクリティカルなバグでもないかというあんばいですが…)

スクリーンショット 2016-02-18 0.34.09

スクリーンショット 2016-02-18 0.34.24

お疲れ様です。( ´Д`)=3

2015/01/18(日)Python勉強メモ#5「homura と madoka で形態素の頻度を数えよ」

あらかじめ「pip」とかで「homura」と「madoka」を入れておきましょう。

まずはダウンローダーの「homura」で形態素解析済みの青空文庫のデータをダウンロード。

my-mbp% ipython
Python 3.4.2 (default, Jan 12 2015, 11:46:28) 
Type "copyright", "credits" or "license" for more information.

IPython 2.3.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: from homura import download

In [2]: download('http://aozora-word.hahasoha.net/utf8/newnew.csv.gz')
   100%    615.8 MiB      11.1 MiB/s            0:00:00 ETA

適当に解凍したら「madoka」(Pythonの辞書より省メモリかもしれないデータ構造を提供するライブラリ)で形態素をカウントです。(Madokaの詳細: http://s-yata.github.io/madoka/index.ja.html

#!/usr/bin/env python

import madoka

sketch = madoka.Sketch()

with open('newnew.csv', 'r') as f:
    for line in f:
        morpheme = line.split(',')[3]
        sketch[morpheme] += 1
        print(morpheme)

print(sketch['人'])
print(sketch['面倒'])
print(sketch['蕩尽'])

出力された頻度(推定値)は以下のようになりました。

1418808
57427
1910

実際の頻度は以下の通り。

#!/usr/bin/env python

from collections import Counter

cnt_of = Counter()

with open('newnew.csv', 'r') as f:
    for line in f:
        morpheme = line.split(',')[3]
        cnt_of[morpheme] += 1
        print(morpheme)

print(cnt_of['人'])
print(cnt_of['面倒'])
print(cnt_of['蕩尽'])
217837
1738
23

2015/01/16(金)Python勉強メモ#4「ベンチマークをとれ」

PyPIにある「Benchmarker」を利用。コードはひらがなリストを作る処理です。リスト内包表記が最速でした。

#!/usr/bin/env python

from benchmarker import Benchmarker

with Benchmarker(1000*100, width=20) as bench:
    code_point = range(0x3040, 0x30A0)

    @bench('1')
    def _(bm):
        for i in bm:
            moji_list = []
            for cp in code_point: moji_list.append( chr(cp) )

    @bench('2')
    def _bm(bm):
        for i in bm:
            moji_list = [ chr(cp) for cp in code_point ]

    @bench('3')
    def _bm(bm):
        for i in bm:
            moji_list = list(chr(cp) for cp in code_point)
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.2
## python compiler:     GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)
## python platform:     Darwin-14.0.0-x86_64-i386-64bit
## python executable:   /Users/username/.anyenv/envs/pyenv/versions/3.4.2/bin/python
## cpu model:           Intel(R) Core(TM) i5-4258U CPU @ 2.40GHz 
## parameters:          loop=100000, cycle=1, extra=0

##                        real    (total    = user    + sys)
1                       3.1367    3.0500    3.0200    0.0300
2                       2.4863    2.4300    2.4200    0.0100
3                       2.8027    2.7300    2.7100    0.0200

## Ranking                real
2                       2.4863  (100.0) ********************
3                       2.8027  ( 88.7) ******************
1                       3.1367  ( 79.3) ****************

## Matrix                 real    [01]    [02]    [03]
[01] 2                  2.4863   100.0   112.7   126.2
[02] 3                  2.8027    88.7   100.0   111.9
[03] 1                  3.1367    79.3    89.3   100.0

2015/01/15(木)Python勉強メモ#3「Unicodeブロック:Hiragana のコードポイントとその文字の順序付き辞書を作り、すべて出力せよ」

ワイの書いたコード↓

#!/usr/bin/env python

from collections import OrderedDict

code_point_list = range(0x3040, 0x30A0)
chara_list      = [ chr(code_point) for code_point in code_point_list ]

moji_dict = OrderedDict(); # 登録順序を記憶
moji_dict.update([ tuple([ code_point, chara ]) for code_point, chara in zip(code_point_list, chara_list) ])
moji_dict.update({ 0x303F: chr(0x3020) }) # 順序が保持されているか確認用

for key, value in moji_dict.items():
    print('U+{0:X} {1}'.format(key, value))
U+3040 ぀
U+3041 ぁ
U+3042 あ
U+3043 ぃ
U+3044 い
U+3045 ぅ
U+3046 う
U+3047 ぇ
U+3048 え
U+3049 ぉ
U+304A お
U+304B か
U+304C が
U+304D き
U+304E ぎ
U+304F く
U+3050 ぐ
U+3051 け
U+3052 げ
U+3053 こ
U+3054 ご
U+3055 さ
U+3056 ざ
U+3057 し
U+3058 じ
U+3059 す
U+305A ず
U+305B せ
U+305C ぜ
U+305D そ
U+305E ぞ
U+305F た
U+3060 だ
U+3061 ち
U+3062 ぢ
U+3063 っ
U+3064 つ
U+3065 づ
U+3066 て
U+3067 で
U+3068 と
U+3069 ど
U+306A な
U+306B に
U+306C ぬ
U+306D ね
U+306E の
U+306F は
U+3070 ば
U+3071 ぱ
U+3072 ひ
U+3073 び
U+3074 ぴ
U+3075 ふ
U+3076 ぶ
U+3077 ぷ
U+3078 へ
U+3079 べ
U+307A ぺ
U+307B ほ
U+307C ぼ
U+307D ぽ
U+307E ま
U+307F み
U+3080 む
U+3081 め
U+3082 も
U+3083 ゃ
U+3084 や
U+3085 ゅ
U+3086 ゆ
U+3087 ょ
U+3088 よ
U+3089 ら
U+308A り
U+308B る
U+308C れ
U+308D ろ
U+308E ゎ
U+308F わ
U+3090 ゐ
U+3091 ゑ
U+3092 を
U+3093 ん
U+3094 ゔ
U+3095 ゕ
U+3096 ゖ
U+3097 ゗
U+3098 ゘
U+3099 ゙
U+309A ゚
U+309B ゛
U+309C ゜
U+309D ゝ
U+309E ゞ
U+309F ゟ
U+303F 〠

2015/01/15(木)Python勉強メモ#2「Unicodeブロック:Hiragana の文字をすべて出力せよ」

#!/usr/bin/env python
for cp in range(0x3040, 0x30A0): print(chr(cp), end=' ')

出力:

぀ ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゕ ゖ ゗ ゘ ゙ ゚ ゛ ゜ ゝ ゞ ゟ

ちなみにPerlで書くと↓のようになります。

#!/usr/bin/env perl
use open qw/:utf8 :std/;
print chr . ' ' for 0x3040 .. 0x309F;

16進数を「0x」で表現できる点はPythonのほうが好きです。

Perlでも「0x」使えました。(汗)

追記: Python版はリスト内包表記のほうがいいですかね。

#!/usr/bin/env python
 [ print(chr(cp), end=' ') for cp in range(0x3040, 0x30A0) ]

これでもいけますね。

#!/usr/bin/env python
print(' '.join(chr(cp) for cp in range(0x3040, 0x30A0)), end='')
OK キャンセル 確認 その他