#! /usr/bin/env python
# -*- coding: Latin1 -*-
import re
import sys
import psycopg
from mx import DateTime
class ParserError(Exception):
"""
Ein 'ParserError' wird ausgelöst, falls während des Parsens einer
Logfile-Zeile ein Fehler auftritt.
"""
pass
SQL_STATEMENT = """
INSERT INTO statistics (
address, userid, datetime, method,
url, protocol, status, bytes,
referer, agent
) VALUES (
%(address)s, %(userid)s, '%(datetime)s', %(method)s,
%(url)s, %(protocol)s, %(status)s, %(bytes)s,
%(referer)s, %(agent)s
)
"""
class Logline:
"""
Ein 'Logline'-Objekt enthält die folgenden Attribute:
address (string) # IP/hostname
userid (string) # authentifizierte User-ID
datetime (DateTime) # Zeitstempel
method (string) # HTTP-Methode, z. B. GET
url (string) # URL
protocol (string) # z. B. HTTP/1.1
status (int) # z. B. 200 or 404
bytes (int) # übertragene Bytes (None, falls unbekannt)
referer (string) # Referer (None, falls unbekannt)
agent (string) # HTTP-Client
Diese Attribute werden im Konstruktor gesetzt, sofern nicht ein
'ParserError' ausgelöst wird.
"""
# regulärer Ausdruck, um eine Zeile des Logfiles zu parsen
_line_regex = re.compile(r"""
^
(?P
\S+)
\s
\S+ # ident ignorieren
\s
(?P\S+)
\s
\[
(?P
(?P\d\d) /
(?P\w{3}) /
(?P\d{4}) :
(?P\d\d) :
(?P\d\d) :
(?P\d\d)
\s
(?P[+-]\d\d)
00 # die letzten zwei Ziffern sollten stets 0 sein
)
\]
\s" # öffnendes Anführungszeichen
(?P\w+)
\s
(?P[^"]+)
\s
(?P[^"]+)
"\s # schließendes Anführungszeichen
(?P\d{3})
\s
(?P-|\d+) # kann für best. Requests "-" sein
# optional, nur im "combined"-Format, nicht im "common"-Format
(?: E
\s
"(?P[^"]+)" # Referer, in Anführungszeichen
\s
"(?P[^"]+)" # Client, in Anführungszeichen
)?
$
""", re.VERBOSE)
_month_numbers = {
"Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6,
"Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12}
def __init__(self, line):
"""
Parse eine Logfile-Zeile 'line' im "common"- oder "combined"-
Format und intialisiere diese Instanz entsprechend. Falls 'line'
nicht verarbeitet werden kann, erzeuge einen 'ParserError'.
"""
match = self._line_regex.search(line)
if not match:
raise ParserError("can't parse line: %s" % line)
groups = match.groupdict()
# wandle einige der Gruppen in Ganzzahlen
for name in ['day', 'year', 'hour', 'minute', 'second',
'timezone', 'status']:
# Umwandlung sollte wegen der "\d"s im reg. Ausdruck immer
# funktionieren
groups[name] = int(groups[name])
# kopiere das Dictionary 'groups' in diese Instanz
self.__dict__.update(groups)
self.month = self._month_numbers[self.month]
# erstelle Zeitstempel
try:
self.datetime = DateTime.DateTime(
self.year, self.month, self.day,
self.hour, self.minute, self.second)
except DateTime.RangeError:
raise ParserError("invalid datetime: %s" % self.datetime)
self.datetime += DateTime.DateTimeDelta(0, self.timezone)
# erstelle User-ID
if self.userid == '-':
self.userid = None
# erstelle 'bytes'
if self.bytes == '-':
self.bytes = None
else:
self.bytes = int(self.bytes)
# erstelle Referer
if self.referer == '-':
self.referer = None
def save(self, connection):
"""
Speichere die Daten aus dieser Instanz in der Datenbank.
Verwende dazu das Connection-Objekt 'connection'.
"""
cursor = connection.cursor()
cursor.execute(SQL_STATEMENT, self.__dict__)
connection.commit()
cursor.close()
def parse_and_save(filename):
"""
Parse das Logfile und speichere die enthaltenen Daten in der
Datenbank.
"""
logfile = open(filename)
connection = psycopg.connect("dbname=webstats user=schwa")
try:
for line in logfile:
try:
logline = Logline(line)
logline.save(connection)
except ParserError:
pass
finally:
connection.close()
parse_and_save(sys.argv[1])