Brauche Resourcenspaarende Variante um eine Exel Tabelle einzulesen.

  • Hey Leute,

    ich hoffe ihr könnt mir helfen, da ich derzeit vor einem kleinen Problem stehe.

    Ich suche eine effiziente Variante mehrere Exel Dateien (Trennzeichen = ";") und mehrere DB(MySQL) Tabellen einzulesen und später die Datenbank anhand der Datensätze der Exel Tabellen upzudaten.

    Also in Deutsch:
    Ich habe Exel Dateien und Datenbank Tabellen.
    Die Infos die in der Datenbank stehen sind nicht mehr aktuell und müssen anhand der Infos aus den Exel Dateien upgedatet werden.

    Die Datenbank darf aber zu keinem Zeitpunkt leer sein!!!
    Ich dachte, dass ich beides einlese, vergleiche und mir die benötigten SQL Befehle in eine Warteschlange (Queue) schreibe und später die Befehle ausführe.

    Früher habe ich immer nur eine Datei und eine Tabelle gehabt. Da habe ich beies in ein Array geschrieben, was aber sehr viel RAM erfordert und lange dauert.

    Die Einträge in den Tabellen und Dateien liegt zwischen 50.000-300.000 je nach Datei / Tabelle.

    Hat jemand bitte einen Tipp wie ich das am Bessten löse?

  • Deine bisher angewandte Methode ist eigentlch die schnellste. Pste also bitte einmal dein bisheriges Skript damit man schauen kann wo du noch optimieren kannst. Ausserdem denke ich das du von mit Excel erstellten Dateien im CSV-Format meinst,

    mfg autoBert

  • Ja, ich meine einfach nur CSV Dateien mit dem Trennzeichen ";".
    Gibt es dafür eigentlich einen guten Parser?

    Das Problem bei den Arrays ist halt, dass das ganze Array bei jedem Zugriff im Speicher liegt. Was sehr viel leisung braucht.
    Kann man das nicht mit einer Liste oder so Optimieren?

    Mein derzeitiges Script ist nur um 2 Tabellen zu syncen. Prizinip ist das selbe, aber jetzt muss ich den Inhalt von ca 10 Dateien auf ca. 15 Tabellen updaten.

  • Ja, ich meine einfach nur CSV Dateien mit dem Trennzeichen ";".
    Gibt es dafür eigentlich einen guten Parser?

    Zeilenweise mit StringSplit arbeiten.

    Das Problem bei den Arrays ist halt, dass das ganze Array bei jedem Zugriff im Speicher liegt.

    Dann geh doch die Datei zeilenweise durch (FileReadLine) dann ist immer nur die derzeitige Zeile im Speicher.

    Vielleicht kannst du mal ein kleines Minimalbeispiel (welches auch bei anderen direkt lauffähig ist) posten und dann können wir konkreter sehen was verbessert werden kann.
    Statt MySQL kannst du für das Beispiel ja z.B. SqLite verwenden.

  • Das mit dem minibsp versuche ich mal. filereadline bringt nichts :D

    Das Problem ist, dass jeder der das script testen will eine db und eine Datei braucht.
    Ich schaue mal, ob ich ein Bsp. mit 2 Dateien machnen kann & den Inhalt der Dateien Posten.

  • Habe die 2. DB auch als Datei gemacht.
    Sieht nicht schön aus, aber ich hoffe du verstehst es.
    Ich bräuchste nämlich möglichst schnell eine Lösung :D, da ich gerade dabei bin den Updater für die neue Datenbank zu schreiben und langsam komme ich ohne diese Funktion nicht mehr weiter.

    Ein Code Bsp.

    Spoiler anzeigen
    [autoit]

    #include <Array.au3>
    #include <File.au3>

    [/autoit] [autoit][/autoit] [autoit]

    $file_1 = "datei1.csv"
    $file_2 = "datei2.csv"
    $line_count_file_1 = _FileCountLines($file_1)
    $line_count_file_2 = _FileCountLines($file_2)

    [/autoit] [autoit][/autoit] [autoit]

    Global $array_datei_1_artikelnummer[$line_count_file_1+1]
    Global $array_datei_2_artikelnummer[$line_count_file_2+1]
    Global $array_datei_1_name[$line_count_file_1+1]
    Global $array_datei_2_name[$line_count_file_2+1]
    Global $array_datei_1_ean[$line_count_file_1+1]
    Global $array_datei_2_ean[$line_count_file_2+1]
    Global $array_datei_1_preis[$line_count_file_1+1]
    Global $array_datei_2_preis[$line_count_file_2+1]

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    $line = 1
    while ($line <= $line_count_file_1)
    $line_read = FileReadLine($file_1,$line)
    $line_split = StringSplit($line_read,";")
    $array_datei_1_artikelnummer[$line] = $line_split[1]
    $array_datei_1_name[$line] = $line_split[2]
    $array_datei_1_ean[$line] = $line_split[3]
    $array_datei_1_preis[$line] = $line_split[4]
    $line = $line +1
    WEnd

    [/autoit] [autoit][/autoit] [autoit]

    $line = 1
    while ($line <= $line_count_file_2)
    $line_read = FileReadLine($file_2,$line)
    $line_split = StringSplit($line_read,";")
    $array_datei_2_artikelnummer[$line] = $line_split[1]
    $array_datei_2_name[$line] = $line_split[2]
    $array_datei_2_ean[$line] = $line_split[3]
    $array_datei_2_preis[$line] = $line_split[4]
    $line = $line +1
    WEnd

    [/autoit] [autoit][/autoit] [autoit]

    ; Ich muss erst beide einlesen, da ich später gleichzeitig Infos aus mehreren Dateien brauche zum update der Datenbank (hier Datei 2).

    [/autoit] [autoit][/autoit] [autoit]

    ; Die Datei 2 soll mit den Infos von Datei 1 upgedated werden. Später ist Datei 2 eine Datenbank!

    [/autoit] [autoit][/autoit] [autoit]

    $line = 1
    while ($line <= $line_count_file_1)

    $line_2 = 1
    while ($line_2 <= $line_count_file_2)
    if $array_datei_1_artikelnummer[$line] = $array_datei_2_artikelnummer[$line_2] Then
    if $array_datei_1_preis[$line] <> $array_datei_2_preis[$line_2] Then
    ConsoleWrite("Query zum Update der Datenbank muss hier erstellt werde fuer den Artikel mit dem Namen: "&$array_datei_2_name[$line]&@crlf)
    EndIf
    ExitLoop
    EndIf
    $line_2 = $line_2 +1
    WEnd

    $line = $line +1
    WEnd

    [/autoit] [autoit][/autoit] [autoit]

    ; natürlich müssen die Änderungen später wieder geschrieben werden... Da ich das aber bei der Datenbank mit Querys mache, lasse ich das hier mal weg.

    [/autoit]


    Datei1:
    Name: Datei1.csv

    Spoiler anzeigen

    1;HP Drucker;1234567;101.41
    2;Brother Drucker;890123;110.32
    3;Canon Drucker;4567890;131.17

    Datei2:
    Name: Datei2.csv

    Spoiler anzeigen

    3;Canon Drucker;4567890;181.17
    2;Brother Drucker;890123;110.32
    1;HP Drucker;1234567;101.41

    2 Mal editiert, zuletzt von computerkoenig (28. September 2012 um 13:07)

  • Bringt dich folgender grundlegender Aufbau weiter?:

    Datenbank-Update mit CSV-Dateien
    [autoit]

    Global $a_Split, $hFile, $Zeile, $ArtNummer, $Preis

    [/autoit] [autoit][/autoit] [autoit]

    $file_1 = "datei1.csv"
    $file_2 = "datei2.csv"

    [/autoit] [autoit][/autoit] [autoit]

    ; Unsere Datenbank - hier in Form eines Strings
    Global $s_DataBase = FileRead($file_2)
    ConsoleWrite("Datenbank vor dem Update: " & @CRLF & $s_DataBase & @CRLF & @CRLF)

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    #region Datei zeilenweise durchgehen:
    $hFile = FileOpen($file_1)
    If $hFile = -1 Then Exit MsgBox(48, "Fehler", "Konnte Datei " & $file_1 & " nicht öffnen!")

    [/autoit] [autoit][/autoit] [autoit]

    Do
    $Zeile = FileReadLine($hFile) ; nächste Zeile einlesen
    If @error Then ExitLoop ; wenn Ende der Datei erreicht ist - raus

    [/autoit] [autoit][/autoit] [autoit]

    $a_Split = StringSplit($Zeile, ";", 2)
    If UBound($a_Split) < 4 Then ExitLoop

    [/autoit] [autoit][/autoit] [autoit]

    $ArtNummer = $a_Split[0]
    $Preis = $a_Split[3]

    [/autoit] [autoit][/autoit] [autoit]

    If GetDataBasePreis($s_DataBase, $ArtNummer) <> $Preis Then
    UpdateDataBasePreis($s_DataBase, $ArtNummer, $Preis)
    EndIf
    Until 0

    [/autoit] [autoit][/autoit] [autoit]

    FileClose($hFile)
    #endregion

    [/autoit] [autoit][/autoit] [autoit]

    ConsoleWrite("Datenbank nach dem Update: " & @CRLF & $s_DataBase & @CRLF & @CRLF)

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit][/autoit] [autoit][/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; Funktion welche zu einer Artikelnummer den Preis aus der Datenbank ausliest
    Func GetDataBasePreis(ByRef $s_DataBase, $Artikelnummer)
    $a_RegExp = StringRegExp($s_DataBase, "(?m)^" & $Artikelnummer & ".+;([^;]+)$")
    If @error Or Not IsArray($a_RegExp) Then Return -1
    Return $a_RegExp[0]
    EndFunc ;==>GetDataBasePreis

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; Funktion welche den Preis einer Artikelnummer in der Datenbank aktualisiert
    Func UpdateDataBasePreis(ByRef $s_DataBase, $Artikelnummer, $NeuerPreis)
    $s_DataBase = StringRegExpReplace($s_DataBase, "(?m)^" & $Artikelnummer & ";([^;]+);([^;]+);[^;]+$", $ArtNummer & ";\1;\2;\3;" & $NeuerPreis)
    EndFunc ;==>UpdateDataBasePreis

    [/autoit] [autoit][/autoit] [autoit][/autoit]

    Einmal editiert, zuletzt von AspirinJunkie (28. September 2012 um 15:53)

  • Leider nicht, da ich später die daten aus 1 Datei z.B. in 2 Tabellen schreiben muss. Und wenn ich die Datei für jede tabelle neu einlese, spaart das zwar Resourcen, aber dauert zu lange.

  • Kurz die Frage:

    Ich habe HIER gesehen, dass Autoit auch listen kann. Weist du, ob ich in der Liste pro Spalte mehr als eine info getrennt speichern kann, oder muss ich dann die datenbankinfos wieder mit einem Trennzeichen trennen und als ein String speichern?

  • Und noch eine Sache:

    Das einlesen der 1. Datei (nur einlesen in eine Liste) ohne Trennung usw. dauert seeeehr lange. (Datei hat 732.481 Zeilen)
    Bis jetzt sind seit ca 20-30min 26.500 Zeilen eingelesen. Der RAM der AutoIt3.exe liegt derzeit bei 20,4MB

  • Leider nicht, da ich später die daten aus 1 Datei z.B. in 2 Tabellen schreiben muss.

    Kannst du die 2 Tabellen nicht gleichzeitig in einem Durchlauf updaten.
    Brauchst die Datei ja nicht 2x einzulesen.

    Ich habe HIER gesehen, dass Autoit auch listen kann.

    Ja die Listen von .Net können benutzt werden aber warum möchtest du diese hier nutzen?

    Das einlesen der 1. Datei (nur einlesen in eine Liste) ohne Trennung usw. dauert seeeehr lange. (Datei hat 732.481 Zeilen)
    Bis jetzt sind seit ca 20-30min 26.500 Zeilen eingelesen.

    Zeig mal bitte deinen Code mit dem du dies bewerkstelligst.
    Das klingt nicht normal.
    Ich vermute du nimmst ein ArrayAdd() oder nutzt FileReadLine mit dem Line-Parameter oder ähnliches was die Geschichte sehr langsam macht.
    Ich denke da kann man sicherlich ne ganze Menge optimieren.

  • Hallo!
    Wenn du in 30min NUR 26000 Zeilen einlesen kannst, dann frage ich mich wieviele Spalten jede Zeile hat??? Das müssen ja gigantische Ausmaße sein!
    Ich habe .csv Tabellen mit 65000 Zeilen zu 256 Spalten in etwa 5min Ausgelesen, Ausgewertet und zum Neuschreiben vorsortiert...

    Grüsse!

  • Hier der Code Ausschitt:

    Spoiler anzeigen
    [autoit]

    ;Einlesen der Preise Datei
    $line = 1
    $line_count = _FileCountLines($file_preise)
    $show_count = 100
    While($line <= $line_count)

    if $show_count = 0 Then
    ConsoleWrite($line&"/"&$line_count&@CRLF)
    $show_count = 100
    EndIf

    $read_line = FileReadLine($file_preise,$line)
    if $read_line <> "" Then $fileList_preise.add ($read_line)
    $line = $line +1
    $show_count = $show_count -1
    WEnd
    ; Ende einlesen der Preise Datei
    ;ENDE: Einlesen der Dateien

    [/autoit]

    Hier die Beschriftung der Zeilen DIESER Datei. (Sieht man auch die Anzahl)

    Spoiler anzeigen

    Artikelnr|Lieferantenname.|Lieferantenartikelnr.|Lieferantenbezeichnung|Menge|Preis|Preis-Datum|nächste Lieferung am|empfohlener VK|Herstellerartikelnr|PCS-Bezeichnung|Datenblatt|Bild|Lieferantennr

  • Du gibt bei FileReadLine an welche Zeile er lesen soll.
    Was passiert hierbei?:

    • Die Datei wird geöffnet
    • Die Datei wird Zeichen für Zeichen durchgegangen und die Anzahl der Zeilenumbruchzeichen gezählt.
    • Ist die gewünschte Zeile erreicht wird der Teil bis zum nächsten Zeilenumbruch zurückgegeben
    • Die Datei wird geschlossen


    All das passiert bei jedem einzelnen Aufruf von FileReadLine. - Immer wieder.
    Je weiter hinten die Zeile liegt desto länger dauert das ganze dann natürlich.
    Schau dir stattdessen mal an wie ich es in meinem Beispiel gemacht habe.
    Hierbei wird die Datei einmal geöffnet und schrittweise von Zeile zu Zeile abgearbeitet.
    Das bringt schonmal einen deutlichen Performance-Unterschied.

    Momentan arbeitest du mit einer ArrayList.
    Als du diese Zeiten von 20-30 Minuten erreicht hast - hast du da auch eine ArrayList verwendet oder ein Array?
    Wenn ja wie hast du das implementiert.

    Die eigentliche Frage die ich mir stelle ist: Brauchst du wirklich die kompletten Daten in einer Datenstruktur im Speicher?
    Wenn ich mir die Aufgabenstellung ansehe: "Update eine oder mehrere Datenbanken mit Daten einer CSV-Datei" sehe ich dafür keine Notwendigkeit.

  • #include <Array.au3>
    $fileList_preise = ObjCreate("System.Collections.ArrayList")
    siehe: Alternativen zum Array

    Ich habe eine Datenbank mit vielen Tabellen.
    Dann habe ich Exel Tabellen, in denen die Informationen anders aufgeteilt sind als in der Datenbank.
    Als nächstes muss ich schauen welche Änderungen vorgenommen wurden. Also was bei den Daten die auf die Dateien aufgeteilt sind anders ist, als bei denen in der Datenbank / in den Tabellen.

    Und zum Schluss muss ich die Datenbank updaten.

    Dafür dachte ich ich erstelle die SQL Befehle, schreibe sie (siehe link oben) in eine Warteschlange und füre diese später aus.

    Zitat

    Schau dir stattdessen mal an wie ich es in meinem Beispiel gemacht habe.

    Ich weiß nicht, ob ich blind bin, aber ich finde dein Bsp. nicht.

    Sorry, dass ich so viele Rechtschreibfehler drin habe, aber ich sitze noch im Geschäft und muss jetzt weg zu einem Termin. Heute Abend lese & schreibe ich wieder.

    Danke schonmal für die Hilfen.

  • Ich weiß nicht, ob ich blind bin, aber ich finde dein Bsp. nicht.

    Post Nr. 8

    Dafür dachte ich ich erstelle die SQL Befehle, schreibe sie (siehe link oben) in eine Warteschlange und füre diese später aus.

    Also kommt am Ende ein großer Befehlsstring dabei heraus?
    Wenn ja dann ist das effektivste wenn du statt einem Array oder ähnlichem einen String verwendest und bei jeder Änderung diesen einfach nur um jeweils die Befehle verlängerst.
    Die ArrayList ist nicht wirklich sehr performant. Sie hat nur Vorteile gegenüber einem AutoIt-Array beim Hinzufügen und Enfernen von Daten.
    Der Zugriff auf die Elemente ist hingegen langsamer als beim Array.

    Sicherlich weiß ich natürlich nicht genau im einzelnen was du alles machen musst und was du brauchst aber vielleicht konnten wir dir ein paar Anregungen geben wie du das ganze effektiver gestalten kannst.
    Ich persönlich bin immer noch der Meinung dass du dir die CSV-Datei nur einmal vorknöpfen musst aber das kannst nur du am besten beurteilen da ja nur du den wirklichen Einblick in dein Projekt hast.

  • Danke. Ich bin echt blind :D
    Werde mir das Script mal anschauen.
    Ich schaue mir mein Script und die Dateien mal an und stelle euch so viel wie möglich zur Verfügung. Dann ist es evtl leichter zu verstehen, ich muss nur schauen, dass keine Firmendaten an die Öffentlichkeit gelangen.

    Erstmal: Ich habe 2 Datenbanken.

    1. Die Datenbank unseres Warensystems (MySQL)
    2. Die Datenbank unseres Shops (MySQL)

    Die 2 Datenbanken sind von der Struktur ähnlich aufgebaut, nur dass in der Shop DB nur die nötigsten Informationen stehen.

    Und dann lade ich mir von unseren Großhändlern jeden Tag die aktuellen Preislisten und Artikelinformationen runter.

    Der Updater den ich derzeit schreibe soll die Shop Datenbank anhand der Dateien der Großhändler updaten, so dass die aktuellen Artikelinfos drin sind, die Preise passen und neue Artikel eingefügt bzw. alte gelöscht werden.

  • Also mal grundlegendes zur Performance und RAM Verbrauch:

    20mb RAM Verbrauch sind nicht wirklich viel und meiner Meinung nach akzeptabel, schon alleine deswegen, weil RAM Zugriffe bedeutend schneller als Festplattenzugriffe sind und du die Daten ja scheinbar noch aufbereiten und mit den SQL Tabelleninhalten abgleichen willst.


    Daher würde ich wie folgt vorgehen:

    1. Du brauchst ein Array. Da du von CSV Strukturen sprichst sinnvollerweise ein 2D Array. Dieses beinhalte den kompletten Dateiinhalt der jeweils relevanten Datei.
    2. Du liest die Datei in einem Rutsch mit _filereadtoarray ein und machst dir daraus ein 2D Array, Alternativ direkt per fileRead und stringsplit auf Zeilenumbrüche, was evtl ein kleiner Performace vorteil ist.
    3. Jetzt durchläufst du das CSV Array, bevorzugt nur einmal, da jeder Durchlauf wertvolle Zeit kostet.
    4. Während des Durchlaufs machst du deine Vergleiche und schreibst in die relevanten Tabellen sofern notwendig. Du kannst ja jeden CSV Datensatz problemlos mit multiplen Tabellen abgleichen.
    5. Sobald die CSV Datei abgearbeitet ist wird das 2D Array gelöscht und die nächste CSV eingelesen, RAM Verbrauch wächst nicht an und ist immer nur von der Dateigröße und Datensatzanzahl der aktuellen CSV abhängig, da der alte Inhalt der vorherigen Datei nicht mehr benötigt und gelöscht wird.

    Fertig, alle SQL Tabellen mit allen Dateien synchronisert.

    Hier ein Beispiel zum CSV einlesen und dem möglichen Aufbau im allgemeinen: Nicht getestet, auch nicht das CSV parsing, sorry keine Zeit gerade...

    Spoiler anzeigen
    [autoit]


    #include <array.au3>
    #include <file.au3>

    [/autoit] [autoit][/autoit] [autoit]

    Global $aCSVfileList = _FileListToArray(@ScriptDir,"*.csv",1) ; liste der CSV Filenames

    [/autoit] [autoit][/autoit] [autoit]

    for $i=1 To $aCSVfileList[0]
    _syncDB(@ScriptDir & "\" & $aCSVfileList[$i])
    Next

    [/autoit] [autoit][/autoit] [autoit]

    Func _syncDB($csvFile)
    Local $aCSV2D[1][1]
    _csvTo2DArray($csvFile,$aCSV2D)
    Local $tableString = _getTablesForCSV($csvFile) ; bsp: "Table1|Table3|Table11"

    ; sofern nicht schon beim einlesen würdest du dann hier das 2d Array durchlaufen und den SQL Abgleich vornehmen
    for $i = 0 To UBound($aCSV2D)-1
    for $j = 0 to ubound($aCSV2D,2)-1
    ;sqlstatements für die jeweilegen Datensätze, je nach tabelle vllt auch noch mit switch case oder vergleichbarem arbeiten
    _addSQL($aCSV2D[$i][$j],$tableString) ; dorf prüfste vorher ob vllt schon vorhanden, oder veraltet... und nutzt das korrekte sql statment
    Next
    Next


    EndFunc

    [/autoit] [autoit][/autoit] [autoit]

    Func _csvTo2DArray($csvFile,ByRef $a2D,$delim = ";")
    Local $aCSV
    _FileReadToArray($csvFile,$aCSV)
    ;_arraydisplay($aCSV) ; debuging

    Local $aTemp = StringSplit($aCSV[1],$delim)
    Local $col = $aTemp[0]

    ReDim $a2D[UBound($aCSV)-1][$col]

    For $i=1 To $aCSV[0]
    $aTemp = StringSplit($aCSV[$i],$delim)
    If $aTemp[0] < 2 Or $aTemp[0] > $col Then
    if $aTemp[0] < 2 Then ConsoleWrite("parse error: no split possible for " & $aCSV[$i] & @CRLF)
    if $aTemp[0] > $col Then ConsoleWrite("parse error: split returned more columns as allowed for " & $aCSV[$i] & @CRLF)
    ContinueLoop
    EndIf

    For $j=1 to $aTemp[0]
    $a2D[$i-1][$j-1]=$aTemp[$j]
    ; hier könnte btw auch schon der Abgleich mit den SQL Tabellen erfolgen,
    ; würde einen erneuten Arraydurchlauf sparen und das 2d Array wäre bereits nach Funktionsende überflüssig oder müsste garnichterst erstellt werden
    Next
    next
    ;_arraydisplay($a2D) ; debuging
    EndFunc

    [/autoit] [autoit][/autoit] [autoit]

    Func _getTablesForCSV($csvFile)
    switch $csvFile
    case @ScriptDir & "\preise.csv"
    $tables = "Table1|Table3|Table11"
    Case @ScriptDir & "\firmen.csv"
    $tables = "Table5|Table7"
    ;case ...
    endswitch

    [/autoit] [autoit][/autoit] [autoit]

    return $tables
    EndFunc

    [/autoit] [autoit][/autoit] [autoit]

    Func _addSQL($data,$tableString)
    $aTemp = StringSplit($tableString,"|")
    for $i = 1 to $aTemp[0] ; für jede relevante tabelle in Bezug auf die CSV
    ; prüfen und richtiges sqlstatement
    ; statemnts könnten in einem array gesammelt werden und dann in einem rutsch mit
    ; einer executeSQLstatemnts() Funktion evtl zusammengefasst und optimiert ausgeführt werden...
    Next
    EndFunc

    [/autoit] [autoit][/autoit] [autoit][/autoit]
  • Wow. Sieht sehr viel besser aus als meins :D
    eins meiner Probleme ist aber, dass ich z.B. wenn ein neuer Artikel in der Datei ist ihn in die Datenbank eintragen muss. und dafür müssen die Infos aus 2 Dateien in EINEM Insert stehen, da sonst eine Fehlermeldung kommt, da viele Felder nicht "null" sein dürfen.

    Einmal editiert, zuletzt von computerkoenig (28. September 2012 um 19:17)