#!/usr/bin/env python import datetime import time import urllib.request from bs4 import BeautifulSoup from datetime import datetime as dt # @brief Checks the DWD pages for wind information # Format: # Provides next update tine (updateTime) of the DWD page in UTC # Provides a list of wind information (windData) # - organized as a list of tuples # - first element of tuple: GAFOR-IDs for the following wind information # - second element of tuple: list of tuples of wind data # - first element of wind data tuple: minimum altitude AMSL for this wind information # - second element of wind data tuple: wind direction # - third element of wind data tuple: wind speed (KT) class DwdCrawler(): def __init__(self): self.updateTime = None self.windData = None def parseGaforAreas(areas : str): areas = areas.replace(':', '') areas = areas.split(' ')[1] areaIds = [] # some IDs are lists for segment in areas.split(','): # check if we have range definitions or single IDs borders = segment.split('-') if 2 == len(borders): areaIds.extend(range(int(borders[0]), int(borders[1]) + 1)) else: areaIds.append(int(borders[0])) return areaIds def parseWindTableRow(row : str, table): # get the columns entries = row.split('|') # check if the line is invalid or we have the header if 2 > len(entries) or 'AMSL' in entries[0]: return table # parse the wind data windData = entries[1].strip().split(' ')[0].split('/') if 2 != len(windData): return table # extend the table altitude = entries[0].strip() if 'FL' in altitude: altitude = int(altitude.replace('FL', '')) * 100 else: altitude = int(altitude.replace('FT', '')) row = ( altitude, int(windData[0]), int(windData[1].replace('KT', '')) ) table.append(row) return table def parseNextUpdateTime(line : str): entries = line.split(' ') if 4 <= len(entries): utcIndex = 2 if 'UTC' in entries[len(entries) - 2]: utcIndex = len(entries) - 3 elif 'UTC' in entries[len(entries) - 1]: utcIndex = len(entries - 2) currentUtc = dt.utcfromtimestamp(int(time.time())) currentHour = int(currentUtc.strftime('%H')) # check if we have a day overlap if currentHour > int(entries[utcIndex].split('.')[0]): nextDay = currentUtc + datetime.timedelta(days=1) date = nextDay.strftime('%Y-%m-%d') else: date = currentUtc.strftime('%Y-%m-%d') # create the new UTC update time return dt.strptime(date + ' ' + entries[utcIndex] + '+0000', '%Y-%m-%d %H.%M%z') def parseGaforPage(self, url : str): with urllib.request.urlopen(url) as site: data = site.read().decode('utf-8') site.close() parsed = BeautifulSoup(data, features='lxml') # search the info about the GAFOR areas content = None for element in parsed.body.find_all('pre'): content = element.text # analyze the received data if None != content: windInformation = [] nextUpdate = None windTable = [] areaIds = None # find all relevant information for line in content.splitlines(): if '' == line: if 0 != len(windTable): windInformation.append(( areaIds, windTable )) areaIds = None windTable = [] elif line.startswith('GAFOR-Gebiete'): areaIds = DwdCrawler.parseGaforAreas(line) windTable = [] elif None != areaIds: windTable = DwdCrawler.parseWindTableRow(line, windTable) elif 'Aktualisierung erfolgt um ' in line: nextUpdate = DwdCrawler.parseNextUpdateTime(line) # return the collected information if 0 == len(windInformation) or None == nextUpdate: return None, None else: return nextUpdate, windInformation def receiveWindData(self): self.updateTime = None self.windData = None with urllib.request.urlopen('https://www.dwd.de/DE/fachnutzer/luftfahrt/teaser/luftsportberichte/luftsportberichte_node.html') as site: data = site.read().decode('utf-8') site.close() # find the pages of the GAFOR reports pages = [] parsed = BeautifulSoup(data, features='lxml') for link in parsed.body.find_all('a', title=True): if 'node' in link['href'] and 'Flugwetterprognose' in link['title']: # remove the jsession from the link pages.append('https://www.dwd.de/' + link['href'].split(';')[0]) # receive the wind data self.updateTime = None self.windData = [] for page in pages: next, wind = self.parseGaforPage(page) if None != next: if None == self.updateTime or self.updateTime > next: self.updateTime = next self.windData.extend(wind) # indicate that new wind data is available if None != self.updateTime: return True else: return False