lxmlでスクレイピングしてGoogleカレンダーに登録

タワレコ大好き、bonlifeです。とか言いつつ、タワレコのインストアイベント情報をチェックし忘れて、「行っときゃ良かった…orz」ってなることが多い今日この頃。(最近では、FREENOTEのインストアイベントに行き損ねたのが大ダメージ!ホントに大ダメージ!!)
ということで、id:claddvdさんの真似をしてGoogleカレンダーに登録するPythonスクリプトを書いてみました。参考にしたのは、このあたりです。

今回は BeautifulSoup じゃなくて lxml を使ってみました。ほら、やっぱり XPath とか便利じゃない!(って言ってることが前回とは大違い?) 注意していただきたいのは、Windows環境では、lxml は easy_install だと上手くインストールできないことがある点。ちょっと古めのパイパイ(ってカタカナで書くとアレですね)のページからWindows用のバイナリをダウンロードしてインストールしてくださいませ。今だったらこのあたりかしら。
では、早速、Pythonスクリプトです。ここに載ってるイベント情報から、必要な情報を抜き出すためのクラスを書きます。

  • TowerInstoreEvent.py
# -*- coding: cp932 -*- 

import re
import urllib2
from lxml import etree
from datetime import datetime

class TowerInstoreEvent(object):
    """TOWRER ECORDS instore event information"""
    def __init__(self):
        self.INSTORE_URL = 'http://www.towerrecords.jp/store/instore.html'
#        self.TARGET_STORE_LIST = [unicode('梅田NU茶屋町店','cp932'),
#                                  unicode('難波店','cp932'),
#                                  unicode('梅田大阪マルビル店','cp932')]
        self.TARGET_STORE_LIST = [unicode('難波店','cp932')]
        self.r_for_store = re.compile(r'store\d+\.html')
        self.r_for_date  = re.compile(r'(\d+?)/(\d+?)[^\d]')
        self.r_for_time  = re.compile(r'(\d+?):(\d{2})')
    def get_datetime(self,date_elem,time_elem):
        today = datetime.today()
        year = today.year
        m = self.r_for_date.match(date_elem.text)
        mm, dd = map(int,m.group(1,2))
        m = self.r_for_time.match(time_elem.text)
        if m != None:
            hh, mi = map(int,m.group(1,2))
        else:
            hh, mi = 0, 0
        d = datetime(year,mm,dd,hh,mi)
        if d < today:
            return datetime(year+1,mm,dd,hh,mi)
        else:
            return d
    def get_events(self):
        parser = etree.HTMLParser()
        et = etree.parse(urllib2.urlopen(self.INSTORE_URL),parser)
        ts = et.xpath('//tr/td/a')
        ts = [ e for e in ts if self.r_for_store.match(e.values()[0]) ]
        if self.TARGET_STORE_LIST:
            ts = [ e for e in ts if e.text in self.TARGET_STORE_LIST ]
        results = list()
        for e in ts:
            tr = e.getparent().getparent()
            date, time, title, type, self.store = tr.findall('td')
            results.append({'event_dt' : self.get_datetime(date,time),
                            'title'    : title.text,
                            'type'     : type.text,
                            'store'    : unicode('タワレコ','cp932') + e.text})
        return results

if __name__ == '__main__':
    te = TowerInstoreEvent()
    events = te.get_events()
    for i in events:
        print i.get('event_dt'),
        print i.get('title').replace('\n',' '),
        print i.get('type'), i.get('store')

TARGET_STORE_LISTに指定した店舗の情報のみ抽出します。単体で動かすと、こんな感じの出力になります。

C:\test\python\lxml>TowerInstoreEvent.py
2007-09-08 17:30:00 NON STYLE T&握手会 タワレコ難波店
2007-09-09 16:00:00 moumoon L&サイン会 タワレコ難波店
2007-09-17 16:00:00 #9 L&サイン会 タワレコ難波店

後は、id:claddvdさんの猿真似でOKですね。カレンダーのURL指定の部分は要注意。public/basic を private/full に書き換えるのを忘れて、403 forbidden を食らいました…。id:claddvdさんの説明にちゃんと書いてあるのにぃ。

  • tower_event_to_google_calendar.py
# -*- coding: cp932 -*-

import datetime
import gdata.calendar.service
import gdata.calendar
import atom
from TowerInstoreEvent import TowerInstoreEvent

# google account email address
google_email    = 'xxxxxxxx@gmail.com'
# google account password
google_password = 'xxxxxxxx'

# calendar
# cal_url = ''
cal_url = 'http://www.google.com/calendar/feeds/XXXXXXXXXXXXXXXXXXXXXXXXXX%40group.calendar.google.com/private/full'
# title prefix
title_prefix = unicode('[タワレコ] ','cp932')

def post(event_dt, title, store, type, calendar_url):
    """Post TOWER RECORDS instore event to Google Calendar"""

    if calendar_url == '':
        calendar_url = '/calendar/feeds/default/private/full'

    # considering timezone (JST -> UTC)
    dt_start = event_dt + datetime.timedelta(hours=-9)
    dt_end   = dt_start  + datetime.timedelta(hours=1)

    event = gdata.calendar.CalendarEventEntry()
    event.title = atom.Title(text=title_prefix+title)
    event.where.append(gdata.calendar.Where(value_string=store))
    event.content = atom.Content(text=type)
    event.when.append(gdata.calendar.When(start_time=dt_start.isoformat()+'.000Z',
                                          end_time=dt_end.isoformat()+'.000Z'))

    new_event = cal_client.InsertEvent(event, calendar_url)

# create gcalendar-service instance
cal_client = gdata.calendar.service.CalendarService()
cal_client.email = google_email
cal_client.password = google_password
cal_client.ProgrammaticLogin()

# create TowerInstoreEvent instance
te = TowerInstoreEvent()
tower_events = te.get_events()

for e in tower_events:
    post(e.get('event_dt'),e.get('title'),
         e.get('store'),e.get('type'),cal_url)
    print 'Registered : %s : %s (%s) @ %s' % (e.get('event_dt'),
                                              e.get('title'),
                                              e.get('type'),
                                              e.get('store'))

出力は以下のようになります。で、Google カレンダーにもちゃんと登録されています。当たり前のことなのに、なんとなく奇跡的!

C:\test\python\lxml>tower_event_to_google_calendar.py
Registered : 2007-09-08 17:30:00 : NON STYLE (T&握手会) @ タワレコ難波店
Registered : 2007-09-09 16:00:00 : moumoon (L&サイン会) @ タワレコ難波店
Registered : 2007-09-17 16:00:00 : #9 (L&サイン会) @ タワレコ難波店

注意点としては、datetime の isoformat() ですね。datetime がどんな情報を持っているのか、によって出力結果が違うので、要注意。(ミリ秒の情報の有無、tzinfoの有無によって表示が異なります。) 普通に strftime() 使うのが吉でしょう。後、時差の計算が面倒だったので固定で datetime.timedelta(hours=-9) を足してますが、pytz とか使ってちゃんと時差を扱った方がより汎用的になるのかもしれません。
ま、とにかく動いたのでオーケーオーイェーです! 2回スクリプトを動かすと、同じイベントが2個登録されちゃうけど、オーケーオーイエーです!(違)

p.s.

会社の proxy 越しにアクセスしようとすると socket.gaierror: (11001, 'getaddrinfo failed') とか出る罠。調べるの大変そうなので、そのあたりはまた後日。