Datei auslesen und damit Linien zeichnen

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.
Antworten
MaxPotter
User
Beiträge: 3
Registriert: Montag 4. November 2019, 10:35

Hallo,
Ich habe gerade erst mit Python angefangen und möchte gerne X und Y Koordinaten aus einer Datei auslesen und als Striche visuell darstellen.

Meine Datei sieht so aus:

PU;
PA 205, 2270;
PD;
PA 233, 2270;
PU;
PA 205, 2320;
PD;
PA 205, 2270;
PU;
PA 205, 2470;
PD;
PA 205, 2420;
PU;
PA 219, 2270;
PD;
PA 219, 2470;
PU;
PA 233, 2270;
PD;
PA 233, 2320;
...

Die Zahlen oben und unten von dem PD gehören jeweils zusammen.

Alle Striche gehören zusammen und müssen auf ein Bild.

Habt ihr Tipps oder sogar ein beispiel Programm für mich ?
Benutzeravatar
__blackjack__
User
Beiträge: 14050
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MaxPotter: Das sieht aus als wenn man es ziemlich einfach nach Turtle-Grafik umsetzen kann — "PU" ist „pen up“, "PD" ist „pen down“, und "PA" bewegt die Schildkröte zu dem angegeben Punkt. Je nach dem ob der Stift auf dem Papier ist oder nicht, wird dabei eine Linie gezeichnet.

So würde ich das auch vom Grundsatz her angehen wenn man das ohne das `turtle`-Modul machen möchte. Also nicht versuchen eine PD-Zeile zu finden und sich dann die davor und danach anzuschauen, sondern die einfach der Reihe nach abarbeiten und sich den jeweiligen Zustand (Stift auf Papier?, aktuelle Koordinate, nächste Koordinate) merken.

Und das übliche beim Programmieren: ein grosses Problem in kleinere Teilprobleme zerlegen, und die wieder in Teilprobleme zerlegen, solange bis die einzelnen Teilprobleme so klein sind, dass man sie mit einer einfachen Funktion mit wenigen Zeilen lösen kann. Diese jeweilige Teillösung testen, und wenn sie funktioniert, mit der nächsten Teillösung weiter machen, solange bis man eine Gesamtlösung hat.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
/me
User
Beiträge: 3561
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

MaxPotter hat geschrieben: Montag 4. November 2019, 10:56 Habt ihr Tipps oder sogar ein beispiel Programm für mich ?
Du öffnest die Datei und liest sie zeilenweise durch. Anhand der ersten zwei Zeichen kannst du feststellen, was getan werden muss. PU und PD klingt übrigens nach "Pen Up" und "Pen Down". Wenn du PU hattest ist der Startpunkt der Linie durch die nächste PA-Zeile gegeben. Den Startpunkt merkst du dir und wenn dann nach einem PD die nächste PA-Zeile kommst kannst du deinen Strich zeichnen. Falls du nicht garantieren kannst, dass sich PU und PD immer abwechseln müsstest du den aktuellen Zielpunkt auch noch als neuen Startpunkt setzen.

Die Frage ist, mit welcher GUI du da umsetzen möchtest. Turtle-Grafik könnte hier eine Option sein.
MaxPotter
User
Beiträge: 3
Registriert: Montag 4. November 2019, 10:35

/me hat geschrieben: Montag 4. November 2019, 11:59
MaxPotter hat geschrieben: Montag 4. November 2019, 10:56 Habt ihr Tipps oder sogar ein beispiel Programm für mich ?
Du öffnest die Datei und liest sie zeilenweise durch. Anhand der ersten zwei Zeichen kannst du feststellen, was getan werden muss. PU und PD klingt übrigens nach "Pen Up" und "Pen Down". Wenn du PU hattest ist der Startpunkt der Linie durch die nächste PA-Zeile gegeben. Den Startpunkt merkst du dir und wenn dann nach einem PD die nächste PA-Zeile kommst kannst du deinen Strich zeichnen. Falls du nicht garantieren kannst, dass sich PU und PD immer abwechseln müsstest du den aktuellen Zielpunkt auch noch als neuen Startpunkt setzen.

Die Frage ist, mit welcher GUI du da umsetzen möchtest. Turtle-Grafik könnte hier eine Option sein.
Naja da ich X und Y Koordinaten habe dachte ich, ich mach es mit tkinter. Mein momentanes Problem ist das einzelne einlesen der Zeilen und die zuordnung welche zahl jetzt X und welche Y ist.
Benutzeravatar
/me
User
Beiträge: 3561
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Das Einlesen der Zeilen ist einfach.

Code: Alles auswählen

with open('hier dateiname eintragen') as lines:
    for line in lines:
        print(line)
Jetzt hast du jeweils eine einzelne Zeile und kannst die analysieren. Die split-Methode eines Strings kann dir da gute Dienste leisten. Reguläre Ausdrücke gehen natürlich auch, aber damit machen wir ein neues Fass auf.
Benutzeravatar
__blackjack__
User
Beiträge: 14050
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MaxPotter: Wie gesagt: Auf Teilprobleme runterbrechen. Das einlesen der Zeilen ist ganz einfach weil Dateiobjekte iterierbar sind und dabei die einzelnen Zeilen liefern.

Code: Alles auswählen

#!/usr/bin/env python3


def main():
    with open("test.txt", encoding="ascii") as lines:
        for line in lines:
            print(line)


if __name__ == "__main__":
    main()
Im nächsten Schritt überlegt man sich dann welche Informationen in den Zeilen stecken und wie man eine Zeile in eine möglichst gleichförmige Datenstruktur überführen kann die diese Informationen enthält. Das wäre beispielsweise ein Tupel aus dem Kommando und einer Liste mit den Zahlen. Wobei nicht jedes Kommando Zahlen hat — dann bleibt die Liste leer. Sie sollte aber trotzdem da sein, das vereinfacht das weiterverarbeiten. Dazu schreibt man sich dann eine Funktion die aus einer gegeben Zeile so ein Tupel erstellt.

Code: Alles auswählen

#!/usr/bin/env python3

COMMAND_NAME_TO_ARGUMENT_COUNT = {"PA": 2, "PD": 0, "PU": 0}


def parse_line(line):
    """
    Parse given input line into a tupel of command name and arguments.
    
    Raises:
        ValueError: if the line doesn't end with ";", or the argument(s) can't
            be parsed as numbers, or has the wrong number of arguments.

        KeyError: if the command name is unknown.
    
    Examples:
        >>> parse_line("PU;")
        ('PU', [])
        >>> parse_line("PA 23, 42;")
        ('PA', [23, 42])
    """
    #
    # TODO Hier kommt der Code hin der das macht was der Docstring beschreibt.
    #
    return (name, arguments)


def main():
    with open("test.txt", encoding="ascii") as lines:
        for name, arguments in map(parse_line, lines):
            print(f"Kommando: {name!r}, Argumente: {arguments!r}")


if __name__ == "__main__":
    main()
Die Ausgabe für Deine Beispieleingabe sieht dann so aus:

Code: Alles auswählen

Kommando: 'PU', Argumente: []
Kommando: 'PA', Argumente: [205, 2270]
Kommando: 'PD', Argumente: []
Kommando: 'PA', Argumente: [233, 2270]
Kommando: 'PU', Argumente: []
Kommando: 'PA', Argumente: [205, 2320]
Kommando: 'PD', Argumente: []
Kommando: 'PA', Argumente: [205, 2270]
Kommando: 'PU', Argumente: []
Kommando: 'PA', Argumente: [205, 2470]
Kommando: 'PD', Argumente: []
Kommando: 'PA', Argumente: [205, 2420]
Kommando: 'PU', Argumente: []
Kommando: 'PA', Argumente: [219, 2270]
Kommando: 'PD', Argumente: []
Kommando: 'PA', Argumente: [219, 2470]
Kommando: 'PU', Argumente: []
Kommando: 'PA', Argumente: [233, 2270]
Kommando: 'PD', Argumente: []
Kommando: 'PA', Argumente: [233, 2320]
Als nächstes muss man sich überlegen wie man mit dem Strom von (name, argumente)-Tupeln dem Ziel mit den Linien näher kommt. Eine Linie wird durch ihre beiden Endpunkte beschrieben. Liesse sich als Tupel mit Koordinatentupeln beschreiben: ((x₁, y₁), (x₂, y₂)). Also könnte man eine Funktion gebrauchen die aus einer Folge von Kommandotupeln, eine Folge von Linienkoordinatentupel erstellt.

Code: Alles auswählen

#!/usr/bin/env python3

COMMAND_NAME_TO_ARGUMENT_COUNT = {"PA": 2, "PD": 0, "PU": 0}


def parse_line(line):
    """
    Parse given input line into a tupel of command name and arguments.
    
    Raises:
        ValueError: if the line doesn't end with ";", or the argument(s) can't
            be parsed as numbers, or has the wrong number of arguments.

        KeyError: if the command name is unknown.
    
    Examples:
        >>> parse_line("PU;")
        ('PU', [])
        >>> parse_line("PA 23, 42;")
        ('PA', [23, 42])
    """
    #
    # TODO Hier kommt der Code hin der das macht was der Docstring beschreibt.
    #
    return (name, arguments)


def process_commands(commands):
    """
    Turn iterable of commands into an iterable of line data in the form of
    tuples with start and end points: ((x₁, y₁), (x₂, y₂)).
    
    Examples:
        >>> list(process_commands([("PD", []), ("PA", [10, 20])]))
        [((0, 0), (10, 20))]
        >>> commands = [("PU", []), ("PA", [10, 20]), ("PD", []), ("PA", [30, 40])]
        >>> list(process_commands(commands))
        [((10, 20), (30, 40))]
    """
    #
    # TODO Hier kommt der Code hin der das macht was der Docstring beschreibt.
    #


def main():
    with open("test.txt", encoding="ascii") as lines:
        for line in process_commands(map(parse_line, lines)):
            print(f"Linie: {line!r}")


if __name__ == "__main__":
    main()
Die Ausgabe von dem Programm sieht dann so aus:

Code: Alles auswählen

Linie: ((205, 2270), (233, 2270))
Linie: ((205, 2320), (205, 2270))
Linie: ((205, 2470), (205, 2420))
Linie: ((219, 2270), (219, 2470))
Linie: ((233, 2270), (233, 2320))
Und dann wäre man auch schon dabei GUI-Code zu schreiben der dann Linien auf einem `tkinter.Canvas` zeichnet.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
MaxPotter
User
Beiträge: 3
Registriert: Montag 4. November 2019, 10:35

@__blackjack__
Erstmal vielen danke für die tolle Antwort, nur leider verstehe ich davon nicht so viel.
Okay also alles in kleine schritte runter brechen:

Dann wehre mein Plan Das ich jede Zeile einzeln einlese und dann bei jeder Zeile einzeln die ersten beiden Buchstaben anschaue.
Damit ich dann sagen kann, wenn PD dann schaue die zahlen darüber an und benutze sie als x1 und y1 und dann schaue die zahlen darunter an und benutze sie als x2 und y2.
Daraufhin kann ich dann mit den x1,y1 und x2,y2 Koordinaten in "tkinter.Canvas" eine nach der anderen Linie zeichnen.

Soweit der Plan, nur kann ich es einfach nicht umsetzen, ich bin heute schon den dritten tag daran und schaffe es noch nicht mal eine nach der anderen Zeile einzulesen.
Ich dachte ich könne es so machen das ich einfach sage schau dir Zeile 1 an, bearbeite Diese und setzt dann eine variable +1 damit er dann im nächsten Durchgang Zeile 2 bearbeitet.

Es ist auch eigentlich nur ein einmaliges ding weshalb ich auch nicht besonders viel lust habe extra dafür Python zu lernen.
Benutzeravatar
__blackjack__
User
Beiträge: 14050
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MaxPotter: Von dem Ansatz habe ich ja schon mal abgeraten sich irgendwelche Zeilen davor anschauen zu müssen. Wenn man das so schreibt das man sich immer nur die aktuelle Zeile anschauen muss, wird das einfacher, ohne komische Sonderfälle die man beachten muss, oder hoffen muss, das die nie in den Daten vorkommen können.

Eine nach der anderen Zeile einlesen ist die erste Iteration der Programmentwicklung in meinem Beitrag gewesen und /me hat das gleiche im Beitrag davor schon gezeigt. Und wie gesagt, es reicht, und ist einfacher, sich immer nur die aktuelle Zeile anzuschauen und zu verarbeiten, ohne auf vorhergehende Zeilen oder gar schon auf folgende Zeilen zugreifen zu wollen. Du musst Dir überlegen was Du bei jedem Kommando aus der Datei an Zustand brauchst und wie das Kommando den ändert. "PU" und "PD" ändern den ”Stift”, ob der auf dem Papier ist oder nicht. Das kann man sich mit einem Wahrheitswert merken, den man beispielsweise `is_pen_down` nennen kann. Bei "PU" bekommt der den Wert `False` bei "PD" den Wert `True`. Bei "PA" muss man wenn `is_pen_down` wahr ist eine Linie von der aktuellen Stiftposition zu den Koordinaten hinter dem "PA" ziehen. In jedem Fall, also unabhängig von `is_pen_down`, werden nach Abarbeitung des "PA" die Koordinaten davon zur aktuellen Stiftposition. Das war's eigentlich auch schon um aus Kommandos Linien zu machen.

Im Kern macht das die Generatorfunktion die ich `process_commands()` genannt habe, nur das die keine Zeilen bekommt und Linien zeichnet, sondern die Kommandos und die Liniendaten als Tupel bekommt und liefert. Eben um das als einzelnen, in sich geschlossenen, und leicht separat entwickel- und testbaren Teil zu haben. Die Funktion besteht aus 14 Zeilen Code (und 8 lokale Namen) der im Grunde 1:1 das macht was im letzten Absatz in Worten steht.

Die Anzahl der Namen habe ich erwähnt, weil der Mensch laut Studien so 5 bis 7 Dinge gleichzeitig im Kopf auseinanderhalten kann, was zu der Faustregel führt das man in einer Funktion möglichst nicht mehr als 10 verschiedene Namen haben sollte, weil es sonst schwerer für den Leser wird die Funktion zu verstehen. Und derjenige der den Code schreibt, ist letztendlich ja auch selbst Leser, denn er muss ja verstehen was er da schreibt.

Ich erwähne das weil ich wirklich wichtig finde das gerade Anfänger eher zu viele kleine Funktionen schreiben als zu wenige. Wenn Du mit einer Schleife anfängst die die Zeilen liest, darin Code schreibst der erkennt womit die Zeichenkette anfängt, und in dieser Schleife dann den Zustand des virtuellen Plotters aktualisierst, und gegebenfalls eine Linie auf einem `Canvas` zeichnest, dann wird das am Ende viel zu unübersichtlich und man kann das auch nicht gut testen, weil man immer eine Datei und eine GUI braucht um auch nur *irgendwas* davon zu testen. Um das umwandeln der Kommandos in Linien(daten) zu testen sollte man aber beispielsweise weder eine Datei noch eine GUI benötigen. Das sieht man ja an den Beispielen im Docstring der `process_commands()`-Funktion. Die ist vom Format der Zeilen in der Datei und einer GUI entkoppelt. Der ist egal woher die Daten kommen, das könnten auch Plotterkommandos sein die aus einer Datei mit einem anderen Format kommen. Und der Funktion ist egal was mit den Daten danach gemacht wird.

Wenn man etwas in einer Programmiersprache umsetzen möchte, dann muss man die wohl oder übel lernen. Wüsste nicht wie es anders geht. Wenn Du dazu keine Lust hast, ist die Frage wie gross die Motivation ist das Problem zu lösen und ob man von dieser Motivation etwas ins lernen der Programmiersprache transferieren kann.

Nun habe ich überlegt ob man das Format was Du da hast nicht vielleicht leicht in ein Standardformat wandeln kann für das es schon Betrachter und Konverter gibt, um weniger selbst programmieren zu müssen, und stellte fest, dass Dein Beispiel *verdammt* nach HP-GL aussieht. *Dafür* gibt es schon Software zum anschauen und umwandeln in andere Formate wie Bilder, PDF, SVG. Das freie Vektorgrafik-Programm Inkscape kann beispielsweise HP-GL-Dateien laden.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
__blackjack__
User
Beiträge: 14050
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Falls das wirklich HP-GL ist, wäre das selbst programmieren übrigens ein wenig komplizierter, denn das Format ist ein kleines bisschen komplexer als Dein Beispiel vermuten lässt. Ich bin da beispielsweise vom Beispiel her davon ausgegangen das es einen Befehl pro Zeile gibt, und zwischen Befehl und Koordinaten mindestens ein Leerzeichen steht, und das es nur bei "PA" Koordinaten geben kann, und das dort auch nur genau zwei Werte stehen können. Alles Vermutungen die in HP-GL nicht zutreffen. Dein Beispiel kann man in einer Zeile und mit nur einem "PA" auch so schreiben:

PA;PU205,2320;PD205,2270,233,2270,233,2320;PU205,2470;PD205,2420;PU219,2270;PD219,2470;
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Antworten