summer_tree_home

Check iOでPython3をマスターするぜっ

Web log sessions (HubSpot) - アクセスログ解析

どんな問題?

Web log sessions
http://www.checkio.org/mission/web-log-sessions/

Webサイトのアクセスログから、セッション(Session)データを作成せよ。

引数のアクセスログは、以下の形式。タイムスタンプのフォーマットは、YYYY-MM-DD-hh-mm-ss となる。

タイムスタンプ;;ユーザー名;;URL

ログファイルを解析して、ユーザー名とURLのセカンドレベルドメインが同じで、かつ30分以内のアクセスは同じセッションとしてまとめる。

セッションデータを以下の形式で作成して、戻り値として返す。

ユーザー名;;サイト(セカンドレベルドメイン);;セッション時間(秒数);;リクエスト数

ユーザー名、サイト、セッション時間、リクエスト数の順でソートすること。

例題:

checkio("""2013-01-01-01-00-00;;Name;;http://checkio.org/task
2013-01-01-01-02-00;;name;;http://checkio.org/task2
2013-01-01-01-31-00;;Name;;https://admin.checkio.org
2013-01-01-03-00-00;;Name;;http://www.checkio.org/profile
2013-01-01-03-00-01;;Name;;http://example.com
2013-02-03-04-00-00;;user2;;http://checkio.org/task
2013-01-01-03-11-00;;Name;;http://checkio.org/task""") ==
"""name;;checkio.org;;661;;2
name;;checkio.org;;1861;;3
name;;example.com;;1;;1
user2;;checkio.org;;1;;1"""

どうやって解く?

めずらしく実用的な問題だなぁ。

まず、URLからセカンドレベルドメインを取得するために、正規表現を使おうと思ったのだが、うまくできなかった。しかたないので、まずURLからhostを取得して、そこからセカンドレベルまでを取得することにした。

host = re.match('.+?://(.+?)(?:[:/].*)?$', url).group(1)  # URLからhostを取得
domain = '.'.join(host.split('.')[-2:])  # セカンドレベルドメインを取得


今回は、ログとセッションのクラスを作成した。CheckiOでクラスを使うのは初めてかも。

import re
from datetime import datetime, timedelta
 
 
class LogItem():
    def __init__(self, log_line):
        timestamp, name, url = log_line.split(';;')
        host = re.match('.+?://(.+?)(?:[:/].*)?$', url).group(1)
        self.name = name.lower()
        self.domain = '.'.join(host.split('.')[-2:])
        self.time = datetime.strptime(timestamp, '%Y-%m-%d-%H-%M-%S')
 
    def __lt__(self, other):
        get_key = lambda i: (i.name, i.domain)
        return get_key(self) < get_key(other)
 
 
class Session():
    def __init__(self, item):
        self.name = item.name
        self.domain = item.domain
        self.first = item.time
        self.last = item.time
        self.count = 1
 
    def add(self, item):
        self.last = item.time
        self.count += 1
 
    def is_same_session(self, item):
        return (item.name, item.domain) == (self.name, self.domain) \
            and item.time - self.last < timedelta(minutes=30)
 
    @property
    def seconds(self):
        return round((self.last - self.first).total_seconds()) + 1
 
    def __lt__(self, other):
        get_key = lambda i: (i.name, i.domain, i.seconds, i.count)
        return get_key(self) < get_key(other)
 
    def __str__(self):
        return '{};;{};;{};;{}'.format(self.name, self.domain, self.seconds, self.count)
 
 
def checkio(log_text):
    # LogItemのリストを作成してソート
    log_items = sorted([LogItem(line) for line in log_text.splitlines()])

    # Sessionのリストを作成していく
    sessions = []
    for item in log_items:
        if sessions and sessions[-1].is_same_session(item):
            # 最後のSessionと同じセッションなら、更新
            sessions[-1].add(item)
        else:
            # 初回や異なるセッションなら、新しいSessionを作成
            sessions.append(Session(item))

    # sessionsをソートして文字列化
    return '\n'.join(str(session) for session in sorted(sessions))

http://www.checkio.org/mission/web-log-sessions/publications/natsuki/python-3/first/

LogItem、Sessionクラスともに、sortedに対応するために __lt__ を定義している。


いま見直すと、アクセスログの並びが時間順でなくてもいいように、LogItemの__lt__で、get_keyにi.timeも追加した方がよかったかも。

    def __lt__(self, other):
        get_key = lambda i: (i.name, i.domain, i.time)
        return get_key(self) < get_key(other)