Auflistung aller Dateien in UNC Pfaden und Speichern der Daten in einer SQL-Datenbank

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
pythonisfrun
User
Beiträge: 4
Registriert: Freitag 15. April 2016, 10:39

Wir haben in unserem ERP-System über 3 Mio Dateien, die auf Fileshares liegen. Für einen Abgleich von Fileshares<=>ERP-System habe ich mich mal an Python herangetraut und folgendes Skript geschrieben, welches die Aufgabe bisher löst.

Erfahrungen mit Python sind bei mir bisher nur rudimentär vorhanden. Andere Programmiersprachen kenne ich nicht unbedingt (früher habe ich mal Basic (ja: das Basic für den C64) und Turbo Pascal programmiert.

Das Skript hat noch viel Verbesserungspotential, aber nachdem ich am Wochenende mit einem Handbuch über Python angefangen habe, bin ich doch stolz, es so weit gebracht zu haben. Das liegt wohl aber auch an der Einfachheit von Python, wenn aber die Einbindung von _mysql mich doch einige Nerven gekostet hat.

Hier das Skript:

Code: Alles auswählen

import sys, os, pathlib, _mysql

pfad_all = (r'\\server\unc01',r'\\server\unc02')

print ("Version 1.6")

c = _mysql.connect('db', 'user','pwd', db='datei_sql')

c.query("DELETE FROM `files` WHERE 1")

z = 0
for pfad in pfad_all:
    if os.path.exists(pfad) == True:
        print(pfad, "existiert.")
    else:
        print(pfad, "existiert nicht.")
    for t in os.walk(pfad):
        (dirs, subdirs, files) = t
        for filename in files:
                z = z+1
                sql = ("INSERT INTO files VALUES ('$1','$2','$3','$4','$5','$6','$7')")
                # 1 = Nummer, count
                # 2 = servername, unc
                # 3 = path nach unc
                # 4 = filename
                # 5 = filesize, 
                # 6 = filname+path, , kompletter pfad mit dateiname
                # 7 = Dateiendung
                a = str(z)
                f = str(dirs)+"\\"+(filename)
                s=os.path.getsize(f)
                filepath = dirs.replace('\\','\\\\')
                servername = pfad.replace('\\','\\\\')
                dateiname = filename.replace("'","_")
                path_filename=filepath+"\\\\"+dateiname
                filetype=dateiname[-3:]
                filetype=filetype.replace(".","") 
                b = len(servername)
                x = filepath[b:]
                sql = sql.replace('$1', a)
                sql = sql.replace('$2', servername)
                sql = sql.replace('$3', x)
                sql = sql.replace('$4', dateiname)
                sql = sql.replace('$5', str(s))
                sql = sql.replace('$6', path_filename)
                sql = sql.replace('$7', filetype)
                c.query(sql)
                c.commit
print ("Es wurden ", z, "Dateien gezählt")
print ("Done")
c.close
Die nächsten Aufgaben:

1.) Prüfen, ob auch richtig gezählt wird. (Bisher stimmt es!)
2.) Den Weg zum endgültigen SQL-Statement logisch aufbauen ;-)
3.) Den Befehl: dateiname = filename.replace("'","_") umstellen, diesen musste ich einbauen, weil es Dateien mit "unmöglichen" Namen (') gibt. Das muss ich noch abfangen.
4.) Verwendung einer Funktion für die Erstellung des SQL-Statements.
5.) Was passiert, wenn eine Dateiendung mehr als 3 Buchstaben hat? Das muss ich noch abfangen.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@pythonisfrun: Unterstriche vor Modulnamen deuten darauf hin, dass man sie nicht benutzen sollte, weil sie nur für interne Zwecke sind. Statt _mysql gibt es das Modul MySQLDB das auch das DBv2-Interface von Python implementiert.
Der WHERE-Block bei DELETE ist optional, macht hier also keinen Sinn. Explizit auf True zu prüfen macht keinen Sinn, da das Ergebnis auch nur wieder True oder False ist. Warum machst Du überhaupt eine Existenzprüfung wenn Du danach sowieso immer os.walk aufrufst?
Das Entpacken des Tuples solltest Du gleich innerhalb des for-Statements machen und nicht eine nichtssagende Variable t dazwischenschalten. dirs ist der falsche Name für EIN Verzeichnis. Zum Zusammensetzen von Pfaden gibt es os.path.join. Um die Endung eines Dateinamens abzuspalten gibt es os.path.splitext. Bei SQL-Statements mit replace zu arbeiten ist ja noch viel schlimmer als mit format. Hände weg von diesem Low-Level-Datenbankmodul. Nimm MySQLDB, das hat Cursors und eine Execute-Methode, die Parameter erlaubt. Niemals von Hand irgendwelche Werte in SQL-Statements hineinstückeln!
Methoden sollte man aufrufen und nicht nur referenzieren. Dein c.commit und c.close machen so gar nichts.
Eingerückt wird immer mit 4 Leerzeichen pro Ebene.

Zum Datenbankdesign: wenn Du eine ID pro Datensatz willst, lass das die Datenbank automatisch machen, statt händisch eine Nummer vorzugeben. Du lauter redundante Daten in Deiner Datenbank, entweder Du speicherst einmal den kompletten Dateinamen oder halt die Einzelteile, nicht beides. Warum speicherst Du Zahlen als Strings? Das ist falsch.
BlackJack

@pythonisfrun: Die Einbindung von `_mysql` mag einiges an Nerven gekostet haben, ist aber falsch. Der Unterstrich am Anfang eines Namens kennzeichnet Objekte die nicht zur öffentlichen API gehören und damit auch nicht verwendet werden sollten, ausser man ist selber Autor davon und verwendet das um die eigentliche öffentliche API darauf auf zu bauen. Die offizielle Schnittstelle ist das `MySQLdb`, und das implementiert dann auch die DB-API 2.0.

Die Module `sys` und `pathlib` werden importiert, aber nicht verwendet. Wobei die Verwendung von `pathlib` eventuell sogar Sinn machen würde.

Auf Modulebene sollten nur Konstanten, Funktionen, und Klassen definiert werden. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Bei Namen sollte man nicht englisch und deutsch mischen, schon gar nicht innerhalb des selben Namens. Und Abkürzungen sind auch keine gute Idee. Einbuchstabige Namen mit einem Gültigkeitsbereich der über einen Ausdruck hinaus geht, gehen gar nicht. Ausnahmen sind Namen die man aus der Mathematik kennt, also `i`, `j` für Indexe oder `x`, `y`, `z` für Koordinaten. Namen sollen dem Leser vermitteln was für eine Bedeutung der Wert hinter dem Namen im Programm hat. `c`, `z`, `a`, `f`, `s`, `b`, und `x` tun das in Deinem Programm nicht. Der Speicher ist nicht mehr begrenzt, so dass man maximal zweibuchstabige Namen wie beim C64-BASIC rechtfertigen könnte. Und mit Tipparbeit kann man bei vernünftigen Editoren auch nicht argumentieren, denn Autovervollständigung gehört mittlerweile zum Standard. Letztendlich wird Quelltext öfter gelesen als geschrieben und beim lesen ist das Verstehen können wichtig.

Der Kommentar `count` zum ersten Wert der in die Datenbank eingefügt wird ist ungünstig, das ist keine Anzahl sondern eher die ID, die wie Sirius3 ja schon angemerkt hat von der Datenbank erzeugt werden sollte und nicht vom Benutzer.

Kommentare werden in Python üblicherweise *vor* den Code geschrieben der im Kommentar näher erklärt wird. Wenn man bei der SQL-Anweisung die Spaltennamen mit angeben würde, und die sprechend bezeichnet sind, dann bräuchte man den Kommentar der die einzelnen Werte erklärt sehr wahrscheinlich gar nicht.

Die Tabelle würde ich nur `file` nennen. So eine Tabellendeklaration ist so ähnlich wie eine Klasse, die beschreibt *einen* Datensatz.

3.) ist nur nötig weil Du selbst Werte in eine SQL-Anweisung in Form von Zeichenketten hinein formatierst. Was man nie machen sollte. Das ist eine Sicherheitslücke, eine Fehlerquelle, und kann die Datenbanksoftware zudem ausbremsen weil sie gezwungen ist jede SQL-Anweisung aufs neue zu parsen.

4.) wird dadurch auch unnötig, denn die SQL-Anweisung bleibt ja immer gleich. Es sind nur die Daten die man der `execute()`-Methode vom `Cursor` übergibt, die sich bei jedem Aufruf ändern. An dieser Stelle sollte man vielleicht eher überlegen ob man sich überhaupt mit SQL-Anweisung herumschlagen will und nicht lieber eine Abstraktionsschicht wie SQLAlchemy dazwischen schiebt.

5.) Und was passiert wenn die Dateiendung weniger als 3 Zeichen hat? Wie Sirius3 schon erwähnte: `os.path.splitext()`.

Wenn man das alles umsetzt, lande ich ungefähr hier (ungetestet):

Code: Alles auswählen

import os
from contextlib import closing

import MySQLdb


def main():
    print('Version 1.6')
    unc_paths = [r'\\server\unc01', r'\\server\unc02']
    connection = MySQLdb.connect('db', 'user', 'pwd', db='datei_sql')
    with closing(connection):
        with closing(connection.cursor()) as cursor:
            cursor.execute('DELETE FROM file')
            connection.commit()

            filename_count = 0
            for base_path in unc_paths:
                if os.path.exists(base_path):
                    print(base_path, 'existiert.')

                    for path, _, filenames in os.walk(base_path):
                        for filename in filenames:
                            file_size = os.path.getsize(
                                os.path.join(path, filename)
                            )
                            unc_name, path = os.path.splitunc(path)
                            filename, extension = os.path.splitext(filename)
                            cursor.execute(
                                'INSERT INTO file (unc_name, path, filename,'
                                ' extension, size)'
                                ' VALUES (%s,%s,%s,%s,%s)',
                                (unc_name, path, filename, extension, file_size)
                            )
                            connection.commit()
                            filename_count += 1
                else:
                    print(base_path, 'existiert nicht.')

            print('Es wurden', filename_count, 'Dateien gezählt.')
            print('Done.')


if __name__ == '__main__':
    main()
Das ist für eine Funktion alles ein bisschen viel. Als nächstes sollte man das sinnvoll auf Funktionen aufteilen.
pythonisfrun
User
Beiträge: 4
Registriert: Freitag 15. April 2016, 10:39

Hallo,

erst einmal vielen Dank für die Anmerkungen und Ergänzungen! Eure Aussagen sind richtig und stellen auch gut dar, wieviel Verbesserungspotential noch drin steckt.

Das verbesserte Skript werde ich mal ausprobieren und ein Feedback geben.

Noch einige Anmerkungen:

1.) _mysql habe ich eingebunden, weil ich bei der Erzeugung des SQL-Statements mit MySQLdb ein Problem hatte, dass ich nicht lösen konnte.
2.) pathlib war noch eingebunden aus einer früheren Version.
3.) Die Buchstaben wurden von mir verwendet, weil ich noch am probieren war, ob das ganze überhaupt läuft. :D
pythonisfrun
User
Beiträge: 4
Registriert: Freitag 15. April 2016, 10:39

Hallo, ich bins nochmal.

Das Skript läuft sehr gut! Danke dafür. Es gibt aber eine kleine Einschränkung. Der Befehl unc_name, path = os.path.splitunc(path) liefert immer nur dann ein Ergebnis für unc_name wenn ein neues Verzeichnis (=Unterverzeichnis) durchlaufen wird.

Meine Vermutung ist, dass dies am Tupel liegt, den os.walk erzeugt. Kann mir das jemand erklären?

Vielen Dank schon mal für jede Unterstützung.

Grüße

Aktuelle Version:

Code: Alles auswählen

import os
from contextlib import closing

import MySQLdb


def main():
    print('Version 1.9')
    unc_paths = [r'\\unc']
    connection = MySQLdb.connect('sql', 'user', 'pwd', db='dateien')
    with closing(connection):
        with closing(connection.cursor()) as cursor:
            cursor.execute('DELETE FROM files')
            connection.commit()

            filename_count = 0
            for base_path in unc_paths:
                if os.path.exists(base_path):
                    print(base_path, 'existiert.')

                    for path, _, filenames in os.walk(base_path):
                        for filename in filenames:
                            filesize = os.path.getsize(
                                os.path.join(base_path, path, filename)
                            )
                            unc_name, path = os.path.splitunc(path) #unc_name erscheint nur 
                            filename, extension = os.path.splitext(filename)
                            cursor.execute(
                                'INSERT INTO files (unc_name, path, filename,'
                                ' extension, filesize)'
                                ' VALUES (%s,%s,%s,%s,%s)',
                                (unc_name, path, filename, extension, filesize)
                            )
                            connection.commit()
                            filename_count += 1
                else:
                    print(base_path, 'existiert nicht.')

            print('Es wurden', filename_count, 'Dateien gezählt.')
            print('Done.')


if __name__ == '__main__':
    main()
pythonisfrun
User
Beiträge: 4
Registriert: Freitag 15. April 2016, 10:39

Update:

Code: Alles auswählen

Der Code unc_name, path = os.path.splitunc(path)
ändert den String, deshalb habe ich nun den Code

Code: Alles auswählen

 unc_name, file_path = os.path.splitunc(path)
eingefügt und im SQL-Statement verwende ich dann file_path.

Damit klappt es. :D
Antworten