Logfile in eine MSSQL Datenbank einlesen

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Hallo zusammen,

ich möchte ein Skript schreiben, welche in der Endausbaustufe alle 5 Minuten ein Logfile ausliest und die Werte in eine Datenbank schreibt. Dabei sind doppelte Einträge zu vermeiden.

Die Datenbank soll für spätere Auswertungszwecke eine MSSQL-Datenbank sein.

Aktuell läuft das ganze auf einem Debian Stretch mit FreeTDS. Die Datenbank läuft ebenfalls auf dem Debian.

Die Datenbank besitzt bislang 3 Tabellen
1. "main" --> Sammlung der Daten der erfolgreichen Kommunikationen.
2. "error" --> Sammlung der Daten der fehlerbehafteten Kommunikation.
3. "transmitter" --> Zusammenstellung der Gerätespezifischen Daten.

Bislang habe ich es geschafft, die relevanten Daten, aus dem Logfile zu extrahieren und in die DB zu schreiben.

Nun tue ich mich gerade schwer die DB, vor dem Schreibvorgang abzufragen, um doppelte Einträge zu vermeiden.
Leider habe ich zu meiner Konstellation nicht allzu viel Dokumentationen gefunden und wäre deshalb für Hilfe jeglicher Art dankbar.

Code: Alles auswählen

#!/usr/bin/python
#
#
# Logfile in eine SQL Datenbank schreiben
#
# Usage: ./filename YYYY-MM-DD
#
# Laden der Module
#
import os
import sys
import re
import _mssql
#
# Definition der globalen Variablen
#
TEXT = 'N00'
ETEXT= 'C:00'
XOIP = '87878788'
ACK = 'ACQUITTEMENT'
CAR = 'CARTERNIS'
LOG_PATH = '/var/log/ESI/ESI2'
LOG_FILENAME_TEMPLATE = os.path.join(LOG_PATH, 'F1log_{}.txt')
WRITE_PATH = '/home/marc/python'
datum = sys.argv[1]
log_filename = LOG_FILENAME_TEMPLATE.format(datum)
#
# Verbindung mit dem MSSQL-Server herstellen
#
conn = _mssql.connect (server = "localhost", user = "sa", password = "xxx", database = "F1")
#
# Anlegen der einzelnen Tabellen
#
#conn.execute_non_query('CREATE TABLE main(zeit datetime, prom VARCHAR(10), message VARCHAR(15), did VARCHAR(6), rufnummer VARCHAR(12))')
#conn.execute_non_query('CREATE TABLE transmitter(prom VARCHAR(10), rufnummer VARCHAR(12))')
#conn.execute_non_query('CREATE TABLE error(zeit datetime, did VARCHAR(6), rufnummer VARCHAR(12), diag text)')
#
# öfnen des Logfiles, auslesen, der relevanten Daten und erzeugen der einzelnen Variablen. 
#
with open(log_filename) as log_file:
    for line in log_file:
        if re.search(TEXT,line) and not re.search(XOIP,line):
            zeit = '20{y}-{m}-{d} {time}'.format(y=line[11:13], m=line[8:10], d=line[5:7], time=line[14:22])
            prom = line[23:31]
            message = line[32:45]
            did = re.search(r" [0-9]{4} ",line)    
            if re.search(r"[0-9]{10,12}",line):
                rufnummer = re.search(r"[0-9]{10,12}",line)
               # print zeit, prom, message, did.group(), rufnummer.group()
                 #
                 # Füllen der Tabellen, "main" und "transmitter" mit den Daten aus dem Logfile 
                 #
                conn.execute_non_query("""INSERT INTO main (zeit, prom, message, did, rufnummer) VALUES (%s, %s, %s, %s, %s )""",(zeit, prom, message, did.group(), rufnummer.group()))
                conn.execute_non_query("""INSERT INTO transmitter(prom, rufnummer) VALUES (%s, %s )""",(prom, rufnummer.group()))
            else:
               #
               # Füllen der spalte, "Rufnummer" falls keine im Logfile angegeben ist.
               #
                rufnummer = '0000000000' 
                #print zeit, prom, message, did.group(), rufnummer
                conn.execute_non_query("""INSERT INTO main (zeit, prom, message, did, rufnummer) VALUES (%s, %s, %s, %s, %s )""",(zeit, prom, message, did.group(), rufnummer))
                conn.execute_non_query("""INSERT INTO transmitter(prom, rufnummer) VALUES (%s, %s )""",(prom, rufnummer))
        #
        # Füllen der Tabelle, "error" mit den Daten aus dem Logfile
        #
        elif re.search(ETEXT,line) and not re.search(CAR,line) :
            zeit = '20{y}-{m}-{d} {time}'.format(y=line[11:13], m=line[8:10], d=line[5:7], time=line[14:22])
            did = re.search(r" [0-9]{4} ",line)
            diag = re.search(r"Diag:.{20}",line)
            if re.search(r"[0-9]{10,12}",line):
                rufnummer = re.search(r"[0-9]{10,12}",line)
                #print zeit,  did.group(), rufnummer.group(), diag.group()[5:25]
                conn.execute_non_query("""INSERT INTO error (zeit, did, rufnummer, diag) VALUES (%s, %s, %s, %s )""",(zeit, did.group(), rufnummer.group(), diag.group()[5:22]))
                #
                # Füllen der spalte, "Rufnummer" falls keine im Logfile angegeben ist.
                #
            else:
                rufnummer = '0000000000' 
                #print zeit, did, rufnummer, diag.group()[5:25]
                conn.execute_non_query("""INSERT INTO error (zeit, did, rufnummer, diag) VALUES (%s, %s, %s, %s )""",(zeit, did.group(), rufnummer, diag.group()[5:22]))
Sirius3
User
Beiträge: 17746
Registriert: Sonntag 21. Oktober 2012, 17:20

Der Unterstrich bei `_mssql` zeigt, dass das Paket nicht für die öffentliche Verwendung gedacht ist.
Die "Definition der globalen Variablen" sind hoffentlich nur Konstanten.
Ab `datum =` gehört deshalb auch alles mindestens in eine Funktion, damit Du nicht wirklich globale Variablen erzeugst.
Nach einem Konstanten Text mit re.search zu suchen ist im besten Fall sehr umständlich, wenn nicht gar falsch. Da reicht bei Dir ein `in` oder `not in`.
Die fixen Indizes, mit denen Du die log-Zeilen verarbeitest sind auch sehr fragil. Wie sieht denn die Struktur einer Zeile aus?
Dass Du dann wild Teile der Zeile mit regulären Ausdrücken suchst, ist auch mehr Hoffen, als dass das sicher funktioniert.
Wenn im if und im else-Block die selben Zeilen stehen, dann gehören die nach dem else-Block.
Oder für die außere if-else-Konstruktion, davor.

Das Datenbank-Design ist ungünstig:
Die `transmitter`-Tabelle enthält nur redundante Daten und kann weg.
`main` und `error` sehen so ähnlich aus, wäre nicht eine Tabelle mit einem error-Flag besser?
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@fredvonfat: Mir ist jetzt nicht klar was *konkret* das Problem ist‽

Grundsätzlich sollte man bei dem ”Programm” vielleicht nicht alles in einen grossen Code-Klumpen schreiben sondern beispielsweise die Verarbeitung vom Log vom einspeisen in die Datenbank trennen.

``# Definition der globalen Variablen`` ist ein Kommentar der in keinem Quelltext stehen sollte, weil man keine globalen Variablen verwenden sollte. Tust Du auch eigentlich gar nicht wirklich wenn man das Hauptprogramm einfach mal in eine Funktion verschiebt und auf Modulebene nur die Definitionen von Konstanten stehen lässt.

So einige Kommentare sind überflüssig. Faustregel: Ein Kommentar beschreibt nicht *was* gemacht wird, denn das steht da ja bereits als Code, sondern *warum* der Code das *so* macht. Sofern das nicht offensichtlich ist.

Um das Gleichheitszeichen bei Schlüsselwortargumenten kommen keine Leerzeichen.

Warum verwendest Du nicht die DB-API V2 sondern das interne `_mssql`-Modul? Der führende Unterstrich im Namen deutet darauf hin, dass das keine öffentliche API ist. Das haben die Programmierer von `pymssql` anscheinend nicht so ganz verstanden, aber ich sehe trotzdem keinen Grund hier die interne API zu verwenden.

Wenn man das alles ordentlich macht, wird die Notwendigkeit das in Funktionen aufzuteilen noch grösser, weil das ordentliche Aufräumen zum Beispiel der Datenbankverbindung und Ausnahmebehandlung mit `commit()`/`rollback()` weitere Einrückstufen erforderlich macht.

Du benutzt `re.search()` für Tests für die ein einfaches ``in`` reichen würde.

Es wiederholt sich einiges an Code. Zum Beispiel die beiden Zweige ob eine Rufnummer gefunden wurde oder nicht sind bis auf die Zuweisung der Nullnummer jeweils gleich. Man sollte keinen Code wiederholen, das macht bei Anpassungen nur unnötig Arbeit, weil man alle Kopien anpassen muss, und das ist dann fehleranfällig. Vielleicht schon ein schönes Beispiel ist das es mal ``diag.group()[5:25]`` und mal ``diag.group()[5:22]`` heisst – soll das so oder ist nur eines davon richtig?

Dann gibt es beim parsen der beiden Zeilentypen auch Codewiederholungen bei denen Daten aus der Zeile extrahiert werden, die es offenbar in beiden Zeilentypen gibt. Auch den Code sollte man nicht zweimal schreiben.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
#
# Logfile in eine SQL Datenbank schreiben
#
# Usage: ./filename YYYY-MM-DD

import os
import re
import sys
from collections import namedtuple
from contextlib import closing

import pymssql

TEXT = 'N00'
ETEXT = 'C:00'
XOIP = '87878788'
ACK = 'ACQUITTEMENT'  # Unbenutzt.
CAR = 'CARTERNIS'
LOG_PATH = '/var/log/ESI/ESI2'
LOG_FILENAME_TEMPLATE = os.path.join(LOG_PATH, 'F1log_{}.txt')
WRITE_PATH = '/home/marc/python'


Main = namedtuple('Main', 'zeit prom message did rufnummer')
Error = namedtuple('Error', 'zeit did rufnummer diag')


def create_tables(connection):
    with closing(connection.cursor()) as cursor:
        cursor.execute(
            'CREATE TABLE main(zeit DATETIME, prom VARCHAR(10), message'
            ' VARCHAR(15), did VARCHAR(6), rufnummer VARCHAR(12))'
        )
        cursor.execute(
            'CREATE TABLE transmitter(prom VARCHAR(10), rufnummer VARCHAR(12))'
        )
        cursor.execute(
            'CREATE TABLE error(zeit DATETIME, did VARCHAR(6), rufnummer'
            ' VARCHAR(12), diag TEXT)'
        )


def parse_common(line):
    zeit = '20{y}-{m}-{d} {time}'.format(
        y=line[11:13], m=line[8:10], d=line[5:7], time=line[14:22]
    )
    did = re.search(r' [0-9]{4} ', line).group()
    match = re.search(r'[0-9]{10,12}', line)
    rufnummer = match.group() if match else '0000000000'
    return (zeit, did, rufnummer)


def parse_log(lines):
    for line in lines:
        if TEXT in line and not XOIP in line:
            zeit, did, rufnummer = parse_common(line)
            yield Main(zeit, line[23:31], line[32:45], did, rufnummer)
        elif ETEXT in line and not CAR in line:
            zeit, did, rufnummer = parse_common(line)
            diag = re.search(r'Diag:.{20}', line).group()[5:22]
            yield Error(zeit, did, rufnummer, diag)


def insert_entries(connection, entries):
    with closing(connection.cursor()) as cursor:
        for entry in entries:
            if isinstance(entry, Main):
                cursor.execute(
                    'INSERT INTO main (zeit, prom, message, did, rufnummer)'
                    ' VALUES (%s, %s, %s, %s, %s)',
                    entry
                )
                cursor.execute(
                    'INSERT INTO transmitter(prom, rufnummer) VALUES (%s, %s)',
                    (entry.prom, entry.rufnummer)
                )
            elif isinstance(entry, Error):
                cursor.execute(
                    'INSERT INTO error (zeit, did, rufnummer, diag)'
                    ' VALUES (%s, %s, %s, %s)',
                    entry
                )
            else:
                assert False, 'unknown entry type: {}'.format(
                    type(entry)
                )


def main():
    log_filename = LOG_FILENAME_TEMPLATE.format(sys.argv[1])
    with open(log_filename) as lines:
        connection = pymssql.connect(
            server='localhost', user='sa', password='xxx', database='F1'
        )
        with closing(connection):
            try:
                insert_entries(connection, parse_log(lines))
            except:
                connection.rollback()
                raise
            else:
                connection.commit()


if __name__ == '__main__':
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Vielen Dank an Euch beide, dass ihr euch die Zeit genommen habt, mir zu helfen.
Der Unterstrich bei `_mssql` zeigt, dass das Paket nicht für die öffentliche Verwendung gedacht ist.
Das war mir nicht bewusst, was wird unter "öffentlicher Verwendung" verstanden?
Die "Definition der globalen Variablen" sind hoffentlich nur Konstanten.
Ab `datum =` gehört deshalb auch alles mindestens in eine Funktion, damit Du nicht wirklich globale Variablen erzeugst.
Habe gerade das Thema "globale und lokale Variablen" nachgelesen und verstanden, dass die Globalen Variablen für das gesammte Programm und für jede darin enthaltene Funktion existent sind. So hatte ich mir das auch bei dem Script gedacht. Was sind nun aber Konstanten?
Nach einem Konstanten Text mit re.search zu suchen ist im besten Fall sehr umständlich, wenn nicht gar falsch. Da reicht bei Dir ein `in` oder `not in`.
Ok, mir scheint die Vielfalt der Möglichkeiten bei Python noch nicht bewusst zu sein.
Die fixen Indizes, mit denen Du die log-Zeilen verarbeitest sind auch sehr fragil. Wie sieht denn die Struktur einer Zeile aus?
Beispiel für ein Logfile:

5400 23/05/19 22:41:04 ISDN Fehlanruf C:00 2112 <rufnummer> Diag:NICHTS EMPFANGEN
6444 23/05/19 23:57:09 16800126 Ersc TSTCAL g N00 2111 <rufnummer> #SDA=2111#IDCOMM=@0mAUuIs#APPELANT=<rufnummer>#PROT=P100#SITE=16800126
23/05/19 23:57:09 ACQUITTEMENT UGI 6444
6454 23/05/19 23:57:39 87878788 Ersc 314 N00 2122 <rufnummer> #SDA=2122#APPELANT=<rufnummer>#DIAGABUSIF=0#AT=F1#NOMENT=10
23/05/19 23:57:39 ACQUITTEMENT UGI 6454
6322 23/05/19 23:46:58 ISDN Fehlanruf C:00 2145 <rufnummer> Diag:DEKODIERUNGSFEHLER
6360 23/05/19 23:50:33 ISDN Fehlanruf C:00 2111 Diag:DEKODIERUNGSFEHLER

N00 = erfolgreiche Kommunikation
87878788 = ID des Empfängers (diese Zeilen werden nicht benötigt)
C:00 = nicht erfolgreiche Kommunikation (entweder kam keine Kommunikation zu Stande (DIAG:NICHTS EMPFANGEN) oder der Dialog war fehlerbehaftet (DIAG:DEKODIERUNGSFEHLER))

Es gibt Transmitter, die ohne Rufnummer senden und Transmitter, die über die gleiche Rufnummer senden. Die Rufnummer, falls gesendet, wird bei jedem Call mitgeloggt.
Aus einer anderen Datenbank sollen später noch einige "Fixdaten", wie z.B. der transmitterspezifische Standort hinzuaddiert werden.

Das "CAR" soll später für die Verwertung folgender Zeilen dienen:

2297 23/05/19 18:55:50 CARTERNIS Ersc Fehler C:00
2300 23/05/19 18:58:49 CARTERNIS Ersc Fehler Leitung C:00
2303 23/05/19 18:58:49 CARTERNIS Ersc Fehler PBX C:00
2306 23/05/19 18:58:49 CARTERNIS Rkst Fehler C:00
2312 23/05/19 18:59:20 CARTERNIS Rkst Fehler Leitung C:00
2325 23/05/19 18:59:51 CARTERNIS Rkst Fehler PBX C:00
Das Datenbank-Design ist ungünstig:
Die `transmitter`-Tabelle enthält nur redundante Daten und kann weg.
`main` und `error` sehen so ähnlich aus, wäre nicht eine Tabelle mit einem error-Flag besser?
Bei meinem bisherigen Versuchen hatte ich Probleme beim "in die Datenbank schreiben", bestimmte Felder leer zu lassen, das gab immer einen Fehlermeldung.
Durch die zwei Tabellen und die "Nullnummer" hab ich das umgehen können.
Die Transmittertabelle bekommt wie oben schon erwähnt, noch Importdaten.
So einige Kommentare sind überflüssig. Faustregel: Ein Kommentar beschreibt nicht *was* gemacht wird, denn das steht da ja bereits als Code, sondern *warum* der Code das *so* macht. Sofern das nicht offensichtlich ist.
Die Kommentare sollten nicht für Euch bestimmt sein, wenn ich in einem halben Jahr mir anschaue was ich da verzapft habe, fällt es mir leichter, das nachvollziehen zu können.
Um das Gleichheitszeichen bei Schlüsselwortargumenten kommen keine Leerzeichen.
Was genau ist ein Schlüsselwortargument?
Warum verwendest Du nicht die DB-API V2 sondern das interne `_mssql`-Modul?
Weil beim Googlen die ersten COde-Schnipsel, die irgendwie erfolgversprechend aussahen und die ich irgendwie verhackstücken konnte mit _mssql ausgeführt waren. Nun bin ich an dem Punkt, wo ich damit nicht weiterkomme, deshalb bin ich dankbar für Deine Anregungen.
Vielleicht schon ein schönes Beispiel ist das es mal ``diag.group()[5:25]`` und mal ``diag.group()[5:22]`` heisst – soll das so oder ist nur eines davon richtig?
Voll erwischt.
Das .group habe ich verwendet um den Inhalt ausgegeben zu bekommen, [5:25] soll eigentlich nur das "Diag:" abschneiden, da is mir nichts besseres eingefallen.


@__blackjack__
Ich probiere Deinen code gleich mal aus.
Sirius3
User
Beiträge: 17746
Registriert: Sonntag 21. Oktober 2012, 17:20

fredvonfat hat geschrieben: Freitag 31. Mai 2019, 09:53 Was sind nun aber Konstanten?
Konstanten sind nicht variable, also unveränderlich.
fredvonfat hat geschrieben: Freitag 31. Mai 2019, 09:53 Bei meinem bisherigen Versuchen hatte ich Probleme beim "in die Datenbank schreiben", bestimmte Felder leer zu lassen, das gab immer einen Fehlermeldung.
Dass man in Felder NULL schreiben kann, ist der Default, also was hast Du versucht und was war die Fehlermeldung?
fredvonfat hat geschrieben: Freitag 31. Mai 2019, 09:53 Aus einer anderen Datenbank sollen später noch einige "Fixdaten", wie z.B. der transmitterspezifische Standort hinzuaddiert werden.
Man kopiert keine "Fixdaten" in eine andere Tabelle hinein, sondern verknüpft die Daten über einen eindeutigen Schlüssel mit einer Tabelle mit "Fixdaten". Da die gleiche Information schon in der Tabelle ›main‹ enthalten ist, bleibt ›transmitter‹ überflüssig.


Am besten schreibst Du Dir reguläre Ausdrücke, die die verschiedenen Log-Zeilenvarianten verarbeiten können.
Also sowas in der Art:

Code: Alles auswählen

RE_ISDN = "(\d+) (\d\d/\d\d/\d\d \d\d:\d\d:\d\d) ISDN Fehlanruf C:00 (\d+) (\d+) Diag:(.*)"
RE_N00 = "(\d+) (\d\d/\d\d/\d\d \d\d:\d\d:\d\d) (\d+) Ersc .*? N00 (\d+) (\d+) (.*)"
RE_ACQUITTEMENT = "(\d\d/\d\d/\d\d \d\d:\d\d:\d\d) ACQUITTEMET UGI (\d+)"
RE_CARTERNIS = "(\d+) (\d\d/\d\d/\d\d \d\d:\d\d:\d\d) CARTERNIS (.*?) C:00"


match = re.match(RE_ISDN, line)
if match:
    sig, datum, did, rufnummer, diag = match.groups()
    prom = message = None
    is_error = True
match = re.match(RE_N00, line)
if match:
    sig, datum, prom, rufnummer, message = match.groups()
    diag = None
    is_error = False
datum = datetime.datetime.strptime(datum, "%d/%m/%y %H:%M:%S")
cursor.execute(...)
Zum Übertragen von Datumswerten an die Datenbank benutze immer datetime-Objekte und versuche nicht, irgendwas selbst zu parsen und zu formatieren.
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@fredvonfat: Mit „nicht für die öffentlicher Verwendung” ist gemeint, das die API nicht für die Öffentlichkeit gedacht ist. Die Programmierer von `pymssql` können die benutzen, weil die *brauchen* die ja auch, aber sonst halt niemand. Diese Aufteilung in zwei Module findet man bei Anbindungen von externen Bibliotheken die in C geschrieben sind öfter. Ein nicht-öffentliches Modul, welches mehr oder weniger die API von der C-Bibliothek ”wrapped” und dann ein Python-Modul welches eine „pythonische“ API zur Verfügung stellt. Bei DB Modulen kann das nicht-öffentliches Modul praktisch sein, wenn man irgendetwas besonderes, was nur diese Datenbank bietet, machen will, was nur mit der API von diesem speziellen DBMS geht, aber das gibt es im vorliegenden Fall ja nicht.

Konstanten sind ”Variablen” die einen Konstanten Wert haben. Python unterscheidet die nicht technisch, also es gibt keine tatsächlichen Konstanten deren Wert man nicht ändern könnte, aber die Namenskonvention Konstanten KOMPLETT_GROSS zu schreiben.

Beim Verarbeiten der Logzeilen wäre es wesentlich robuster wenn man sich da mehr an der Struktur orientieren würde und nicht nach Teilen *irgendwo* in der Zeile suchen würde, sondern auch sicherstellen würde, dass die Reihenfolge eingehalten wird. Bei `did` fiel mir eben auch gerade auf, das Du das Leerzeichen davor und danach ebenfalls in der Datenbank speicherst‽ Und das scheint ja eine Zahl zu sein, warum speicherst Du die als Text?

Über Primärschlüssel und welche Felder „nullable“ sein müssen und welche nicht, wurde sich beim DB-Entwurf anscheinend auch keine Gedanken gemacht.

Und Fehler beim ”leer lassen” von Feldern kann es eigentlich nicht geben, denn im Moment sind ja alle Spalten „nullable“. Was vielleicht auch gar nicht sein sollte. NULL wäre vielleicht auch der bessere Wert für eine Nullnummer, nur für den Fall das die aus irgendwelchen Gründen tatsächlich mal in einem Log vorkommen sollte.

Redundant scheinen die Daten dann trotzdem noch zu sein wenn `transmitter`-Tabelle nicht überflüssig ist. Dann sollte die vielleicht noch eine `id`-Spalte haben und die `main`-Tabelle keine `prom`- und `rufnummer`-Spalten, sondern einen Fremdschlüssel in die `transmitter`-Tabelle.

Schlüsselwortargumente sind Argumente bei denen man den Argumentnamen mit angibt, statt nur das Argument.

Wenn Du einfach nur 5 Zeichen am Anfang abschneiden willst, dann ist der „slice“ dafür ``[5:]``, also ohne einen Endindex.

Wieder vollkommen ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
#
# Logfile in eine SQL Datenbank schreiben
#
# Usage: ./filename YYYY-MM-DD

import os
import re
import sys
from collections import namedtuple
from contextlib import closing
from datetime import datetime as DateTime

import pymssql

# 
# TODO Überdenken was Zeichenketten und was Zahlen sind/sein sollten.
# 
XOIP = '87878788'
LOG_PATH = '/var/log/ESI/ESI2'
LOG_FILENAME_TEMPLATE = os.path.join(LOG_PATH, 'F1log_{}.txt')
WRITE_PATH = '/home/marc/python'

COMMON_PATTERN = r'^(\d+ )?(?P<zeit>\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) '
MAIN_RE = re.compile(
    COMMON_PATTERN
    + r'(?P<prom>\d+) (?P<message>.+) N00 (?P<did>\d+) (?P<rufnummer>\d+)? ?#'
)
ERROR_RE = re.compile(
    COMMON_PATTERN +
    r'ISDN Fehlanruf C:00 (?P<prom>\d+) (?P<rufnummer>\d+)? ?Diag:(?P<diag>.+)'
)


Main = namedtuple('Main', 'zeit prom message did rufnummer')
Error = namedtuple('Error', 'zeit did rufnummer diag')


def create_tables(connection):
    with closing(connection.cursor()) as cursor:
        cursor.execute(
            'CREATE TABLE main(zeit DATETIME, prom VARCHAR(10),'
            ' message VARCHAR(15), did INTEGER, rufnummer VARCHAR(12))'
        )
        cursor.execute(
            'CREATE TABLE transmitter(prom VARCHAR(10), rufnummer VARCHAR(12))'
        )
        cursor.execute(
            'CREATE TABLE error(zeit DATETIME, did INTEGER,'
            ' rufnummer VARCHAR(12), diag TEXT)'
        )


def get_common(match):
    zeit = DateTime.strptime(match.group('zeit'), '%d/%m/%Y %H:%M:%S')
    did = int(match.group('did'))
    rufnummer = match.group('rufnummer')
    return (zeit, did, rufnummer)


def parse_log(lines):
    for line in lines:
        # 
        # TODO In eine Schleife über (RE, Verarbeitungsfunktion)-Paare umbauen.
        # 
        match = MAIN_RE.match(line)
        if match:
            prom = match.group('prom')
            if prom != XOIP:
                zeit, did, rufnummer = get_common(match)
                yield Main(zeit, prom, match.group('message'), did, rufnummer)
        else:
            match = ERROR_RE.match(line)
            if match:
                zeit, did, rufnummer = get_common(match)
                yield Error(zeit, did, rufnummer, match.group('diag'))


def insert_entries(connection, entries):
    with closing(connection.cursor()) as cursor:
        for entry in entries:
            # 
            # TODO Das sieht nach einem Fall für `functools.singledispatch` aus.
            # 
            if isinstance(entry, Main):
                cursor.execute(
                    'INSERT INTO main (zeit, prom, message, did, rufnummer)'
                    ' VALUES (%s, %s, %s, %s, %s)',
                    entry
                )
                cursor.execute(
                    'INSERT INTO transmitter(prom, rufnummer) VALUES (%s, %s)',
                    (entry.prom, entry.rufnummer)
                )
            elif isinstance(entry, Error):
                cursor.execute(
                    'INSERT INTO error (zeit, did, rufnummer, diag)'
                    ' VALUES (%s, %s, %s, %s)',
                    entry
                )
            else:
                assert False, 'unknown entry type: {}'.format(
                    type(entry)
                )


def main():
    log_filename = LOG_FILENAME_TEMPLATE.format(sys.argv[1])
    with open(log_filename) as lines:
        connection = pymssql.connect(
            server='localhost', user='sa', password='xxx', database='F1'
        )
        with closing(connection):
            try:
                insert_entries(connection, parse_log(lines))
            except:
                connection.rollback()
                raise
            else:
                connection.commit()


if __name__ == '__main__':
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Ok, danke für Euer Feedback.

@Sirius3
Dass man in Felder NULL schreiben kann, ist der Default, also was hast Du versucht und was war die Fehlermeldung?
Ich habe versucht, nichts in die Felder zu schreiben. Auf die Idee, "NULL" reinzuschreiben bin ich nicht gekommen.
Am besten schreibst Du Dir reguläre Ausdrücke, die die verschiedenen Log-Zeilenvarianten verarbeiten können.
Wo liegt der Vorteil darin, die die komplette Zeile in reguläre Ausdrücke zu "gießen"?
Bislang habe ich eindeutige Beschreibungen von bestimmten Zeichenfolgen verwendet und diese in jeder Zeile für sich gesucht. Dadurch werden die Einträge auch gefunden, falls sich mal was an der Zeilenstruktur ändern sollte....., so war jedenfalls mein Ansatz.
Auf der anderen Seite hatte ich beim Schreiben des "Codeklumpens" des öfteren Fehler, weil Zeilen, die ich nicht berücksichtigen wollte, sich mit eingeschlichen haben.

@__blackjack__
Bei `did` fiel mir eben auch gerade auf, das Du das Leerzeichen davor und danach ebenfalls in der Datenbank speicherst‽ Und das scheint ja eine Zahl zu sein, warum speicherst Du die als Text?
Das Leerzeichen davor und danach beschreibt diesen Abschnitt der Zeile eindeutig. Mit der Zahl soll nicht gerechnet werden, sie dient allenfalls einer Protokollzuordnung. Deshalb habe ich hier nicht auf die Zahl bestanden. Kann mir das Nachteile bringen?
Über Primärschlüssel und welche Felder „nullable“ sein müssen und welche nicht, wurde sich beim DB-Entwurf anscheinend auch keine Gedanken gemacht.
Korrekt, mangels Kenntnis, Erfahrung und kurzgehaltenen Howtoos wurde sich für learning by doing entschieden.
Redundant scheinen die Daten dann trotzdem noch zu sein wenn `transmitter`-Tabelle nicht überflüssig ist. Dann sollte die vielleicht noch eine `id`-Spalte haben und die `main`-Tabelle keine `prom`- und `rufnummer`-Spalten, sondern einen Fremdschlüssel in die `transmitter`-Tabelle.
Wo liegen die Vor- und Nachteile von einer vs. mehrerer Tabellen? Evtl. Rechtevergabe kann ich mir schon vorstellen, aber gibt es noch andere Gründe das eine oder andere vorzuziehen?
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

fredvonfat hat geschrieben: Freitag 7. Juni 2019, 10:23Wo liegen die Vor- und Nachteile von einer vs. mehrerer Tabellen? Evtl. Rechtevergabe kann ich mir schon vorstellen, aber gibt es noch andere Gründe das eine oder andere vorzuziehen?
Weil relationale Datenbanken genau dafür da sind, dass Daten nicht redundant gespeichert werden.
Das macht die Pflege der Daten unnötig kompliziert.

Such mal nach "Normalisierung" von Datenbanken.
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Ja, das mit der Normalisierung hatte ich mir schon mal angeschaut und scheinbar nicht verstanden.
Ich hatte mir überlegt zu einem späteren Zeitpunkt, z.B. die "ID" als "Schlüssel" zu definieren und so eine Verknüpfung herzustellen.

Langsam erahne ich allerdings, das Primär- und Fremdschlüssel noch was ganz anderes sind.....
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@fredvonfat: NULL heisst das in SQL – das wird auf `None` in Python abgebildet. Du musst also aufpassen wo Du NULL schreibst.

Ein regulärer Ausdruck der die ganze Zeile abdeckt, trifft sicherer die Zeilen die diesem Muster entsprechen, weil da beispielsweise nicht nur die einzelnen Teile sondern auch ihre Reihenfolge und teilweise was dazwischen stehen oder nicht stehen darf, mit beschrieben werden. Und selbst wenn man die Zeilen nicht kennt, kann man am Quelltext besser ablesen wie sie denn aussehen können. An Deinem Code konnte man beispielsweise nicht ablesen, dass der Zeitstempel vor der Rufnummer kommt. Oder das vor dem Zeitstempel optional noch eine Zahl stehen kann.

Das bei der `did` das Leerzeichen vor und nach der Ziffernfolge dazu da ist diesen Wert sicher(er) in der Zeile zu indentifizieren ist mir schon klar, was ich nicht verstehe ist warum diese beiden Leerzeichen mit in der Datenbank gespeichert werden, denn die haben ja keinerlei Informationgehalt mehr wenn man den Wert bereits isoliert hat.

Neben Rechnen ist eine weitere Operation bei der sich Zahlen von Zeichenketten unterscheiden das Vergleichen. Zahlen werden anders sortiert als Zeichenketten. Zudem ist das auch ein Stück Dokumentation. Wenn ich INTEGER lese, habe ich eine andere, genauere Vorstellung davon wie der Wertebereich aussieht als wenn ich VARCHAR(6) für die gleiche Spalte lesen würde. Und es hat auch Auswirkungen auf die Art wie eine Datenbank die Werte speichert, wie eventuelle Indextabellen aussehen können, und wie effizient Vergleiche bei Suchen sein können. Einiges davon trifft auf SQLite nicht unbedingt zu, aber so ganz generell würde ich Zahlen auch als Zahlen in Datenbanken deklarieren und speichern, statt als Zeichenketten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

@__blackjack__

Ich bin gerade dabei Deinen oben eingefügten Code zu verstehen und möchte ihn stück für stück ausprobieren.

Im Augenblick geht es um die regex, "COMMON_PATTERN".
Ich möchte mir den Inhalt ausgeben.

Code: Alles auswählen

#!/usr/bin/env python3
import os
import re
import sys


LOG_PATH = '/var/log/ESI/ESI2'
LOG_FILENAME_TEMPLATE = os.path.join(LOG_PATH, 'F1log_{}.txt')

COMMON_PATTERN = r'([0-9]{4})'
log_filename = LOG_FILENAME_TEMPLATE.format(sys.argv[1])
TEXT = 'N00'

with open (log_filename) as log_file:
    for line in log_file:
        if TEXT in line:
            test = COMMON_PATTERN
            print(test)
Leider bekomme ich immer nur die Ausgabe:
([0-9]{4})
Aber die Ausgabe sollte eine vierstellige Zahl sein.
was läuft da falsch?
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Es scheint, als wenn nach den Zeichen und nicht nach den regex gesucht wird.
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Du bindest einen String an den Namen "COMMON_PATTERN" und später zusätzlich an "test".
Dann lässt du dir "test" ausgeben, was natürlich noch immer den Wert hat, den du am Anfang "COMMON_PATTERN" zugewiesen hast.

Reguläre Ausdrücke sind ein kompliziertes Thema, dem sich ganze Bücher widmen.

In dem Quellcode, den du verstehen willst, werden mit COMMON_PATTERN noch ganz andere Sachen gemacht.
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Ok, und was muss ich tun, damit ich die regex und nicht den String zuweise?
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Ja, das habe ich gesehen da werden mit re.compile strings zusammengesetzt und das ganze in Namedtupel bereitgestellt.

Nur wenn ich das so mache, wie es da steht komme ich auch nicht weiter, deshalb hilft es mir nur das ganze in "TIPTOP"-Schritten zu verstehen.
Sirius3
User
Beiträge: 17746
Registriert: Sonntag 21. Oktober 2012, 17:20

@fredvonfat: dann arbeite Dich erst an einfachen Beispielen ins re-Modul ein, dann lernst Du, wie Du es anwenden kannst, und kannst danach Dich auch an kompliziertere Ausdrücke wagen.
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Na ja, mein zu letzt geposteter Schnipsel sehe ich jetzt nicht als so kompliziert an, es ist auch nicht so, dass die regex mir völlig fremd sind, nur unter Python hatte ich damit noch nichts zu tun.
Sirius3
User
Beiträge: 17746
Registriert: Sonntag 21. Oktober 2012, 17:20

daher meine Empfehlung sich in das re-Modul einzuarbeiten.
fredvonfat
User
Beiträge: 51
Registriert: Mittwoch 12. September 2018, 10:00
Wohnort: Berlin

Ich bin nun schon ein kleines Stück weiter gekommen, dank eurer Hilfe habe ich auch die DB-Struktur noch einmal überdacht.
Eine Tabelle sollte reichen und die exclusionen habe ich auch rausgenommen, dass ist dann ein Job für die Datenbankabfrage.
Zunächst denke ich, dass es besser ist erst einmal alles mitzunehmen.

Auf dem Weg, den Code zu lernen bin ich nun bei den Funktionen bzw. deren Verkettungen angelangt.
Bevor ich mich an die Datenbank mache würde ich mir die Daten gerne ausgeben lassen.

Allerdings bekomme ich immer ein
<generator object parse_log at 0x7f70b388be08>
Was hab ich noch nicht verstanden bzw. wo kann ich ansetzen um das zu ändern?

Code: Alles auswählen

#!/usr/bin/env python3
#
#
#
#
#
#
import os
import sys
import re
from collections import namedtuple
from contextlib import closing
from datetime import datetime as DateTime

import pymssql

LOG_PATH = '/var/log/ESI/ESI2'
LOG_FILENAME_TEMPLATE = os.path.join(LOG_PATH, 'F1log_{}.txt')

COMMON_PATTERN = r'^(\d+) (?P<zeit>\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) '
MAIN_RE = re.compile(COMMON_PATTERN + r'(?P<prom>\d{8}) (?P<message>.{14}) .+ N00 (?P<did>\d{4}) (?P<rufnummer>\d+)? ?#')
ERROR_RE = re.compile(COMMON_PATTERN + r'ISDN Fehlanruf C.00 (?P<did>\d+) (?P<rufnummer>\d+)? ?Diag:(?P<message>.{14})')

Main = namedtuple('Main', 'zeit prom message did rufnummer')
Error = namedtuple('Error', 'zeit prom message did rufnummer')

       
def get_common(match):
    zeit = DateTime.strptime(match.group('zeit'), '%d/%m/%y %H:%M:%S')
    did = int(match.group('did'))
    rufnummer = match.group('rufnummer')
    return (zeit, did, rufnummer)

def parse_log(lines):
    for line in  lines:
        match = MAIN_RE.match(line)
        if match:
            zeit, did, rufnummer = get_common(match)
            message = match.group('message')
            prom = match.group('prom')
            yield Main(zeit, prom, message, did, rufnummer)
        else:
            match = ERROR_RE.match(line)
            if match:
                zeit, did, rufnummer = get_common(match)
                prom = 'x{8}'
                message = match.group('message')
                yield Error(zeit, prom, message, did, rufnummer)

def main():
    log_filename = LOG_FILENAME_TEMPLATE.format(sys.argv[1])
    with open (log_filename) as lines:
        print(parse_log(lines))

main()
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@fredvonfat: `parse_log()` ist eine Generatorfunktion (wegen ``yield``) — die macht beim Aufruf nichts ausser sofort ein Generator-Objekt zu liefern. Die Arbeit passiert erst wenn man die Elemente von diesem Generator abfragt. Also entweder schreibst Du da eine Schleife drüber und gibts die einzelnen Elemente aus, oder Du rufst `list()` mit dem Generator auf, damit die Elemente alle in einer Liste gesammelt werden, und gibst dann die Liste aus.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten