Uhrzeiten/Tag der Gezeiten aus Datei auslesen?

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
raspPy
User
Beiträge: 15
Registriert: Dienstag 15. September 2020, 13:54

Hi,

ich würde gerne die Daten für die Gezeiten vom BSH nutzen. Da kann sich jeder eine TXT-Datei herunterladen. Ich möchte die Date nutzen, um mit einem NeoPixel-Streifen die Gezeiten als eine Art Kalender anzeigen zu lassen. Dafür will ich einen ESP oder RasPi Pico mit Micropython nutzen. Ich dachte, ich nutze einen NTP-Server für die aktuelle Zeit und Datum und suche dann in der Datei nach den Hoch und Tiefstand des Wassers. Die TXT sieht wie folgt aus:

Code: Alles auswählen

I_E#DE__798P2023#
A01#Hersteller :#M1103/BSH-Hamburg, 29.09.2021  08:31:00#
A02#Daten-Art  :#Vorausberechnungen#
A03#PegelNr.   :#DE__798P#
A04#GT-Name    :#Borkum, S¸dstrand#
A06#GT-Jahr    :#      2023#
A07#Def.-Jahr  :#2020#19#
A08#Position   :#53∞34'37''N   6∞39'41''E WGS84#
A11#Zeitzone   :#UTC+ 1h00min (MEZ)#
A12#Position GK:# 25 43850.72 R  59 38584.00 H#
A13#Messstelle :#9340030 #
C01#Analyse-Ber:#selbstst‰ndig#        #39 39#
C02#Datenumfang:#Zeiten u. Hˆhen: HW NW      #
C03#Hˆhenniveau:#PNP#
C04#EinheitZeit:#StdMin#
C05#EinheitHˆhe:# m#
C12#VB-Methode :#HDdU (HW/NW)#
D01#PNP u. NHN :#- 5.02 #
D02#SKN u. NHN :#- 1.79 #
D03#SKN ¸. PNP :#  3.23 #
F01#MHWI       :# 9:29   #
F02#MNWI       :#15:54   #
G01#MHW        :#  6.10 #
G02#MNW        :#  3.76 #
LLL#
VB2#DE__798P# #H#So# 1. 1.2023# 5:39# 6.22 # #  1#+ 1:00#  25760#   1#2459945.693419#
VB2#DE__798P# #N#So# 1. 1.2023#12:08# 4.01 # #  1#+ 1:00#  25760#   2#2459945.963949#
VB2#DE__798P# #H#So# 1. 1.2023#18:26# 5.90 # #  1#+ 1:00#  25760#   3#2459946.226667#
VB2#DE__798P# #N#Mo# 2. 1.2023# 0:29# 4.02 # #  2#+ 1:00#  25760#   4#2459946.478812#
VB2#DE__798P# #H#Mo# 2. 1.2023# 6:46# 6.16 # #  2#+ 1:00#  25761#   1#2459946.740542#
VB2#DE__798P# #N#Mo# 2. 1.2023#13:13# 4.04 # #  2#+ 1:00#  25761#   2#2459947.008928#
VB2#DE__798P# #H#Mo# 2. 1.2023#19:29# 5.95 # #  2#+ 1:00#  25761#   3#2459947.270425#
VB2#DE__798P# #N#Di# 3. 1.2023# 1:42# 4.03 # #  3#+ 1:00#  25761#   4#2459947.529475#
VB2#DE__798P# #H#Di# 3. 1.2023# 7:53# 6.14 # #  3#+ 1:00#  25762#   1#2459947.787045#
VB2#DE__798P# #N#Di# 3. 1.2023#14:17# 4.05 # #  3#+ 1:00#  25762#   2#2459948.053569#
VB2#DE__798P# #H#Di# 3. 1.2023#20:28# 6.04 # #  3#+ 1:00#  25762#   3#2459948.310968

Mein Ansatz ist nach der Spalte mit den Zeiten zu suchen und dann die Zeilennummern als Tagesnummer im Jahr zu nutzen. Aber irgendwie ist das so umelegant. Habt ihr da eine Idee?

Mein Code bisher:

Code: Alles auswählen

import datetime

# open file for reading
with open('data.txt', 'r') as file:

  # read all lines
  lines = file.readlines()

  # get the last line starting with 'VB1'
  vb1_line = [line for line in lines if line.startswith('VB1')][-1]

  # extract the 7th column after LLL#
  time_strings = [x.strip() for x in vb1_line.split('LLL#')[-1].split('#') if x]
  time_strings = time_strings[6::7]

  # convert time strings to datetime objects
  times = [datetime.datetime.strptime(x, '%H:%M') for x in time_strings]

  # do something with the datetime objects, e.g. print them
  for time in times:
    print(time)
Benutzeravatar
Axel-WAK
User
Beiträge: 62
Registriert: Dienstag 29. November 2022, 11:52

Code: Alles auswählen

if line.startswith('VB1')

kommt doch gar nicht vor, nur VB2
OS: LMDE5 *** Homepage *** Github Seite
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

Eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht 2. Textdateien müssen immer mit dem richtigen Encoding geöffnet werden.
In der Textdatei steht VB2 statt VB1. Wenn Du die letzte Zeile suchst, dann ja immer für das Datum 31.12.
LLL# kommt dann in der Zeile auch gar nicht vor.
Da das Datum in Spalte 5 steht, ist es doch gar nicht nötig, das Datum über irgendeine Zeilennummer zu suchen (was Dein Code ja auch gar nicht enthält).
Was hat also Dein Code mit der Textdatei und dem Problem zu tun, das Du zu lösen versuchst?
raspPy
User
Beiträge: 15
Registriert: Dienstag 15. September 2020, 13:54

Axel-WAK hat geschrieben: Sonntag 14. Mai 2023, 14:09

Code: Alles auswählen

if line.startswith('VB1')

kommt doch gar nicht vor, nur VB2
Stimmt, sorry. Anscheinend nimmt das BSH je nach Ort andere X für VBX.
raspPy
User
Beiträge: 15
Registriert: Dienstag 15. September 2020, 13:54

Sirius3 hat geschrieben: Sonntag 14. Mai 2023, 14:15 Eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht 2. Textdateien müssen immer mit dem richtigen Encoding geöffnet werden.
In der Textdatei steht VB2 statt VB1. Wenn Du die letzte Zeile suchst, dann ja immer für das Datum 31.12.
LLL# kommt dann in der Zeile auch gar nicht vor.
Da das Datum in Spalte 5 steht, ist es doch gar nicht nötig, das Datum über irgendeine Zeilennummer zu suchen (was Dein Code ja auch gar nicht enthält).
Was hat also Dein Code mit der Textdatei und dem Problem zu tun, das Du zu lösen versuchst?
Ich bin nicht ganz fertig mit dem code, da mir meine Idee zu umständlich vorkommt.

Mein Gedanke war, dass ich nicht nach dem Datum suche sondern der Tag-Nummer, die für heute z.B. die 134 ist. Dann nehme ich die Zeilennummern der TXT, wozu ich "LLL#" als Beginn nehme und dann mich durch die Tag-Nummern hangele.

Wenn ich dich verstehe, dann soll ich folgendes tun?:
- Frage datum von heute ab
- suche Datum von heute in spalte 5
- nehme Hochwasser-Zeit
- nehme Niedrigwasser-Zeit
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@raspPy: Ich weiss nicht so recht was Du da mit Tagnummer und Zeilennummer meinst, denn es ist ja nicht so als wenn das ein Tag pro Zeile wäre.

Falls das "LLL#" den Anfang der Zeilen für die einzelnen Vorhersagen markiert, kann man sich daran schon orientieren, aber dann müsste man anfangen wirklich jede Zeile auszuwerten bis man zu Tag *und Zeit* kommt die relevant ist/sind.

Ich würde eine Funktion schreiben die eine Zeile parst und daraus die Werte extrahiert. Also mindestens Datum + Uhrzeit, Art (Hoch- oder Niedriegwasser). Dabei kann man dann vielleicht auch eine kleine Kontrolle des Wochentags einbauen ob der zum Datum passt.

Durch die geparsten Daten kann man dann mit `more_itertools.pairwise()` durchgehen, bis man die beiden Einträge hat wo die aktuelle Zeit dazwischen liegt, falls ich die Anforderung richtig verstanden habe. Oder brauchst Du alle Zeiten für den gewünschten Tag? Dann könnte man `itertools.groupby()` verwenden.

Die Kodierung sieht übrigens so aus, als hättest Du eine ISO-8859-15 kodierte Datei auf einem Mac geöffnet. Wie Sirius3 schon anmerkte: Bei Textdateien beim öffnen immer die Kodierung explizit angeben!

Und das ganze sollte in Funktionen stehen und nicht einfach so auf Modulebene.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
nezzcarth
User
Beiträge: 1635
Registriert: Samstag 16. April 2011, 12:47

Auf der Seite, von der du die Daten beziehst, gibt es auch eine Formatbeschreibung (https://www.bsh.de/DE/DATEN/Vorhersagen ... _node.html; im Tab "Downloadbereich" ganz unten).
Dort wird auch erklärt, was es mit VB1/VB2 auf sich hat. Im Prinzip ähnelt das Format einer CSV (bzw. DSV) Datei mit # als Spaltentrenner. Da das Format jedoch Zeilen- und nicht Spaltenbasiert ist und zudem eine variable Anzahl an Spalten aufweisen kann, schätze ich, dass das CSV-Modul nicht sinnvoll eingesetzt werden kann.
raspPy
User
Beiträge: 15
Registriert: Dienstag 15. September 2020, 13:54

__blackjack__ hat geschrieben: Sonntag 14. Mai 2023, 19:17 @raspPy: Ich weiss nicht so recht was Du da mit Tagnummer und Zeilennummer meinst, denn es ist ja nicht so als wenn das ein Tag pro Zeile wäre.

Falls das "LLL#" den Anfang der Zeilen für die einzelnen Vorhersagen markiert, kann man sich daran schon orientieren, aber dann müsste man anfangen wirklich jede Zeile auszuwerten bis man zu Tag *und Zeit* kommt die relevant ist/sind.

Ich würde eine Funktion schreiben die eine Zeile parst und daraus die Werte extrahiert. Also mindestens Datum + Uhrzeit, Art (Hoch- oder Niedriegwasser). Dabei kann man dann vielleicht auch eine kleine Kontrolle des Wochentags einbauen ob der zum Datum passt.

Durch die geparsten Daten kann man dann mit `more_itertools.pairwise()` durchgehen, bis man die beiden Einträge hat wo die aktuelle Zeit dazwischen liegt, falls ich die Anforderung richtig verstanden habe. Oder brauchst Du alle Zeiten für den gewünschten Tag? Dann könnte man `itertools.groupby()` verwenden.

Die Kodierung sieht übrigens so aus, als hättest Du eine ISO-8859-15 kodierte Datei auf einem Mac geöffnet. Wie Sirius3 schon anmerkte: Bei Textdateien beim öffnen immer die Kodierung explizit angeben!

Und das ganze sollte in Funktionen stehen und nicht einfach so auf Modulebene.
Ich meine mit "Tagnummer" das gleiche wie mit Kalenderwoche nur für Tagen. Der 01.01. ist der erste Tag im Jahr. 02.01. der Zweite. Aber du hast Recht, nicht jeder Tag hat 4 Einträge, sodass meine Idee gar nicht klappt.

Ich benötige eigentlich nur das Datum und dazu gehörenden Hoch- oder Niedriegwasserzeiten, die ich dann zwecks LEDs zur Anzeige weiterverarbeiten will.

Ja, Datei ist am Mac geöffnet worden. 😀 Guter Punkt.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich würde da einfach erst einmal *jede* Zeile parsen, und dann ein Wörterbuch aufbauen, das Datum auf Liste mit Datenpunkten abbildet. Im nächsten Schritt kann man sich *da* dann die Datenpunkte zu einem gegebenen Datum geben lassen:

Code: Alles auswählen

#!/usr/bin/env python3
from collections import namedtuple
from datetime import datetime as DateTime
from enum import Enum
from itertools import groupby

DATA_LINE_PREFIX = "VB2#"
WEEKDAY_TO_ORDINAL = {
    day_name: number
    for number, day_name in enumerate("Mo Di Mi Do Fr Sa So".split(), 1)
}
assert len(WEEKDAY_TO_ORDINAL) == 7


class WaterLevel(Enum):
    LOW = "N"
    HIGH = "H"


DataPoint = namedtuple("DataPoint", "water_level timestamp")


def get_timestamp_part(line, start, length):
    return line[start : start + length].replace(" ", "0")


def parse_data_line(line):
    if not line.startswith(DATA_LINE_PREFIX):
        raise ValueError(f"not a data line: {line!r}")

    timestamp = DateTime.strptime(
        f"{get_timestamp_part(line, 20, 10)} {get_timestamp_part(line, 31, 5)}",
        "%d.%m.%Y %H:%M",
    )
    day_name = line[17:19]
    if timestamp.isoweekday() != WEEKDAY_TO_ORDINAL[day_name]:
        raise ValueError(f"day {day_name!r} doesn't match date {timestamp}")

    return DataPoint(WaterLevel(line[15]), timestamp)


def main():
    with open("DE__798P2023.txt", encoding="iso-8859-15") as lines:
        date_to_data_points = {
            date: list(group)
            for date, group in groupby(
                (
                    parse_data_line(line)
                    for line in lines
                    if line.startswith(DATA_LINE_PREFIX)
                ),
                lambda data_point: data_point.timestamp.date(),
            )
        }

    now = DateTime.now()
    print(now)
    for data_point in date_to_data_points[now.date()]:
        print(data_point)


if __name__ == "__main__":
    main()
Testlauf:

Code: Alles auswählen

$ ./forum4.py
2023-05-23 11:39:52.184145
DataPoint(water_level=<WaterLevel.HIGH: 'H'>, timestamp=datetime.datetime(2023, 5, 23, 0, 55))
DataPoint(water_level=<WaterLevel.LOW: 'N'>, timestamp=datetime.datetime(2023, 5, 23, 7, 9))
DataPoint(water_level=<WaterLevel.HIGH: 'H'>, timestamp=datetime.datetime(2023, 5, 23, 13, 3))
DataPoint(water_level=<WaterLevel.LOW: 'N'>, timestamp=datetime.datetime(2023, 5, 23, 19, 39))
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

So wie ich das sehe, ist das Verständnis der von nezzcarth verlinkten Dokumentation des Formats sehr hilfreich für die Aufgabe. Nach dem Einlesen der Header-Daten lässt sich für das Lesen der vorberechneten Gezeitenwerte durchaus das csv-Modul nutzen, bietet aus meiner Sicht bei dem gegebenen Format aber keinen Mehrwert. __blackjacks__ Ansatz, die einzelnen Datenzeilen in eine passende Python-Datenstruktur zu überführen, halte ich für sinnvoll, ein group_by nach "VB1" oder "VB2" jedoch nicht, denn dies sind Indikatoren ob die jeweiligen Datenzeilen mit Gezeitenhöhen versehen sind, oder lediglich Zeitberechnungen enthalten.

@raspPy: nicht jeder Tag hat vier Einträge, da sich die Gezeiten aus der Rotation der Erde in Verbindung mit der Laufbahn um die Sonne, sowie der Position des Mondes ergeben. Zwischen den Maxima und Minima liegen nicht immer genau sechs Stunden.
Zuletzt geändert von kbr am Dienstag 23. Mai 2023, 11:58, insgesamt 1-mal geändert.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kbr: Das `groupby()` ist nach dem Datum, nicht nach dem ”Zeilentyp”, weil am Ende ja die Daten für ein gegebenes Datum herausgesucht werden sollen.

Ich habe aus dem `namedtuple()` mal eine echte Klasse gemacht um da auch Properties drauf definieren zu können, und das erstellen dieser Objekte in eine eigene Funktion heraus gezogen:

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import datetime as DateTime
from enum import Enum
from itertools import groupby
from operator import attrgetter

from attr import attrib, attrs

DATA_LINE_PREFIX = "VB2#"
WEEKDAY_TO_ORDINAL = {
    day_name: number
    for number, day_name in enumerate("Mo Di Mi Do Fr Sa So".split(), 1)
}
assert len(WEEKDAY_TO_ORDINAL) == 7


class WaterLevel(Enum):
    LOW = "N"
    HIGH = "H"


def get_timestamp_part(line, start, end):
    return line[start:end].replace(" ", "0")


@attrs
class DataPoint:
    water_level = attrib()
    timestamp = attrib()

    @property
    def date(self):
        return self.timestamp.date()

    @property
    def time(self):
        return self.timestamp.time()

    @classmethod
    def parse(cls, line):
        if not line.startswith(DATA_LINE_PREFIX):
            raise ValueError(f"not a data line: {line!r}")

        timestamp = DateTime.strptime(
            (
                f"{get_timestamp_part(line, 20, 30)}"
                f" {get_timestamp_part(line, 31, 36)}"
            ),
            "%d.%m.%Y %H:%M",
        )
        day_name = line[17:19]
        if timestamp.isoweekday() != WEEKDAY_TO_ORDINAL[day_name]:
            raise ValueError(
                f"day {day_name!r} doesn't match date {timestamp}"
            )

        return cls(WaterLevel(line[15]), timestamp)


def parse_data_points(lines):
    return (
        DataPoint.parse(line)
        for line in lines
        if line.startswith(DATA_LINE_PREFIX)
    )


def main():
    with open("DE__798P2023.txt", encoding="iso-8859-15") as lines:
        date_to_data_points = {
            date: list(group)
            for date, group in groupby(
                parse_data_points(lines), attrgetter("date")
            )
        }

    now = DateTime.now()
    print(now)
    for data_point in date_to_data_points[now.date()]:
        print(data_point)


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@__blackjack__: stimmt, hatte ich falsch abgespeichert. Ein solches group_by könnte, je nach Anforderung, sinnvoll sein.
raspPy
User
Beiträge: 15
Registriert: Dienstag 15. September 2020, 13:54

Danke für den Code und die Ideen.

Code 1 gibt mir:

Code: Alles auswählen

2023-05-27 17:41:21.962956
Traceback (most recent call last):
  File "/Users/Me/Desktop/TideUhr/Code_1_Forum.py", line 64, in <module>
    main()
  File "/Users/Me/Desktop/TideUhr/Code_1_Forum.py", line 59, in main
    for data_point in date_to_data_points[now.date()]:
KeyError: datetime.date(2023, 5, 27)
und Code 2 gibt mir, obwohl ich attr vorhanden ist:

Code: Alles auswählen

Traceback (most recent call last):
  File "/Users/Me/Desktop/TideUhr/Code_2_Forum.py", line 8, in <module>
    from attr import attrib, attrs
ImportError: cannot import name 'attrib' from 'attr' (/Users/Me/Library/Application Support/mu/mu_venv-38-20230527-173644/lib/python3.8/site-packages/attr.py)
Ich verstehe weder den Grund für die erste noch die zweite Fehlermeldung :-) Wieso kommt es zu den Nachrichten?
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Also der erste Code funktioniert auch heute bei mir noch und gibt folgendes aus:

Code: Alles auswählen

2023-05-27 18:10:28.478050
DataPoint(water_level=<WaterLevel.HIGH: 'H'>, timestamp=datetime.datetime(2023, 5, 27, 3, 39))
DataPoint(water_level=<WaterLevel.LOW: 'N'>, timestamp=datetime.datetime(2023, 5, 27, 9, 33))
DataPoint(water_level=<WaterLevel.HIGH: 'H'>, timestamp=datetime.datetime(2023, 5, 27, 15, 44))
DataPoint(water_level=<WaterLevel.LOW: 'N'>, timestamp=datetime.datetime(2023, 5, 27, 22, 18))
Liegt vermutlich an der Datendatei‽

Und beim zweiten hast Du wahrscheinlich das falsche Package installiert. Das PyPI-Package heisst `attrs`, das Modul heisst aber `attr`. Es gibt auch ein PyPI-Package mit dem Namen `attr`. Das ist aber was anderes.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten