UP | HOME

いまさらですがPythonを使ってみた

Table of Contents

1 どうして?

  • ログ解析
  • 何を使うか
    • Java + Groovy
      • 直接インストールしなければならない
    • Python
      • OSのパッケージで入っている。Windowsでも動く。バージョンの違いに注意
        • CentOS 5 -> 2.4.3
        • FreeBSD 7.2-RELEASE-p8 -> 2.5.2
        • Cygwin 1.7.7 -> 2.6.5
      • Google App Engineで採用
  • 勉強も兼ねて

2 ゴール

  • Apacheのログを読んで
  • ファイル Hoge.zip の一週間のダウンロード数を計測して
  • メールで送信する

3 書いてみる

3.1 Hello World

hello.py

print "Hello World"

実行結果

> python hello.py
Hello World  

3.2 if文

  • ブロックを始める前に":"、次の行はインデント

http://docs.python.org/tutorial/controlflow.html#if-statements

x = int(raw_input("Please enter an integer: "))
if x < 0:
     x = 0
     print 'Negative changed to zero'
elif x == 0:
     print 'Zero'
elif x == 1:
     print 'Single'
else:
     print 'More' 
  • None や 0 は 偽と判定
>>> if None:
...     print "true"
... else:
...     print "false"
...
false
>>> if 0:
...     print "true"
... else:
...     print "false"
...
false

3.3 ファイルの一覧を取る

import glob

list = glob.glob("/etc/httpd/logs/hoge_access.log*")

3.4 ファイルの判別

  • 読み込むファイルは hoge_access.loghoge_access.log.gz のどちらか
  • リストからファイル名を取り出して、その種類によって分岐する
    • ファイル名の識別には re モジュールを使う
for i in list:
    if re.match(".*\.gz", i):
        ...
    else:

3.5 ファイルを読み込む

  • 通常ファイルを読み込む場合
    • ビルドインの open() を使う
  • gzファイルを読み込む場合
    • gzip モジュールの open() を使う
  • IOErrorを返す可能性があるため、 tryexceptelse ブロックを使う
  • ファイルのクローズも忘れずに
  • "ret = []"は空のリストを作成します
ret = []

try:
    f = open(i)
except:
    print "failed to open ", i
else:
    for line in f:
#   lineに1行分の文字列が入る
        m = chooseHoge(analyze(line))
        if m:
            ret.append(m)
    f.close()

3.6 ログ1行の解析

  • その行が解析対象のファイルか確認する
    • 仮にHoge.zipとします

対象行

XXX.XXX.XXX.XXX - - [15/Aug/2010:10:41:55 +0900] "GET /download_step3.jsp?filename=Hoge.zip HTTP/1.1"
 200 140180790 "http://hoge.com/download.html" 
"Mozilla/4.0 (compatible; MSIE 7.0;Windows NT 5.1; GTB6.5; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"
  • 日付とファイル名を取り出して、dictに保存する
    • dictはキーと値を格納するためのデータ構造。"ret = {}" が dict
  • 関数として用意し、呼び出す
    • "def 関数名(引数リスト):"で始まり、次行はインデントする
  • re.match() は 完全一致、 re.search() は部分一致
    • 結果はマッチオブジェクト。 m.group(0) は全体、 m.group(1) は最初の括弧の内容を返す
  • time.strptime は、フォーマットされた日付文字列を time.struct_time 型にします
def analyze(line):
    ret = {}
    m = re.match("([0-9.]+) - - (\[.*\]) (\"[^\"]+\") ([0-9.]+) ([0-9.]+) (\"[^\"]+\") (\"[^\"]+\")", line)
    if m:
        ret["ipaddr"] = m.group(1)
# [15/Aug/2010:10:41:55 +0900]
        t = time.strptime(m.group(2), "[%d/%b/%Y:%H:%M:%S +0900]")
        ret["datetime"] = datetime(t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)

        ret["file"] = m.group(3)
        mm = re.search("Hoge\.zip", ret["file"])
        if mm:
            ret["file"] = mm.group(0)
        ret["browser"] = m.group(7)
    return ret

3.7 日付の扱い

  • datetime.datetime クラス や datetime.timedelta クラス を使います
    • datetime 日付と時刻を表します
    • timedelta 2つのdate、datetime、timeの差分を表現します
      • datetimeの場合は日単位
from datetime import datetime, timedelta

...

today = datetime.today()
startday = today - timedelta (today.weekday()) - timedelta(3)
lastday = startday + timedelta (7)
start = datetime(startday.year,
   startday.month, startday.day,0,0,0)
end = datetime(lastday.year,
   lastday.month, lastday.day,0,0,0)
  • 関数chooseHogeにて、指定した期間内のログであることを確認します
def chooseHoge(m):
    if not "datetime" in m:
        return None
    if m["datetime"] < start:
        return None
    if m["datetime"] > end:
        return None
    if not re.search("Hoge.zip", m["file"]):
        return None
    for robot in robots:
       if re.search(robot, m["browser"]):
           return None
    return m

3.8 メールメッセージの作成

  • string.Template クラス
    • substitute() は文字列内の"${key}"の部分を引数のdictからkeyに結び付く値を取り出して当てはめます
  • """…""" は改行を含める事ができます
  • u"""…"""はユニコードを意味します
def makeMessage(list, lendict):

    message = Template(u"""
お疲れ様です。

${start}から${end}までのダウンロード数は${num}です。
IPアドレス総数は${ipnum}です。

${liststr}
IPアドレス毎のダウンロード回数は以下の通りです。
(ダウンロード回数の多い順です。)

${ipliststr}
以上です。

""").substitute(start=str(start), end=str(end), num=str(len(list)),liststr=makeDownloadList(list),ipliststr=makeDownloadListParIP(lendict),ipnum=str(ipNum(lendict)))
    return message
  • メール送信時は本文をエンコードする必要があります
  • メールの情報(本文、To、Fromなど)は MIMEText オブジェクトに格納します
  • メールの送信は smtplib モジュールを使用します
def sendmail(message):

    enc = "ISO-2022-JP"

    msg = MIMEText(message.encode(enc),"plain",enc)

    msg["Subject"] = Header(u"Hoge ダウンロード数 " + str(start.date()) + " - " + str(end.date()), enc)
    msg["From"] = frm
    msg["To"] = to

    smtp = smtplib.SMTP("mail.xxxxxx.xx.jp")
    smtp.sendmail(frm, [to], msg.as_string())
    smtp.quit()

4 感想

  • ライブラリの名称がバージョン毎に微妙に違う
    • 2.4 email.MIMEText.MIMEText
    • 2.6.5 email.mime.text.MIMEText
  • Unixを考慮したライブラリが多い
    • gzファイルからの読込など
  • ドキュメントが整備されているので調べやすい

Author: hiroki <hiroki_at_ofug.net>

Date: 2010-09-25 土

HTML generated by org-mode 6.33x in emacs 23