txt nach csv Konverter

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
higginsa1
User
Beiträge: 11
Registriert: Freitag 16. Juni 2017, 07:24

gebe dir ja recht das die metriken nicht serientauglich sind, darauf kommt es mir aber jetzt nicht an. Übrigens umfasst OutLine nur eine Zeile und zwar die neue die ausgegeben werden soll, ist aber auch egal

Gibt es in Pythen einen einfachen Befehl, welcher mir die Anzahl meines gesuchten Zeichens in einer Zeichenkette ausgibt?
BlackJack

@higginsa1: Nein, einen Befehl gibt's nicht, aber Zeichenketten haben eine Methode die das macht.

Da Du laut den Beispielen aber letztendlich auch an den Werten interessiert bist, würde ich das `re`-Modul empfehlen, damit kannst Du das erkennen einer Start-/Endzeile und heraus holen der Teilzeichenketten die von Interesse sind, in einem Schritt machen. Zudem wird der Test auch ein klein wenig genauer wenn Du nach Plus oder Minus, gefolgt von einem oder mehr Leerzeichen, gefolgt von einer oder mehr Ziffern, gefolgt von einem Komma, gefolgt von genau zwei Ziffern suchst, als wenn nur ein Plus oder Minus und ein Komma *irgendwo* in der Zeile vorkommen.
higginsa1
User
Beiträge: 11
Registriert: Freitag 16. Juni 2017, 07:24

das re Modul werde ich mir auch noch anschauen, vorerst habe ich folgenden Code, macht auch schon annähernd was es soll.

Folgende Probleme sind noch da...
... er packt das Line Feed mit in die OutLine, dieses würde ich gerne mit ";" ersetzen
... danach werde ich noch etwas Text ersetzen, mit re.replace

dann sollte es schon passen

In der nächsten Ausbaustufe werde ich eine exe erstellen mit 2 Übergabeparametern Filename und Jahr

Sorry dass ich nur ab und zu schreibe, bin nebenbei im Garten am schippen.

Code: Alles auswählen

def numberElements(InString, SearchString):
    RetVal = 0

    for x in range(0, (len(InString)-1)):
       if InString[x] == SearchString:
           RetVal += 1

    return RetVal


def convertFile():
    txt_file = open("PB_KAZ_KtoNr_0790130119_04-06-2016_0709.txt", "r")
    csv_file = open("PB_KAZ_KtoNr_0790130119_04-06-2016_0709.csv", "w")

    LineCnt = 1
    OutLine = ""
    FoundEntry = False

    for InLine in txt_file:
        # analyse line
        numberPlus = numberElements(InLine, "+")
        numberMinus = numberElements(InLine, "-")
        numberComma = numberElements(InLine, ",")
        numberFullStop = numberElements(InLine, ".")

        # start of entry
        if (numberPlus > 0 or numberMinus > 0) and numberComma > 0:
            FoundEntry = True

        # assemble outline
        if FoundEntry == True:
            OutLine += InLine

        #end of entry
        if (numberFullStop > 3):
            csv_file.write(OutLine)
            print(OutLine)
            OutLine = ""
            FoundEntry = False

        LineCnt = LineCnt + 1

    txt_file.close()
    csv_file.close()
Zuletzt geändert von Anonymous am Samstag 17. Juni 2017, 12:01, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
higginsa1
User
Beiträge: 11
Registriert: Freitag 16. Juni 2017, 07:24

OutLine += InLine.replace("\n", ";")
BlackJack

@higginsa1: Die `numberElements()`-Funktion ist überflüssig, so etwas gibt es wie schon gesagt als Methode auf Zeichenketten.

Zudem ist die extremst umständlich geschrieben und allgemein sogar falsch, weil das letzte Zeichen nicht berücksichtigt wird.

``for i in range(len(sequenze)):`` ist in Python ein „anti pattern“. Man kann *direkt* über die Elemente von Sequenzen wie Zeichenketten, Listen, … iterieren, da braucht man keinen Umweg über einen Index.

`RetVal` ist eine scheussliche Komposition aus zwei Abkürzungen. Wenn man es schon supergenerisch haben möchte, dann bietet sich `result` an.

Was soll `InString` bedeuten? Das `In` macht da keinen Sinn.

`numberElements` ist auch kein guter Name. Das würde ich mit „nummeriere Elemente“ übersetzen, hier wird aber etwas gezählt, also `count_elements()`.

Zählen brauchst Du doch auch nur bei Punkten. Bei den anderen Zeichen reicht ein einfacher Test mit ``in`` ob sie vorhanden sind oder nicht.

`LineCnt` ist immer noch mit dem Namen da, und immer noch manuell hochgezählt. (Und immer noch nicht verwendet.)

Bei den `numberThing`-Werten habe ich auch ein wenig Bauchschmerzen bei den Namen, denn das müsste eigentlich `number_of_things` heissen, oder etwas weniger umständlich `thing_count`. `number_of_plusses` vs. `plus_count`.

Und wie ebenfalls schon gesagt: Bastel da nicht selbst schon eine CSV-Zeile zusammen, sondern trenne das einlesen und das schreiben in die Zieldatei. CSV-Dateien möchte man nicht selber schreiben, denn ganz so einfach wie man denkt sind die nicht. Nichts hindert jemanden beim Überweisungstext ein Semikolon oder Anführungszeichen zu schreiben. Das `csv`-Modul kümmert sich um solche Sachen.

Die Funktion wird noch komplexer werden, da sollte man jetzt schon Sachen rausnehmen die thematisch getrennt sind. Du willst zudem nicht mit `replace()` auf der Ausgabezeile rumpfuschen, sondern auf den einzelnen Teilen wenn sie verarbeitet werden und *bevor* sie in der Ausgabezeile beziehungsweise in der oder den Datenstrukturen landen. Die Zeilen zwischen Start- und Endzeile muss man ja erst einmal sammeln und dann zusammenfügen bevor man sie als *einen* Wert zum Ausgabedatensatz hinzufügt.

Wobei sich das mit dem `replace()` sowieso komisch anhört und mit regulären Ausdrücken auch hinfällig wird. Denn eigentlich will man ja nichts entfernen, sondern gezielt genau das aus der Zeile holen was einen interessiert.

`FoundEntry` ist im Grunde redundant wenn man `OutLine` den Wert `None` zuweist wenn man sich ausserhalb einer Kontobewegung befindet und darauf dann testet.
BlackJack

Hier mal der Ablauf des Progrmmteils zum Parsen als Diagramm:
Bild
Ich hab das mal spasseshalber in Io eher funktional in ca. 100 Zeilen implementiert. Die `parseAccountActivitiesFile()`-”Methode” ist mir dabei aber schon fast zu gross. Das schreit förmlich nach eigenen Datentypen, mindestens für den Parser und eine `AccountActivity`.

Aus dem hier (das Argument für's Jahr war offensichtlich 1986):
[codebox=text file=Unbenannt.txt]Testdatei
=========

Gutschr.SEPA +42,23
Max Müller Referenz ryxsdfasfwef Auftraggeber
Identifikation sdfgsdfg Verwendungszweck
Klassenkasse5x Holger Schmidt
17.05. 17.05.

Blah. Hauptsache nix mit + oder - und Ziffern und , in der ”falschen”
Reihenfolge

Gutschr.SEPA + 10,00
Beim Verwendungszweck ist wichtig, dass nichts vorkommt das wie die Endzeile
aussieht. Wo ich nicht glaube, dass das bei diesem Format garantiert ist.
17.05. 16.05.

Zeile Müll 1
Zeile Müll 2
Zeile Müll 3

D Gut SEPA - 20,00
Zeile 1
Zeile 2
Zeile 3
17.05. 18.05.
[/code]
Macht das Programm das hier:
[codebox=text file=Unbenannt.txt]+42,23;Max Müller Referenz ryxsdfasfwef Auftraggeber Identifikation sdfgsdfg Verwendungszweck Klassenkasse5x Holger Schmidt;17.05.1986;17.05.1986
+10,00;Beim Verwendungszweck ist wichtig, dass nichts vorkommt das wie die Endzeile aussieht. Wo ich nicht glaube, dass das bei diesem Format garantiert ist.;16.05.1986;16.05.1986
-20,00;Zeile 1 Zeile 2 Zeile 3;18.05.1986;18.05.1986[/code]
higginsa1
User
Beiträge: 11
Registriert: Freitag 16. Juni 2017, 07:24

Danke für die ausführliche Antwort, noch mal kurz zum Hintergrund, ich habe 15 Kontoauszüge mit ca. 20 Buchungen in PDF, diese habe ich mir als Text Datei abgespeichert. Diese Infos möchte ich mir in Excel importieren.

Von Hand hätte ich die Infos schon längst rüber kopiert. Darum geht es mir aber nicht, ich möchte Python kennen lernen, ursprünglich komme ich aus der C Fraktion. Deshalb auch len(x)-1, sonst zählst Du über das Array.

Die Namen sind mir jetzt Wurst, wenn die Daten in Excel sind dann stampfe ich das Script wieder ein.

Mir ist klar, dass jemand mit seinem Buchungstext mein Scipt durcheinander bringen kann. Mir fällt für die aktuelle Situation nichts besseres ein. Dir?

Aber darum geht es mir auch, wenn es Python einfacher geht, dann bitte ein Beispiel wie ich die numberElements() umgehen kann.

Bis auf das Datum zu ergänzen macht mein Skipt übrigens das gleiche...
__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

0-basierte Indizes hat auch Python, weshalb range dein -1 schon implizit macht. Das ist also schonmal ein Fehler.

Und es ist jetzt schon diverse Male gesagt worden, aber reguläre Ausdrücke lösen dein Problem in diesem Fall deutlich besser. Nicht zuletzt weil sie Reihenfolgen beibehalten, und damit die Möglichkeit eins false positives im Buchungstext reduziert wird.

Ungetestet:

Code: Alles auswählen

m = re.match(r"^\d\d?\.\d\d?\.$", line.strip())
if m:
    ....
__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und jetzt noch mal getestet (und repariert...)

Code: Alles auswählen

import re


DATA = """Zeile 3
17.05. 18.05.
"""

def is_end_line(line):
    return bool(re.match(r"^\d\d?\.\d\d?\. \d\d?\.\d\d?\.$", line))


def main():
    for line in DATA.split("\n"):
        print(line, is_end_line(line))

if __name__ == '__main__':
    main()
BlackJack

@higgins1a: Namen sollten nicht egal sein. Wegwerfskripte haben oft die Angewohnheit nicht sterben zu wollen und man löscht die ja auch meistens nicht. Und dann kommt in ein oder zwei Jahren wieder ein PDF, welches genau so oder so ähnlich aussieht, und man erinnert sich das man da ja schon irgendwas hatte, und schon muss man sich in etwas mit schlechten Namen einarbeiten. Was einfacher wäre, wenn man dann nicht überlegen müsste was die Namen bedeuten, oder noch schlimmer wenn man keine Fehler machen würde, weil man denkt man weiss was ein Name bedeutet, aber falsch liegt.

Zumal es auch wenig Sinn macht Python lernen zu wollen, aber dann nicht auch die Namenskonventionen zu lernen. Was man auch in echten Wegwerfskripten üben kann.

Zumindest ein bisschen besser wäre es wenn man die Endzeile nicht nur an der Anzahl der Punkte erkennt, sondern mindestens einen regulären Ausdruck verwendet, der die Datumsangaben parst. Noch besser wenn man dann danach noch überprüft, ob es zusammen mit der Jahreszahl auch in beiden Fällen ein gültiges Datum ist, also da beispielsweise nicht der '29.02.2017' steht, was man mit regulären Ausdrücken alleine ja nicht erkennen kann. Das kann man in Python sehr einfach mit dem `datetime`-Modul machen, in dem man einfach versucht das Datum in ein `datetime.datetime`-Objekt zu parsen.

Noch mal zum `numberElements()` wie man das in Python machen würde, wenn Zeichenketten (und andere Sequenztypen) dafür nicht bereits eine Methode hätten. Deine Funktion mit besseren Namen, nicht nur was die Schreibweise angeht, sondern auch generischer, denn das funktioniert mit allen Sequenztypen und nicht nur mit Zeichenketten:

Code: Alles auswählen

def item_count(sequence, needle):
    result = 0
 
    for i in range(len(sequence)):
        if sequence[i] == needle:
            result += 1
 
    return result
Ich habe das `item_count()` statt `element_count()` genannt, weil `item` in Python wesentlich üblicher als `element` ist, sowohl in der Dokumentation als auch in der API. Das wäre ein Beispiel warum Namensgebung auch zum lernen einer Sprache gehört.

Die Funktion ist ”unpythonisch” wegen dem unnötigen Laufindex. Gerade wenn man von Sprachen kommt in denen viel mit Indexwerten hantiert wird, muss man in Python umdenken, denn da geht es fast immer ohne Index, und sollte man einen Index oder eine laufende Zahl *zusätzlich* zum Element in einer Iteration benötigen, dann gibt es dafür die `enumerate()`-Funktion. Ohne Laufindex:

Code: Alles auswählen

def item_count(iterable, needle):
    result = 0
 
    for item in iterable:
        if item == needle:
            result += 1

    return result
Hier konnte ich jetzt `sequence` in `iterable` umbenennen weil das Argument keine Sequenz mehr sein muss, sondern ein beliebiges iterierbares Objekt sein kann. Einzige praktische Einschränkung: Es sollte ein endlich iterierbares Objekt sein, sonst wartet man *sehr* lange auf das Ergebnis. ;-) Auch hier ist ein überlegen bezüglich des Namens wieder nicht ganz unwichtig beim lernen der Sprache.

Mit der `sum()`-Funktion und einem Generatorausdruck kann man das nun auf eine Zeile zusammenschrumpfen lassen:

Code: Alles auswählen

def item_count(iterable, needle):
    return sum((1 if item == needle else 0) for item in iterable)
Da `bool` von `int` abgeleitet ist und `True` als ganze Zahl den Wert 1 und `False` den Wert 0 hat, kann das sogar noch ein kleines bisschen verkürzen:

Code: Alles auswählen

def item_count(iterable, needle):
    return sum(item == needle for item in iterable)
Aber wie schon gesagt: Sequenztypen, und damit auch Zeichenketten, haben dafür schon eine Methode.

Auch in einem Wegwerfskript würde ich die beiden Zustände (ausserhalb einer Kontobewegung und innerhalb einer Kontobewegung) im Code deutlich trennen. Deine drei ``if``-Bedingungen funktionieren zwar, aber das das so ist, muss man erst überlegen.

Und ich würde hier möglichst defensiv programmieren. Io hat nichts für CSV-Dateien in der Standardbibliothek, aber ich habe nicht einfach nur einen `join()` mit dem Feldtrenner gemacht, sondern ich teste vorher ob der Feldtrenner oder Anführungszeichen in den Werten vorkommen und löse dann eine Ausnahme aus, damit so etwas nicht lautlos durchrutscht und die kaputten Daten erst später Probleme bereiten. Aber wie auch schon mehrfach gesagt: In Python gibt es schon etwas für CSV-Dateien in der Standardbibliothek was mit Trennern und Anführungszeichen in Feldern klar kommt.

Hier ist die Io-Funktion, die nach dem Diagramm entstanden ist:
[codebox=io file=Unbenannt.txt]parseAccountActivitiesFile := method(fileName, year,
rows := List clone

parseDate := method(string, Date fromString(string .. year, dateFormat))

file := fileName asFile openForReading
error := try(
amount := nil
reasonLines := nil
file foreachLine(i, line,
if(amount isNil) then(
startLineMatch := line findRegex(amountRegex)
if(startLineMatch) then(
amount = startLineMatch at(1) .. startLineMatch at(2)
reasonLines = List clone
)
// Lines between account activities are ignored here.
) else(
endLineMatch := line findRegex(datesRegex)
if(endLineMatch) then(
dateA := parseDate(endLineMatch at(1))
dateB := parseDate(endLineMatch at(2))
if((dateA asNumber != -1) and(dateB asNumber != -1)) then(
rows append(
List with(
amount,
reasonLines join(" "),
dateA asString(dateFormat),
dateB asString(dateFormat)
)
)
amount = nil
reasonLines = nil
)
) else(
reasonLines append(line strip)
)
)
)
)
file close
error pass

amount ifNonNil(ParseError raise("missing end line at " .. (i + 1)))

rows
)[/code]
Das ist schon ziemlich lang für *eine* Funktion. IMHO schon zu lang. Aber man könnte das fast 1:1 in Python umschreiben.

`amount` und `reasonLines` gehören eigentlich eng zusammen wie man im Code sehen kann. Und `dateA` und `dateB` gehören da eigentlich auch dazu, was man im Code nicht so gut sehen kann. Also mindestens die Werte, die zu einer Kontobewegung gehören, würde ich in einer `AccountActivity` Klasse zusammenfassen.

Ich habe noch eine Lösung in CoffeeScript geschrieben, da sieht die `AccountActivity`-Klasse zum Sammeln der Daten für eine Kontobewegung so aus:
[codebox=coffeescript file=Unbenannt.coffee]class AccountActivity

@DATE_FORMAT: 'DD.MM.YYYY'

@parseDate: (string) => new moment string, @DATE_FORMAT

constructor: (@amount, @reasonLines = [], @dateA, @dateB) ->

addReasonLine: (line) => @reasonLines.push line

asRow: => [
@amount,
@reasonLines.join(' '),
@dateA.format(@constructor.DATE_FORMAT),
@dateB.format(@constructor.DATE_FORMAT),
][/code]
Und `parseAccountActivitiesFile()` aus dem Io-Programm ist in der CoffeeScript-Lösung in einer `AccountActivityParser`-Klasse auf mehrere Methoden verteilt. Es gibt zwar auch bei Node.js nichts für CSV-Dateien in der Standardbibliothek, aber mit ``npm`` ist schnell etwas installiert. `csv-fast` in dem Fall. Und es gibt mit `argparse` eine Portierung vom `argparse`-Modul aus der Python-Standardbibliothek. Das CoffeeScript-Programm hat also eine nette Kommandozeilen-API mit generierter Hilfe:
[codebox=text file=Unbenannt.txt]$ ./test.coffee
usage: test.coffee [-h] [-v] filename [year]
test.coffee: error: too few arguments
$ ./test.coffee -h
usage: test.coffee [-h] [-v] filename [year]

Convert account activity text file to CSV file.

Positional arguments:
filename filename of the text file with account activities.
year year of dates in the text file. (default: current year)

Optional arguments:
-h, --help Show this help message and exit.
-v, --version Show program's version number and exit.
$ ./test.coffee --version
0.0.1[/code]
Antworten