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.
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: 14529
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: 14529
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