Commit 832a665b authored by Marco Schmiedel's avatar Marco Schmiedel

Initial commit

parents
.ipynb_checkpoints
__pycache__
workbench/*.bak
cache/*.html
cache/*.csv
\ No newline at end of file
{
"paths": [
"./work/"
],
"exclude": [
"Workbench.mwb.bak",
"__pycache__",
".ipynb_checkpoints",
"./work/cache",
".DS_Store",
"node_modules",
"package-lock.json",
"package.json",
".gitignore",
".sidekick",
".vscode",
".git"
]
}
{
"fileId": "22983490-9c01-4bd1-8649-dfe87c659225",
"originalPath": "work/config/MauiConfig.py",
"currentPath": "work/config/MauiConfig.py",
"hash": "7749ff881e9fac6aaeffe5274aa9566b2b5692a1ec4e7f47e20b79671a0788a3",
"docContent": "<p>In this configuration, you’ll find the credentials required to log in to Freenet-Maui.</p>",
"checkedStatus": "done",
"comments": [
{
"commentId": "3bc16f5e-4032-44a8-9012-4b632849ba50",
"text": "This data is currently stored statically and should be dynamically linked to the Docker container at the appropriate time.",
"timestamp": 1744614418809
}
],
"lastCheckedTimestamp": 1744614348029,
"lastFileModificationTimestamp": 1744614095522.7373
}
{
"fileId": "36e791b4-e235-42f6-ac61-8560f1762892",
"originalPath": "work/workbench/Workbench.mwb",
"currentPath": "work/workbench/Workbench.mwb",
"hash": "050435dbe63c623b2d9a4201250dad0b5b1c43601ece3e390392e4541b182c9b",
"docContent": "<p><br></p>",
"checkedStatus": "done",
"comments": [],
"lastCheckedTimestamp": 1744619680767,
"lastFileModificationTimestamp": 1744619453253.4011
}
{
"fileId": "5ec4e9ba-309d-438b-8e17-ce5802f3deb2",
"originalPath": "work/workbench/Documentation.md",
"currentPath": "work/workbench/Documentation.md",
"hash": "7f2058d974b71f0a0da97fc41b918c41c33a4905c15cd54d189ec8d1311fa972",
"docContent": "<p><br></p>",
"checkedStatus": "done",
"comments": [],
"lastCheckedTimestamp": 1744619683307,
"lastFileModificationTimestamp": 1744619461883.1462
}
{
"fileId": "d470fd53-8f95-47d0-a63b-5851586c0eda",
"originalPath": "work/manager/SeleniumManager.py",
"currentPath": "work/manager/SeleniumManager.py",
"hash": "be7dde51af30a8ea1f80ea3090f39d2e9a84168cf7d3e15086c16bdbbfde2c26",
"docContent": "<p><br></p>",
"checkedStatus": "done",
"comments": [],
"lastCheckedTimestamp": 1744620379152,
"lastFileModificationTimestamp": 1744547804595.8647
}
{
"files.exclude": {
"**/__pycache__": true,
"**/.ipynb_checkpoints" : true,
"**/*.mwb.bak" : true,
"cache/*.html" : true,
"cache/.gitkeep" : true,
".gitignore" : false
}
}
# This variable stores the username for accessing Maui.
MAUI_USERNAME = "28009594-198"
# This variable stores the password for accessing Maui.
MAUI_PASSWORD = "8v#5YeeQyh"
# This variable stores the authentication code (2FA code) for accessing Maui.
MAUI_AUTHCODE = "2D3JJNG3WWGSWRDI5KRW2MZKL3NJEZXJ"
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium import webdriver
import time
import random
import os
import uuid
import glob
# Diese Klasse verwaltet eine Selenium-Instanz und stellt Methoden zum Abrufen von Webseiten bereit.
class SeleniumManager:
# Diese Methode ist der Konstruktor und initialisiert den Firefox-Browser mit den gewünschten Optionen.
def __init__(
self,
window_size: str = "1920,1080",
headless: bool = True,
disable_gpu: bool = True,
no_sandbox: bool = True,
disable_dev_shm_usage: bool = True,
geckodriver_path: str = '/usr/bin/geckodriver'
):
# Diese Variable speichert die Firefox-Optionen, die für den Browser gelten.
firefox_options = Options()
# Diese Anweisung setzt die Fenstergröße nach den Vorgaben.
firefox_options.add_argument(f"--window-size={window_size}")
# Diese Abfrage fügt die Headless-Option nur hinzu, wenn headless True ist.
if headless:
# Diese Anweisung macht den Browser unsichtbar.
firefox_options.add_argument("--headless")
# Diese Anweisung deaktiviert die GPU, wenn disable_gpu True ist, ansonsten wird nichts hinzugefügt.
firefox_options.add_argument("--disable-gpu" if disable_gpu else "")
# Diese Anweisung deaktiviert die Sandbox, wenn no_sandbox True ist, ansonsten wird nichts hinzugefügt.
firefox_options.add_argument("--no-sandbox" if no_sandbox else "")
# Diese Anweisung deaktiviert dev-shm, wenn disable_dev_shm_usage True ist, ansonsten wird nichts hinzugefügt.
firefox_options.add_argument("--disable-dev-shm-usage" if disable_dev_shm_usage else "")
# Diese Anweisung setzt den User-Agent auf einen zufällig generierten Wert.
firefox_options.set_preference("general.useragent.override", self.getRandomUserAgent())
# Diese Variable erstellt den Dienst für den Firefox-Treiber unter Verwendung des angegebenen Pfads.
service = Service(geckodriver_path)
# Diese Variable initialisiert den Firefox-WebDriver mit dem Dienst und den Optionen.
self.driver = webdriver.Firefox(service=service, options=firefox_options)
# Diese Methode führt eine einfache Anfrage an eine bestimmte URL durch und speichert den HTML-Inhalt im Cache.
def simpleRequest(self, url):
# Diese Anweisung öffnet die angegebene URL mit dem Selenium-Treiber.
self.driver.get(url)
# Diese Variable speichert den HTML-Quellcode der aktuellen Seite.
html = self.driver.page_source
# Diese Variable legt den Pfad für den Cache-Ordner fest, relativ zum Skript.
cache_dir = os.path.join(os.path.dirname(__file__), "..", "cache")
# Diese Abfrage prüft, ob der Cache-Ordner existiert.
if not os.path.exists(cache_dir):
# Diese Anweisung erstellt den Cache-Ordner, falls er nicht vorhanden ist.
os.makedirs(cache_dir)
# Diese Variable generiert einen eindeutigen Dateinamen anhand einer UUID.
unique_filename = f"{uuid.uuid4().hex}.html"
# Diese Variable kombiniert den Cache-Pfad mit dem eindeutigen Dateinamen.
file_path = os.path.join(cache_dir, unique_filename)
# Diese Anweisung öffnet die Datei und schreibt den HTML-Inhalt hinein.
with open(file_path, "w", encoding="utf-8") as f:
f.write(html)
# Diese Variable sammelt alle HTML-Dateien im Cache-Ordner.
files = glob.glob(os.path.join(cache_dir, "*.html"))
# Diese Anweisung sortiert die Dateien nach Änderungsdatum, wobei die neueste zuerst steht.
files.sort(key=os.path.getmtime, reverse=True)
# Diese Schleife löscht alle Dateien außer den 10 neuesten.
for old_file in files[10:]:
# Diese Fehlerbehandlung versucht die Datei zu entfernen und gibt einen Fehler aus, wenn es fehlschlägt.
try:
os.remove(old_file)
except Exception as e:
print(f"Fehler beim Löschen der Datei {old_file}: {e}")
# Diese Anweisung gibt den Selenium-Treiber zurück.
return self.driver
# Diese Methode scrollt die Seite schrittweise, um möglichst alle dynamischen Inhalte zu laden.
def performScroll(self, scroll_step=600, scroll_pause=0.01, max_tries=10):
# Diese Variable speichert die anfängliche Höhe des Dokuments.
last_height = self.driver.execute_script("return document.body.scrollHeight")
# Diese Variable zählt, wie oft sich die Höhe nacheinander nicht geändert hat.
stop_counter = 0
# Diese Schleife wird durchlaufen, um mehrmals zu scrollen und somit alle Inhalte zu erfassen.
for _ in range(max_tries):
# Diese Schleife scrollt schrittweise durch die Seite.
for pos in range(0, last_height, scroll_step):
# Diese Anweisung scrollt zum jeweiligen Abschnitt.
self.driver.execute_script(f"window.scrollTo(0, {pos});")
time.sleep(scroll_pause)
# Diese Anweisung scrollt an das Ende des Dokuments.
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(scroll_pause)
# Diese Variable liest die neue Höhe nach dem Scrollen.
new_height = self.driver.execute_script("return document.body.scrollHeight")
# Diese Abfrage prüft, ob sich die Höhe im Vergleich zum letzten Durchlauf nicht geändert hat.
if new_height == last_height:
# Diese Anweisung erhöht den Zähler, wenn keine Änderung festgestellt wird.
stop_counter += 1
# Diese Abfrage bricht die Schleife ab, wenn keine Änderung zweimal in Folge festgestellt wurde.
if stop_counter >= 2:
break
else:
# Diese Anweisung setzt den Zähler zurück, wenn eine Veränderung festgestellt wird.
stop_counter = 0
# Diese Anweisung aktualisiert last_height mit der neuen Höhe.
last_height = new_height
# Diese Anweisung gibt den Selenium-Treiber zurück.
return self.driver
# Diese Methode schließt die Selenium-Instanz und beendet den Browser.
def closeDriver(self):
self.driver.quit()
# Diese Methode generiert einen zufälligen User-Agent-String, um die Browsererkennung zu erschweren.
def getRandomUserAgent(self):
# Diese Variable enthält typische Betriebssystem-Angaben.
operating_systems = [
"Windows NT 10.0; Win64; x64",
"Windows NT 6.1; Win64; x64",
"Macintosh; Intel Mac OS X 10_15_7",
"Macintosh; Intel Mac OS X 13_3_1",
"X11; Linux x86_64",
"iPhone; CPU iPhone OS 16_0 like Mac OS X",
"Android 11; Mobile; rv:68.0"
]
# Diese Variable listet verschiedene Browsertypen auf.
browsers = ["chrome", "firefox", "safari", "edge", "opera"]
# Diese Anweisung wählt ein zufälliges Betriebssystem aus.
os_ = random.choice(operating_systems)
# Diese Anweisung wählt einen zufälligen Browser aus.
browser = random.choice(browsers)
# Diese Variablen generieren zufällige Versionsnummern.
major_version = random.randint(60, 120)
minor_version = random.randint(0, 4000)
build_version = random.randint(0, 999)
patch_version = random.randint(0, 99)
# Diese Abfrage erstellt den User-Agent für Chrome.
if browser == "chrome":
ua = (
f"Mozilla/5.0 ({os_}) AppleWebKit/537.36 "
f"(KHTML, like Gecko) Chrome/{major_version}.{build_version}.{minor_version}.{patch_version} "
f"Safari/537.36"
)
# Diese Abfrage erstellt den User-Agent für Firefox.
elif browser == "firefox":
rv_version = f"{major_version}.0"
ua = (
f"Mozilla/5.0 ({os_}; rv:{rv_version}) "
f"Gecko/20100101 Firefox/{rv_version}"
)
# Diese Abfrage erstellt den User-Agent für Safari.
elif browser == "safari":
safari_version = f"{major_version}.{random.randint(0, 9)}"
webkit_version = f"605.1.{random.randint(0, 99)}"
ua = (
f"Mozilla/5.0 ({os_}) AppleWebKit/{webkit_version} "
f"(KHTML, like Gecko) Version/{safari_version} Safari/{webkit_version}"
)
# Diese Abfrage erstellt den User-Agent für Edge.
elif browser == "edge":
blink_version = f"{major_version}.{build_version}.{minor_version}.{patch_version}"
ua = (
f"Mozilla/5.0 ({os_}) AppleWebKit/537.36 "
f"(KHTML, like Gecko) Chrome/{blink_version} Safari/537.36 Edg/{blink_version}"
)
# Diese Abfrage erstellt den User-Agent für Opera.
else:
blink_version = f"{major_version}.{build_version}.{minor_version}.{patch_version}"
ua = (
f"Mozilla/5.0 ({os_}) AppleWebKit/537.36 "
f"(KHTML, like Gecko) Chrome/{blink_version} Safari/537.36 OPR/{blink_version}"
)
# Diese Anweisung gibt den generierten User-Agent zurück.
return ua
{
"cells": [
{
"cell_type": "code",
"execution_count": 5,
"id": "193fb1fb-6ad1-447e-b641-73857be9d9dd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3877325 - D1 - Allnet Flat 20 GB Telekom (Okt 2024)\n",
"3877349 - D1 - Allnet Flat 20 GB Telekom (Okt 2024) mit Smartphone 10\n",
"3877337 - D1 - Allnet Flat 20 GB Telekom (Okt 2024) mit Smartphone 5\n",
"3878213 - D1 - Allnet Flat 25 GB Telekom (Okt 2024)\n",
"3878237 - D1 - Allnet Flat 25 GB Telekom (Okt 2024) mit Smartphone 10\n",
"3878225 - D1 - Allnet Flat 25 GB Telekom (Okt 2024) mit Smartphone 5\n",
"3878165 - D1 - Allnet Flat 35 GB Telekom (Okt 2024)\n",
"3878189 - D1 - Allnet Flat 35 GB Telekom (Okt 2024) mit Smartphone 10\n",
"3878177 - D1 - Allnet Flat 35 GB Telekom (Okt 2024) mit Smartphone 5\n",
"3905336 - D1 - Allnet Flat 60 GB Telekom (Okt 2024)\n",
"3905360 - D1 - Allnet Flat 60 GB Telekom (Okt 2024) mit Smartphone 10\n",
"3905348 - D1 - Allnet Flat 60 GB Telekom (Okt 2024) mit Smartphone 5\n",
"3877289 - D1 - Allnet Flat 8 GB Telekom (Okt 2024)\n",
"3877313 - D1 - Allnet Flat 8 GB Telekom (Okt 2024) mit Smartphone 10\n",
"3877301 - D1 - Allnet Flat 8 GB Telekom (Okt 2024) mit Smartphone 5\n",
"3878261 - D1 - Allnet Flat 80 GB Telekom (Okt 2024)\n",
"3878285 - D1 - Allnet Flat 80 GB Telekom (Okt 2024) mit Smartphone 10\n",
"3878273 - D1 - Allnet Flat 80 GB Telekom (Okt 2024) mit Smartphone 5\n",
"3398811 - D1 - green Data L (2021)\n",
"3435075 - D1 - green Data L (2021) mit Hardware 10\n",
"3398807 - D1 - green Data M (2021)\n",
"3435071 - D1 - green Data M (2021) mit Hardware 10\n",
"3398803 - D1 - green Data S (2021)\n",
"3435067 - D1 - green Data S (2021) mit Hardware 10\n",
"3398815 - D1 - green Data XL (2021)\n",
"3435079 - D1 - green Data XL (2021) mit Hardware 10\n",
"3649995 - D1 - green LTE 15 GB Telekom (Jul 2023) mit Smartphone 20\n",
"3596218 - D1 - green LTE 25 GB Telekom (Jul 2023) mit Smartphone 10\n",
"3650007 - D1 - green LTE 25 GB Telekom (Jul 2023) mit Smartphone 20\n",
"3650019 - D1 - green LTE 40 GB Telekom (Jul 2023) mit Smartphone 20\n",
"3650031 - D1 - green LTE 60 GB Telekom (Jul 2023) mit Smartphone 20\n",
"3461557 - D2 - green Data L (SEP 2022)\n",
"3461561 - D2 - green Data L mit Smartphone 10 (SEP 2022)\n",
"3446028 - D2 - green Data S (Jul 2022)\n",
"3461565 - D2 - green Data XL (SEP 2022)\n",
"3461569 - D2 - green Data XL mit Smartphone 10 (SEP 2022)\n",
"3765006 - D2 - green LTE 20 GB VF (April 2024) mit Smartphone 5\n",
"3782151 - O2 - Allnet Flat 10 GB (Jun 2024)\n",
"3833962 - O2 - Allnet Flat 10+7 GB (Aug 2024)\n",
"3833968 - O2 - Allnet Flat 10+7 GB (Aug 2024) mit Smartphone 10\n",
"3833965 - O2 - Allnet Flat 10+7 GB (Aug 2024) mit Smartphone 5\n",
"3975848 - O2 - Allnet Flat 100 GB (Nov 2024)\n",
"3975869 - O2 - Allnet Flat 100 GB (Nov 2024) 1M\n",
"3833971 - O2 - Allnet Flat 15+10 GB (Aug 2024)\n",
"3833977 - O2 - Allnet Flat 15+10 GB (Aug 2024) mit Smartphone 10\n",
"3833974 - O2 - Allnet Flat 15+10 GB (Aug 2024) mit Smartphone 5\n",
"3975851 - O2 - Allnet Flat 150 GB (Nov 2024)\n",
"3975872 - O2 - Allnet Flat 150 GB (Nov 2024) 1M\n",
"3975854 - O2 - Allnet Flat 200 GB (Nov 2024)\n",
"3975875 - O2 - Allnet Flat 200 GB (Nov 2024) 1M\n",
"3833983 - O2 - Allnet Flat 25+10 GB (Aug 2024)\n",
"3833989 - O2 - Allnet Flat 25+10 GB (Aug 2024) mit Smartphone 10\n",
"3833986 - O2 - Allnet Flat 25+10 GB (Aug 2024) mit Smartphone 5\n",
"3975860 - O2 - Allnet Flat 300 GB (Nov 2024)\n",
"3975881 - O2 - Allnet Flat 300 GB (Nov 2024) 1M\n",
"3833995 - O2 - Allnet Flat 35+15 GB (Aug 2024)\n",
"3834001 - O2 - Allnet Flat 35+15 GB (Aug 2024) mit Smartphone 10\n",
"3833998 - O2 - Allnet Flat 35+15 GB (Aug 2024) mit Smartphone 5\n",
"3833953 - O2 - Allnet Flat 5+2 GB (Aug 2024)\n",
"3833959 - O2 - Allnet Flat 5+2 GB (Aug 2024) mit Smartphone 10\n",
"3833956 - O2 - Allnet Flat 5+2 GB (Aug 2024) mit Smartphone 5\n",
"3975845 - O2 - Allnet Flat 70 GB (Nov 2024)\n",
"3975866 - O2 - Allnet Flat 70 GB (Nov 2024) 1M\n",
"3974111 - O2 - Datentarif 400 (Dez 2024)\n",
"3833776 - O2 - family & friends 25+10 GB (Aug 2024)\n",
"3833773 - O2 - family & friends 5+2 GB (Aug 2024)\n",
"3476568 - D1 - KM Allnet Flat 10 GB (D1) 24M (Nov 2022)\n",
"3697996 - D1 - KM Allnet Flat 12 GB (D1) 24M (Nov 2023)\n",
"3877175 - D1 - KM Allnet Flat 15 GB (D1) 24M (Okt 2024)\n",
"3711205 - D1 - KM Allnet Flat 22 GB LTE50 (D1) 24M (Nov 2023)\n",
"3877193 - D1 - KM Allnet Flat 25 GB LTE50 (D1) 24M (Okt 2024)\n",
"3711328 - D1 - KM Allnet Flat 32 GB LTE50 (D1) 24M (Nov 2023)\n",
"3877211 - D1 - KM Allnet Flat 40 GB LTE50 (D1) 1M (Okt 2024)\n",
"3877208 - D1 - KM Allnet Flat 40 GB LTE50 (D1) 24M (Okt 2024)\n",
"3388975 - D1 - KM Minuten Tarif 24M 1GB/100Min/100SMS (D1)\n",
"3389002 - D1 - KM Minuten Tarif 24M 2GB/100Min/100SMS (D1)\n",
"3380690 - D2 - KM Allnet Flat 10GB/LTE50/Voice-&SMS-Flat (VF) 24M\n",
"3743289 - D2 - KM Allnet Flat 17GB/LTE50/Voice-&SMS-Flat (VF) 24M\n",
"3743301 - D2 - KM Allnet Flat 27GB/LTE50/Voice-&SMS-Flat (VF) 24M (Jan 2024)\n",
"3415206 - D2 - KM Allnet Flat 30GB/LTE50/Voice-&SMS-Flat (VF) 24M\n",
"3473445 - D2 - KM Allnet Flat 40GB/LTE50/Voice-&SMS-Flat (VF) 24M\n",
"3385059 - D2 - KM Daten Flat 10GB/LTE50 (VF) 24M\n",
"3385056 - D2 - KM Daten Flat 5GB/LTE50 (VF) 24M\n",
"3861200 - D2 - KM Family & Friends 20 GB 24M\n",
"3861206 - D2 - KM Family & Friends 5 GB 24M\n",
"3387917 - D2 - KM Minuten Tarif 2GB/LTE21/100Min/100SMS (VF) 24M\n",
"3243715 - D2 - Minuten Tarif / Smartphone Flat 24M 1000MB/100Min (VF)\n",
"3833026 - F1 - Family & Friends 25+10 GB (Aug 2024) 24M\n",
"3833020 - F1 - Family & Friends 5+2 GB (Aug 2024) 24M\n",
"3832003 - D1 - Magenta Mobil Basic mit Smartphone 10 (Aug 2024)\n",
"3832111 - D1 - Magenta Mobil L mit Smartphone 10 (Aug 2024)\n",
"3907595 - D1 - Magenta Mobil L Young 5G mit Smartphone 10 (Okt 2024)\n",
"3696895 - D1 - Magenta Mobil L Young 5G mit Smartphone 20 (Mai 2022)\n",
"3832075 - D1 - Magenta Mobil M mit Smartphone 10 (Aug 2024)\n",
"3907559 - D1 - Magenta Mobil M Young 5G mit Smartphone 10 (Okt 2024)\n",
"3696883 - D1 - Magenta Mobil M Young 5G mit Smartphone 20 (Mai 2022)\n",
"3832039 - D1 - Magenta Mobil S mit Smartphone 10 (Aug 2024)\n",
"3907523 - D1 - Magenta Mobil S Young 5G mit Smartphone 10 (Okt 2024)\n",
"3696871 - D1 - Magenta Mobil S Young 5G mit Smartphone 20 (Mai 2022)\n",
"3332926 - D1 - Magenta Mobil Speedbox 100 GB\n",
"3832147 - D1 - Magenta Mobil XL mit Smartphone 10 (Aug 2024)\n",
"3907631 - D1 - Magenta Mobil XL Young 5G mit Smartphone 10 (Okt 2024)\n",
"3696907 - D1 - Magenta Mobil XL Young 5G mit Smartphone 20 (Mai 2022)\n",
"3448928 - D1 - Smart Connect M (Juli 2022)\n",
"3337095 - D1 - Smart Connect S (2019)\n",
"3430251 - D1 - Smart Connect S 12 Monate (Mrz 2022)\n",
"3337091 - D1 - Smart Connect S mit Smartphone 5 (2019)\n",
"3936372 - D2 - Vodafone GigaMobil L mit Smartphone 10 (Nov24)\n",
"3936336 - D2 - Vodafone GigaMobil M mit Smartphone 10 (Nov24)\n",
"3936300 - D2 - Vodafone GigaMobil S mit Smartphone 10 (Nov24)\n",
"3936408 - D2 - Vodafone GigaMobil XL mit Smartphone 10 (Nov24)\n",
"3936264 - D2 - Vodafone GigaMobil XS mit Smartphone 10 (Nov24)\n",
"3750758 - D2 - Vodafone GigaMobil Young L mit Smartphone 10\n",
"3750710 - D2 - Vodafone GigaMobil Young M mit Smartphone 10\n",
"3750083 - D2 - Vodafone GigaMobil Young S mit Smartphone 10\n",
"3750806 - D2 - Vodafone GigaMobil Young XL mit Smartphone 10\n",
"3973286 - O2 - o2 Mobile L (Nov 2024)\n",
"3973292 - O2 - o2 Mobile L (Nov 2024) mit Smartphone 10\n",
"3973289 - O2 - o2 Mobile L (Nov 2024) mit Smartphone 5\n",
"3973277 - O2 - o2 Mobile M PROMO (Nov 2024)\n",
"3973283 - O2 - o2 Mobile M PROMO (Nov 2024) mit Smartphone 10\n",
"3973280 - O2 - o2 Mobile M PROMO (Nov 2024) mit Smartphone 5\n",
"3973259 - O2 - o2 Mobile S (Nov 2024)\n",
"3973265 - O2 - o2 Mobile S (Nov 2024) mit Smartphone 10\n",
"3973262 - O2 - o2 Mobile S (Nov 2024) mit Smartphone 5\n",
"3973250 - O2 - o2 Mobile Starter Flex (Nov 2024)\n",
"3973256 - O2 - o2 Mobile Starter Flex (Nov 2024) mit Smartphone 10\n",
"3973253 - O2 - o2 Mobile Starter Flex (Nov 2024) mit Smartphone 5\n",
"3782346 - O2 - o2 Mobile Unlimited Max (Jun 2024)\n",
"3782349 - O2 - o2 Mobile Unlimited Max (Jun 2024) 1M\n",
"3935061 - O2 - o2 Mobile Unlimited on Demand (Okt 2024)\n",
"3782322 - O2 - o2 Mobile Unlimited Smart (Jun 2024)\n",
"3782325 - O2 - o2 Mobile Unlimited Smart (Jun 2024) 1M\n",
"3782331 - O2 - o2 Mobile Unlimited Smart (Jun 2024) mit Smartphone 10\n",
"3782328 - O2 - o2 Mobile Unlimited Smart (Jun 2024) mit Smartphone 5\n",
"3973295 - O2 - o2 Mobile XL (Nov 2024)\n",
"3973301 - O2 - o2 Mobile XL (Nov 2024) mit Smartphone 10\n",
"3973298 - O2 - o2 Mobile XL (Nov 2024) mit Smartphone 5\n"
]
}
],
"source": [
"import sys; sys.path.append(\"..\")\n",
"\n",
"# Diese Import-Anweisung importiert die Klasse SeleniumManager, um Selenium-Aktionen zu verwalten.\n",
"from manager.SeleniumManager import SeleniumManager\n",
"\n",
"# Diese Import-Anweisung importiert die Zugangsdaten aus der entsprechenden Konfigurationsdatei.\n",
"from config.MauiConfig import MAUI_USERNAME, MAUI_PASSWORD, MAUI_AUTHCODE\n",
"\n",
"# Diese Import-Anweisung importiert die Klasse By, um Webelemente nach bestimmten Merkmalen zu suchen.\n",
"from selenium.webdriver.common.by import By\n",
"\n",
"# Diese Import-Anweisung importiert die Funktion ActionChains, um komplexe Aktionen wie Mausbewegungen auszuführen.\n",
"from selenium.webdriver.common.action_chains import ActionChains\n",
"\n",
"# Diese Import-Anweisung importiert WebDriverWait und Select, um explizite Wartezeiten und Auswahl in Dropdowns zu ermöglichen.\n",
"from selenium.webdriver.support.ui import WebDriverWait, Select\n",
"\n",
"# Diese Import-Anweisung importiert die Bedingungsklassen aus expected_conditions, um bestimmte Zustände im Browser abzuwarten.\n",
"from selenium.webdriver.support import expected_conditions as EC\n",
"\n",
"# Diese Import-Anweisung importiert die Klasse BeautifulSoup, um HTML-Inhalte zu parsen und Elemente auszulesen.\n",
"from bs4 import BeautifulSoup\n",
"\n",
"# Diese Import-Anweisung importiert das time-Modul, um Wartezeiten über time.sleep zu realisieren.\n",
"import time\n",
"\n",
"# Diese Import-Anweisung importiert die Bibliothek pyotp, um Einmalpasswörter für die Zwei-Faktor-Authentifizierung zu generieren.\n",
"import pyotp\n",
"\n",
"# Diese Import-Anweisung importiert das csv-Modul, um CSV-Dateien zu lesen und zu schreiben.\n",
"import csv\n",
"\n",
"# Diese Import-Anweisung importiert das re-Modul, um reguläre Ausdrücke zu verwenden.\n",
"import re\n",
"\n",
"# Diese Variable speichert die IDs der bereits geschriebenen Kategorien.\n",
"unique_categories = set()\n",
"\n",
"# Diese Funktion führt den Login-Vorgang durch, indem sie Nutzername, Passwort und den 2FA-Code einfügt.\n",
"def login(manager, username, password, rawtoken):\n",
"\n",
" # Diese Variablenzuweisung speichert das WebDriver-Objekt, das durch simpleRequest zurückgegeben wird.\n",
" driver = manager.simpleRequest(\"https://maui.md.de\")\n",
"\n",
" # Diese Variablenzuweisung erstellt ein WebDriverWait-Objekt, um explizite Wartezeiten zu ermöglichen.\n",
" wait = WebDriverWait(driver, 10)\n",
"\n",
" # Diese Variablenzuweisung wartet auf das Vorhandensein des Eingabefelds für den Nutzernamen.\n",
" username_field = wait.until(EC.presence_of_element_located((By.ID, \"mat-input-0\")))\n",
"\n",
" # Diese Variablenzuweisung wartet auf das Vorhandensein des Eingabefelds für das Passwort.\n",
" password_field = wait.until(EC.presence_of_element_located((By.ID, \"mat-input-1\")))\n",
"\n",
" # Diese Anweisung füllt den Nutzernamen in das entsprechende Eingabefeld ein.\n",
" username_field.send_keys(username)\n",
"\n",
" # Diese Anweisung füllt das Passwort in das entsprechende Eingabefeld ein.\n",
" password_field.send_keys(password)\n",
"\n",
" # Diese Anweisung pausiert den Ablauf für eine Sekunde.\n",
" time.sleep(1)\n",
"\n",
" # Diese Variablenzuweisung wartet darauf, dass der \"Anmelden\"-Button klickbar ist.\n",
" login_button = wait.until(EC.element_to_be_clickable((By.XPATH, \"//button[.//span[contains(text(),'Anmelden')]]\")))\n",
"\n",
" # Diese Anweisung löst den Klick auf den \"Anmelden\"-Button aus.\n",
" login_button.click()\n",
"\n",
" # Diese Variablenzuweisung erstellt ein TOTP-Objekt mithilfe des bereitgestellten Tokens.\n",
" totp = pyotp.TOTP(rawtoken)\n",
"\n",
" # Diese Variablenzuweisung generiert den aktuellen Einmalcode aus dem TOTP-Objekt.\n",
" auth_code = totp.now()\n",
"\n",
" # Diese Variablenzuweisung wartet auf das Vorhandensein des 2FA-Codefelds.\n",
" code_field = wait.until(EC.presence_of_element_located((By.ID, \"mat-input-2\")))\n",
"\n",
" # Diese Anweisung füllt den generierten 2FA-Code in das entsprechende Eingabefeld ein.\n",
" code_field.send_keys(auth_code)\n",
"\n",
" # Diese Variablenzuweisung wartet darauf, dass der neue \"Anmelden\"-Button im erscheinenden Modal klickbar ist.\n",
" modal_login_button = wait.until(EC.element_to_be_clickable(\n",
" (By.XPATH, \"//mat-dialog-actions//button[span[contains(text(),'Anmelden')]]\")\n",
" ))\n",
"\n",
" # Diese Anweisung führt den Klick auf den \"Anmelden\"-Button im Modal über JavaScript aus.\n",
" driver.execute_script(\"arguments[0].click();\", modal_login_button)\n",
"\n",
"# Diese Funktion öffnet die Seite zum Laufzeitvertrag, indem sie auf den entsprechenden Link klickt.\n",
"def openLaufzeitvertrag(manager):\n",
"\n",
" # Diese Variablenzuweisung erstellt ein WebDriverWait-Objekt mit 10 Sekunden Timeout.\n",
" wait = WebDriverWait(manager.driver, 10)\n",
"\n",
" # Diese Variablenzuweisung wartet, bis das Element mit dem Text \"Laufzeitvertrag\" im DOM vorhanden ist.\n",
" laufzeit_element = wait.until(EC.presence_of_element_located(\n",
" (By.XPATH, \"//a[contains(text(),'Laufzeitvertrag')]\")\n",
" ))\n",
"\n",
" # Diese Variablenzuweisung liest das href-Attribut des gefundenen Elements aus.\n",
" url = laufzeit_element.get_attribute(\"href\")\n",
"\n",
" # Diese Anweisung lädt die Laufzeitvertrag-Seite, indem sie die URL aufruft.\n",
" manager.driver.get(url)\n",
"\n",
"# Diese Funktion wartet, bis das Overlay (wait.html) nicht mehr sichtbar ist und das Dropdown 'tarif_id' angezeigt wird.\n",
"def wait_for_dropdown_ready(driver, wait):\n",
"\n",
" # Diese Anweisung wartet, bis das iframe mit src='wait.html' unsichtbar ist.\n",
" wait.until(\n",
" EC.invisibility_of_element_located(\n",
" (By.XPATH, \"//iframe[contains(@src, 'wait.html')]\")\n",
" )\n",
" )\n",
"\n",
" # Diese Anweisung wartet, bis das Element mit dem Namen 'tarif_id' im DOM vorhanden ist.\n",
" wait.until(\n",
" EC.presence_of_element_located((By.NAME, \"tarif_id\"))\n",
" )\n",
"\n",
"# Diese Funktion parst den Tarifpreis, falls vorhanden, und gibt den Nettopreis zurück.\n",
"def parse_plan_price(driver):\n",
"\n",
" # Diese Variablenzuweisung setzt einen Standardpreis auf 0.0.\n",
" price_net = 0.0\n",
"\n",
" # Diese Versuchsstruktur sucht das Element für den Tarifpreis anhand der ID.\n",
" try:\n",
"\n",
" # Diese Variablenzuweisung findet das Element, das den Tarifpreis anzeigt.\n",
" price_element = driver.find_element(By.ID, \"preis_anzeige_tarif\")\n",
"\n",
" # Diese Variablenzuweisung liest den sichtbaren Text aus dem gefundenen Element.\n",
" price_text = price_element.text\n",
"\n",
" # Diese Variablenzuweisung durchsucht den Text nach einem Muster, das den Betrag in EUR findet.\n",
" match = re.search(r'([\\d\\.,]+)\\s*EUR', price_text)\n",
"\n",
" # Diese Bedingung prüft, ob ein Wert im Text gefunden wurde.\n",
" if match:\n",
"\n",
" # Diese Variablenzuweisung wandelt das gefundene Match in eine float-Zahl um.\n",
" raw_str = match.group(1).replace(\",\", \".\")\n",
" gross_price = float(raw_str)\n",
"\n",
" # Diese Variablenzuweisung dividiert den Bruttopreis durch 1.19 und rundet auf 5 Nachkommastellen.\n",
" price_net = round(gross_price / 1.19, 5)\n",
" except:\n",
" # Diese Ausnahme wird ausgelöst, wenn das Element oder der Text nicht gefunden wurde.\n",
" pass\n",
"\n",
" # Diese Anweisung gibt den Nettopreis zurück.\n",
" return price_net\n",
"\n",
"# Diese Funktion parst alle verfügbaren Aktionen aus dem Select-Feld und gibt sie als Liste von Tupeln zurück.\n",
"def parse_campaigns(driver):\n",
"\n",
" # Diese Variablenzuweisung erstellt eine leere Ergebnisliste für Aktionen.\n",
" campaigns_list = []\n",
"\n",
" # Diese Versuchsstruktur sucht das Dropdown für Aktionen anhand seines Namens.\n",
" try:\n",
"\n",
" # Diese Variablenzuweisung sucht das Aktionen-Select-Element.\n",
" campaign_select = driver.find_element(By.NAME, \"am_aktion_select\")\n",
"\n",
" # Diese Variablenzuweisung findet alle Option-Elemente innerhalb des Selects.\n",
" campaign_options = campaign_select.find_elements(By.TAG_NAME, \"option\")\n",
"\n",
" # Diese Schleife durchläuft jede Option, um sie zu analysieren.\n",
" for copt in campaign_options:\n",
"\n",
" # Diese Variablenzuweisung extrahiert den Value, der die ID der Aktion enthält.\n",
" val = copt.get_attribute(\"value\")\n",
"\n",
" # Diese Variablenzuweisung extrahiert den sichtbaren Text der Option.\n",
" txt = copt.text.strip()\n",
"\n",
" # Diese Bedingung überspringt ungültige oder Platzhalter-Optionen.\n",
" if not val or val in [\" |\", \"-1|\", \"|\", \"-1|\"]:\n",
" continue\n",
"\n",
" # Diese Variablenzuweisung teilt den Value an der Pipe, um die ID zu erhalten.\n",
" parts_val = val.split(\"|\")\n",
" campaign_id = parts_val[0].strip()\n",
"\n",
" # Diese Bedingung verarbeitet nur sinnvolle Einträge.\n",
" if not campaign_id:\n",
" continue\n",
"\n",
" # Diese Variablenzuweisung versucht, den Aktionsnamen aus dem sichtbaren Text zu extrahieren.\n",
" if \"-\" in txt:\n",
" splitted = txt.split(\"-\", 1)\n",
" campaign_name = splitted[1].strip()\n",
" else:\n",
" campaign_name = txt\n",
"\n",
" # Diese Anweisung fügt ein Tupel (aktions-id, aktionsname) der Ergebnisliste hinzu.\n",
" campaigns_list.append((campaign_id, campaign_name))\n",
"\n",
" except:\n",
" # Diese Ausnahme wird ausgelöst, wenn das Select-Element nicht gefunden wurde.\n",
" pass\n",
"\n",
" # Diese Anweisung gibt die Liste mit (aktions-id, aktionsname) zurück.\n",
" return campaigns_list\n",
"\n",
"# Diese Funktion durchläuft alle verfügbaren Tarifwelten und Netze, wählt jeden Tarif aus,\n",
"# parst den Preis und die Kampagnen, schreibt in die CSVs und ruft scrapeOption auf.\n",
"def scrapeData(manager):\n",
"\n",
" # Diese Variablenzuweisung speichert das WebDriver-Objekt des Managers.\n",
" driver = manager.driver\n",
"\n",
" # Diese Variablenzuweisung erstellt ein WebDriverWait-Objekt mit 20 Sekunden Timeout.\n",
" wait = WebDriverWait(driver, 20)\n",
"\n",
" # Diese Anweisung öffnet die 4 CSV-Dateien im Schreibmodus und legt die Header fest.\n",
" with open(\"../cache/plans.csv\", mode=\"w\", newline=\"\", encoding=\"utf-8\") as plansfile, \\\n",
" open(\"../cache/campaigns.csv\", mode=\"w\", newline=\"\", encoding=\"utf-8\") as campaignsfile, \\\n",
" open(\"../cache/options.csv\", mode=\"w\", newline=\"\", encoding=\"utf-8\") as optionsfile, \\\n",
" open(\"../cache/categorys.csv\", mode=\"w\", newline=\"\", encoding=\"utf-8\") as categorysfile:\n",
"\n",
" # Diese Variablenzuweisung erstellt Writer-Objekte für alle vier CSV-Dateien.\n",
" plans_writer = csv.writer(plansfile, delimiter=\";\")\n",
" campaigns_writer = csv.writer(campaignsfile, delimiter=\";\")\n",
" options_writer = csv.writer(optionsfile, delimiter=\";\")\n",
" categorys_writer = csv.writer(categorysfile, delimiter=\";\")\n",
"\n",
" # Diese Anweisung schreibt die Kopfzeilen in die jeweiligen CSV-Dateien.\n",
" plans_writer.writerow([\"id\", \"provider\", \"network\", \"name\", \"price\"])\n",
" campaigns_writer.writerow([\"id\", \"plan\", \"name\"])\n",
" options_writer.writerow([\"id\", \"category\", \"plan\", \"name\", \"price\"])\n",
" categorys_writer.writerow([\"id\", \"name\"])\n",
"\n",
" # Diese Variablenzuweisung sucht alle Elemente mit Name \"tarif_welt\" und extrahiert deren Werte in eine Liste.\n",
" tarifwelt_elements = driver.find_elements(By.NAME, \"tarif_welt\")\n",
" tarifwelten = [elem.get_attribute(\"value\") for elem in tarifwelt_elements if elem.get_attribute(\"value\")]\n",
"\n",
" # Diese Variablenzuweisung sucht alle Elemente mit Name \"netz\" und extrahiert deren Werte in eine Liste.\n",
" netz_elements = driver.find_elements(By.NAME, \"netz\")\n",
" netze = [elem.get_attribute(\"value\") for elem in netz_elements if elem.get_attribute(\"value\")]\n",
"\n",
" # Diese Variablenzuweisung wartet auf das Element mit name=\"sel_produkt_kategorie\" und value=\"A\" und klickt es.\n",
" element = wait.until(\n",
" EC.presence_of_element_located(\n",
" (By.XPATH, '//input[@name=\"sel_produkt_kategorie\" and @value=\"A\"]')\n",
" )\n",
" )\n",
" element.click()\n",
"\n",
" # Diese Schleife iteriert über alle gefundenen Tarifwelten.\n",
" for tw in tarifwelten:\n",
"\n",
" # Diese Variablenzuweisung wartet, bis das Radio-Element für die aktuelle Tarifwelt klickbar ist.\n",
" tw_radio = wait.until(\n",
" EC.element_to_be_clickable((By.XPATH, f'//input[@name=\"tarif_welt\" and @value=\"{tw}\"]'))\n",
" )\n",
"\n",
" # Diese Anweisung wartet, bis das Hintergrund-Layer-Element \"bg_layer\" unsichtbar ist.\n",
" WebDriverWait(driver, timeout=60).until(\n",
" EC.invisibility_of_element_located((By.ID, \"bg_layer\"))\n",
" )\n",
"\n",
" # Diese Anweisung klickt auf die Radio-Option für die aktuelle Tarifwelt.\n",
" driver.execute_script(\"arguments[0].click();\", tw_radio)\n",
"\n",
" # Diese Schleife iteriert über alle gefundenen Netze.\n",
" for net in netze:\n",
"\n",
" # Diese Variablenzuweisung wartet, bis das Radio-Element für das aktuelle Netz klickbar ist.\n",
" net_radio = wait.until(\n",
" EC.presence_of_element_located((By.XPATH, f'//input[@name=\"netz\" and @value=\"{net}\"]'))\n",
" )\n",
"\n",
" # Diese Anweisung klickt auf die Radio-Option für das aktuelle Netz.\n",
" driver.execute_script(\"arguments[0].click();\", net_radio)\n",
"\n",
" # Diese Anweisung wartet erneut, bis das Hintergrund-Layer-Element \"bg_layer\" unsichtbar ist.\n",
" WebDriverWait(driver, timeout=60).until(\n",
" EC.invisibility_of_element_located((By.ID, \"bg_layer\"))\n",
" )\n",
"\n",
" # Diese Anweisung ruft die Hilfsfunktion auf, um zu prüfen, ob das Dropdown bereit ist.\n",
" wait_for_dropdown_ready(driver, wait)\n",
"\n",
" # Diese Variablenzuweisung sucht das Dropdown-Element mit Name \"tarif_id\" und speichert es ab.\n",
" dropdown = wait.until(EC.presence_of_element_located((By.NAME, \"tarif_id\")))\n",
"\n",
" # Diese Variablenzuweisung erstellt ein Select-Objekt, um das Dropdown zu steuern.\n",
" select_obj = Select(dropdown)\n",
"\n",
" # Diese Variablenzuweisung ermittelt die Gesamtzahl der Optionen im Dropdown.\n",
" total_options = len(select_obj.options)\n",
"\n",
" # Diese Schleife iteriert über alle Tarif-Optionen des aktuellen Dropdowns.\n",
" for i in range(total_options):\n",
"\n",
" # Diese Anweisung sucht das Dropdown neu, um StaleElementReference zu vermeiden.\n",
" dropdown = wait.until(EC.presence_of_element_located((By.NAME, \"tarif_id\")))\n",
" select_obj = Select(dropdown)\n",
" all_opts = select_obj.options\n",
"\n",
" # Diese Bedingung überprüft, ob der Index über die Länge der verfügbaren Optionen hinausgeht.\n",
" if i >= len(all_opts):\n",
" break\n",
"\n",
" # Diese Variablenzuweisung nimmt die aktuelle Option aus der Dropdown-Liste.\n",
" opt = all_opts[i]\n",
"\n",
" # Diese Variablenzuweisung speichert den Text der aktuellen Option, bereinigt um Leerzeichen.\n",
" opt_text = opt.text.strip()\n",
"\n",
" # Diese Bedingung überspringt ungültige oder leere Optionen.\n",
" if opt_text in [\"Bitte wählen Sie aus...\", \"\"]:\n",
" continue\n",
"\n",
" # Diese Anweisung ruft erneut die Hilfsfunktion auf, um sicherzustellen, dass das Dropdown bereit ist.\n",
" wait_for_dropdown_ready(driver, wait)\n",
"\n",
" # Diese Anweisung wählt die Option mit dem gefundenen Text aus.\n",
" select_obj.select_by_visible_text(opt_text)\n",
"\n",
" # Diese Variablenzuweisung holt den Wert des ausgewählten Tarifs aus dem value-Attribut.\n",
" tariff_id = opt.get_attribute(\"value\")\n",
"\n",
" # Diese Anweisung gibt den aktuellen Tarif in der Konsole aus (Debug-Ausgabe).\n",
" print(f\"{tariff_id} - {net} - {opt_text}\")\n",
"\n",
" # Diese Anweisung wartet, bis das Hintergrund-Layer-Element \"bg_layer\" unsichtbar ist.\n",
" WebDriverWait(driver, timeout=60).until(\n",
" EC.invisibility_of_element_located((By.ID, \"bg_layer\"))\n",
" )\n",
"\n",
" # Diese Zuweisung wird erneut ausgeführt, damit das Dropdown-Objekt vorhanden bleibt.\n",
" dropdown = wait.until(EC.presence_of_element_located((By.NAME, \"tarif_id\")))\n",
"\n",
" # Diese Anweisung pausiert den Ablauf kurz, damit das Dropdown korrekt reagieren kann.\n",
" time.sleep(1)\n",
"\n",
" # Diese Variablenzuweisung parst den Nettopreis des aktuellen Tarifs.\n",
" plan_price_net = parse_plan_price(driver)\n",
"\n",
" # Diese Variablenzuweisung ruft die Funktion auf, um alle verfügbaren Aktionen zu ermitteln.\n",
" campaigns = parse_campaigns(driver)\n",
"\n",
" # Diese Anweisung schreibt den aktuellen Plan in die plans.csv.\n",
" plans_writer.writerow([\n",
" tariff_id,\n",
" tw,\n",
" net,\n",
" opt_text,\n",
" plan_price_net\n",
" ])\n",
"\n",
" # Diese Schleife durchläuft alle gefundenen Aktionen und schreibt sie in campaigns.csv.\n",
" for (camp_id, camp_name) in campaigns:\n",
" campaigns_writer.writerow([\n",
" camp_id,\n",
" tariff_id,\n",
" camp_name\n",
" ])\n",
"\n",
" # Diese Anweisung navigiert mittels JavaScript zur zweiten Seite (sim).\n",
" driver.execute_script(\"send_form(document.mobildaten, 'sim')\")\n",
"\n",
" # Diese Anweisung ruft die Funktion scrapeOption auf, um Tarifeinzelheiten und Optionen zu sammeln.\n",
" scrapeOption(\n",
" manager,\n",
" tariff_id,\n",
" net,\n",
" opt_text,\n",
" options_writer,\n",
" categorys_writer\n",
" )\n",
"\n",
"# Diese Funktion sammelt auf der zweiten Seite die Optionsdaten und schreibt sie zusammen mit den Tarifinformationen\n",
"# in die Dateien options.csv und categorys.csv.\n",
"def scrapeOption(manager, tariff_id, net, tariff_text, options_writer, categorys_writer):\n",
"\n",
" # Diese Variablenzuweisung speichert das WebDriver-Objekt des Managers.\n",
" driver = manager.driver\n",
"\n",
" # Diese Variablenzuweisung erstellt ein WebDriverWait-Objekt mit 20 Sekunden Timeout.\n",
" wait = WebDriverWait(driver, 20)\n",
"\n",
" # Diese Anweisung wartet, bis das Hintergrund-Layer-Element \"bg_layer\" unsichtbar ist.\n",
" WebDriverWait(driver, timeout=60).until(\n",
" EC.invisibility_of_element_located((By.ID, \"bg_layer\"))\n",
" )\n",
"\n",
" # Diese Variablenzuweisung wartet, bis ein Element mit der Klasse \"tb_back\" vorhanden ist, was für die zweite Seite kennzeichnend ist.\n",
" wait.until(EC.presence_of_element_located((By.CLASS_NAME, \"tb_back\")))\n",
"\n",
" # Diese Variablenzuweisung speichert den Seitenquelltext, um ihn später mit BeautifulSoup zu parsen.\n",
" html_content = driver.page_source\n",
"\n",
" # Diese Variablenzuweisung ruft die parse_options-Funktion auf, um Optionsdaten zu extrahieren.\n",
" options_data = parse_options(html_content)\n",
"\n",
" # Diese Schleife geht alle extrahierten Optionszeilen durch.\n",
" for line in options_data:\n",
"\n",
" # Diese Variablenzuweisung teilt die Zeile anhand des Semikolons auf.\n",
" parts = line.split(\";\")\n",
"\n",
" # parts[0] => category_id\n",
" # parts[1] => item_id\n",
" # parts[2] => category_name\n",
" # parts[3] => item_name\n",
" # parts[4] => price_str\n",
"\n",
" # Diese Variablenzuweisung konvertiert die Preisangabe in eine float-Zahl und dividiert durch 1.19.\n",
" try:\n",
" gross_price = float(parts[4])\n",
" except ValueError:\n",
" gross_price = 0.0\n",
"\n",
" # Diese Variablenzuweisung wandelt den Bruttopreis in Nettopreis um und rundet auf 5 Nachkommastellen.\n",
" net_price = round(gross_price / 1.19, 5)\n",
"\n",
" # Diese Anweisung schreibt in options.csv.\n",
" options_writer.writerow([\n",
" parts[1],\n",
" parts[0],\n",
" tariff_id,\n",
" parts[3],\n",
" net_price\n",
" ])\n",
"\n",
" # Diese Anweisung prüft, ob die Kategorie-ID bereits vorhanden ist. Falls nicht, wird sie geschrieben.\n",
" global unique_categories\n",
" if parts[0] not in unique_categories:\n",
" categorys_writer.writerow([\n",
" parts[0],\n",
" parts[2]\n",
" ])\n",
" unique_categories.add(parts[0])\n",
"\n",
" # Diese try-except-Struktur wartet optional auf ein bestimmtes Element, das per XPATH gesucht wird.\n",
" try:\n",
" element = wait.until(\n",
" lambda d: d.find_element(\n",
" By.XPATH, \"//a[contains(@href, \\\"send_form(document.tarifoptionen,'nur_fuer_layer')\\\")]\"\n",
" )\n",
" )\n",
" except:\n",
" pass\n",
"\n",
" # Diese Anweisung navigiert zurück zur ersten Seite (\"Mobildaten\"), um anschließend den nächsten Tarif wählen zu können.\n",
" driver.execute_script(\"jump_2_container('Mobildaten')\")\n",
"\n",
"# Diese Funktion parst den Seitenquelltext der zweiten Seite und sammelt Optionsdaten in einer Liste.\n",
"def parse_options(html_content):\n",
"\n",
" # Diese Variablenzuweisung erzeugt ein BeautifulSoup-Objekt, um die HTML-Struktur zu analysieren.\n",
" soup = BeautifulSoup(html_content, \"html.parser\")\n",
"\n",
" # Diese Variablenzuweisung erstellt eine leere Ergebnisliste.\n",
" results = []\n",
"\n",
" # Diese Variablenzuweisung definiert ein Muster, um die service_code-Einträge für Kategorien zu erkennen.\n",
" category_check_pattern = re.compile(r'service_code\\[(G\\d+)_check\\]')\n",
"\n",
" # Diese Variablenzuweisung definiert ein Muster, um Werte wie G\\d+ oder O\\d+ zu erkennen.\n",
" item_value_pattern = re.compile(r'^(G\\d+|O\\d+)$')\n",
"\n",
" # Diese Variablenzuweisung definiert ein Muster, um Preise im Format '/ € X,XX monatlich' zu erkennen.\n",
" price_pattern = re.compile(r'/\\s*€\\s*([\\d.,]+)\\s*monatlich', re.IGNORECASE)\n",
"\n",
" # Diese Variablenzuweisung findet alle Tabellen mit der Klasse \"tb_back\".\n",
" all_tables = soup.find_all(\"table\", class_=\"tb_back\")\n",
"\n",
" # Diese Schleife durchläuft alle gefundenen Tabellen und interpretiert sie als Kategorien.\n",
" for tbl in all_tables:\n",
"\n",
" # Diese Variablenzuweisung sucht in der aktuellen Tabelle nach einem Input-Feld mit Name service_code[...]_check.\n",
" cat_input = tbl.find(\"input\", attrs={\"name\": category_check_pattern})\n",
"\n",
" # Diese Bedingung prüft, ob ein solches Feld gefunden wurde.\n",
" if cat_input:\n",
" match = category_check_pattern.search(cat_input[\"name\"])\n",
" if match:\n",
" category_id = match.group(1)\n",
" else:\n",
" continue\n",
" else:\n",
" potential_inputs = tbl.find_all(\"input\", attrs={\"name\": re.compile(r'service_code\\[(G\\d+)\\]')})\n",
" if potential_inputs:\n",
" fallback_match = re.search(r'service_code\\[(G\\d+)\\]', potential_inputs[0][\"name\"])\n",
" if fallback_match:\n",
" category_id = fallback_match.group(1)\n",
" else:\n",
" continue\n",
" else:\n",
" continue\n",
"\n",
" # Diese Variablenzuweisung sucht nach dem Tabellenkopf mit der Klasse \"tb_head\", um den Kategorienamen zu lesen.\n",
" cat_name_el = tbl.find(\"td\", class_=\"tb_head\")\n",
"\n",
" # Diese Bedingung prüft, ob ein Tabellenkopf gefunden wurde.\n",
" if cat_name_el:\n",
" cat_text = cat_name_el.get_text(strip=True)\n",
" cat_text = re.sub(r'^\\xa0+', '', cat_text).strip()\n",
" else:\n",
" cat_text = \"Unbekannte Kategorie\"\n",
"\n",
" # Diese Bedingung entfernt HTML-NBSPs, falls vorhanden.\n",
" if cat_text.startswith(\"&nbsp;\"):\n",
" cat_text = cat_text.replace(\"&nbsp;\", \"\").strip()\n",
"\n",
" # Diese Bedingung setzt einen Standardwert, falls kein Text ermittelt werden konnte.\n",
" if not cat_text:\n",
" cat_text = \"Unbekannte Kategorie\"\n",
"\n",
" # Diese Variablenzuweisung sucht alle Input-Felder, deren value mit dem Muster G\\d+ oder O\\d+ übereinstimmt.\n",
" inputs = tbl.find_all(\"input\", attrs={\"value\": item_value_pattern})\n",
"\n",
" # Diese Schleife durchläuft alle gefundenen Inputs.\n",
" for inp in inputs:\n",
" item_id = inp.get(\"value\", \"\").strip()\n",
" if not item_id:\n",
" continue\n",
" item_label_tag = inp.find_next(\"a\", attrs={\"id\": f\"err_{item_id}\"})\n",
" if not item_label_tag:\n",
" item_label_tag = inp.find_next(\"a\", attrs={\"name\": f\"err_{item_id}\"})\n",
" if item_label_tag and item_label_tag.text.strip():\n",
" item_name = item_label_tag.text.strip()\n",
" else:\n",
" item_name = \"Unbekannt\"\n",
" container_for_price = item_label_tag.parent if item_label_tag else inp.parent\n",
" combined_text = container_for_price.get_text(\" \", strip=True) if container_for_price else \"\"\n",
" m_price = price_pattern.search(combined_text)\n",
" if m_price:\n",
" raw_price = m_price.group(1)\n",
" normalized = raw_price.replace(\",\", \".\")\n",
" try:\n",
" price_val = float(normalized)\n",
" except ValueError:\n",
" price_val = None\n",
" else:\n",
" price_val = None\n",
" if price_val is None:\n",
" price_str = \"0.0\"\n",
" else:\n",
" price_str = f\"{price_val}\"\n",
" if item_name == \"Unbekannt\" or cat_text == \"Unbekannte Kategorie\":\n",
" continue\n",
" if cat_text in [\"Sonstige Angaben\", \"Pflicht-Angaben\"]:\n",
" continue\n",
" line = f\"{category_id};{item_id};{cat_text};{item_name};{price_str}\"\n",
" results.append(line)\n",
"\n",
" # Diese Anweisung gibt die gesammelten Daten zurück.\n",
" return results\n",
"\n",
"# Diese Variablenzuweisung erzeugt eine Instanz des Managers mit den gewünschten Parametern.\n",
"manager = SeleniumManager(headless=False, geckodriver_path=\"/opt/homebrew/bin/geckodriver\")\n",
"\n",
"# Diese Anweisungen führen den gesamten Ablauf aus.\n",
"login(manager, MAUI_USERNAME, MAUI_PASSWORD, MAUI_AUTHCODE)\n",
"openLaufzeitvertrag(manager)\n",
"time.sleep(10)\n",
"scrapeData(manager)\n",
"time.sleep(10)\n",
"manager.closeDriver()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d6387255-71e0-40b4-8579-674320efc4db",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
jupyter lab
docker build --platform linux/amd64 -t obsidian:latest .
# dev
docker run -it -v ./commands:/obsidian/commands -v ./cache:/obsidian/cache -v ./config:/obsidian/config -v ./manager:/obsidian/manager -v ./models:/obsidian/models -v ./files:/obsidian/files -p 80:80 obsidian:latest /bin/bash
# detached
docker run -it -d obsidian:latest
# ecr
aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 181802255479.dkr.ecr.eu-central-1.amazonaws.com
docker tag obsidian:latest 181802255479.dkr.ecr.eu-central-1.amazonaws.com/obsidian:latest
docker push 181802255479.dkr.ecr.eu-central-1.amazonaws.com/obsidian:latest
docker pull 181802255479.dkr.ecr.eu-central-1.amazonaws.com/obsidian:latest
docker run -it -d --restart always -p 2000:80 181802255479.dkr.ecr.eu-central-1.amazonaws.com/obsidian:latest
####
# MAUI Data Toolkit
## Jupyter Lab
You can use "jupyter lab" to further develop or test this project.
```bash
jupyter lab
```
## Sidekick Documentation
Please use Sidekick for extended documentation and for maintaining the data structure.
```bash
docker run -it --rm -p 2000:8888 -v .:/app/work ceetrox/sidekick:latest
```
# Documentation
## Base (base_base)
In dieser Tabelle sind die grundlegenden Tarife gespeichert, die als Ausgangsbasis für das gesamte Datenbankschema dienen. Die hinterlegten Tarife bilden das Fundament, auf dem alle weiteren Strukturen aufbauen.
Beispielhafte Tarife:
```
Green Allnet Flat
Green Allnet Flat mit Handy 5
Green Allnet Flat mit Handy 10
```
### id_base
Dieser Primärschlüssel identifiziert jeden Tarif eindeutig in der Tabelle.
### basegroup_base
Dieses optionale Feld dient als Fremdschlüssel und verweist auf die Tabelle **Basegroup (basegroup_bgro)**, wodurch eine thematische Gruppierung der Tarife ermöglicht wird.
### provider_base
Hier wird der Name des Providers gespeichert, beispielsweise "Freenet" oder "Klarmobil".
### providercode_base
Ein optionales Feld, in dem ein zusätzlicher Provider-Code (Fremdschlüssel) hinterlegt werden kann.
### name_base
Dieses Feld enthält den offiziellen Tarifnamen, wie er vom Provider vorgegeben wird.
### alias_base
Hier kann ein alternativer Name definiert werden, der den offiziellen Tarifnamen ersetzt.
### network_base
In diesem Feld wird das verwendete Funknetz definiert.
**Mögliche Werte:**
- **1**: D1/Telekom
- **2**: D2/Vodafone
- **4**: O2/Telefonica
### type_base
Dieses Feld legt den Tariftyp fest.
**Mögliche Werte:**
- **1**: Voice
- **2**: Daten
### created_base
Der Zeitstempel, der angibt, wann der Datensatz erstellt wurde.
### updated_base
Der Zeitstempel, an dem der Datensatz zuletzt aktualisiert wurde.
---
## Basegroup (basegroup_bgro)
Diese Tabelle fasst Tarife in Gruppen zusammen, um eine übersichtliche Struktur zu ermöglichen. Die Gruppierung hilft dabei, ähnliche Tarife gemeinsam zu verwalten.
Beispielhafte Tarifgruppen:
```
Red Allnet Flat Tarifgruppe
Green Allnet Flat Tarifgruppe
Blue Allnet Flat Tarifgruppe
```
### id_bgro
Der Primärschlüssel der Tabelle, der jede Tarifgruppe eindeutig identifiziert.
### name_bgro
Hier wird der Name der Tarifgruppe festgelegt.
### created_bgro
Der Zeitpunkt, zu dem die Tarifgruppe erstellt wurde.
### updated_bgro
Der Zeitpunkt, zu dem die Tarifgruppe zuletzt aktualisiert wurde.
---
## Deal (deal_deal)
In dieser Tabelle werden zeitlich befristete Aktionen erfasst. Jede Aktion, jeder Deal verknüpft einen Basis-Tarif aus der Tabelle **Base (base_base)** mit einer Provisiongruppe und definiert spezielle Konditionen.
### id_deal
Der Primärschlüssel der Tabelle, der jede Aktion eindeutig identifiziert.
### base_deal
Dieses Feld ist ein Fremdschlüssel, der auf einen Tarif in der Tabelle **Base (base_base)** verweist.
### provisiongroup_deal
Dieses Feld definiert, unter welcher Provisionsgruppe dieser Tarif verwaltet wird.
### providercode_deal
Ein optionales Feld, das einen zusätzlichen Provider-Code (Fremdschlüssel) speichern kann.
### alias_deal
Ein optionaler, alternativer Name des Deals.
### price_deal
In diesem Feld wird der Preis des Deals als Dezimalwert festgehalten (Netto).
### starts_deal
Das Datum und die Uhrzeit, ab wann der Deal aktiv wird.
### stops_deal
Ein optionales Datum und eine Uhrzeit, bis zu denen der Deal gültig ist.
### provision1_deal bis provision4_deal
Diese Felder enthalten verschiedene Provisionswerte für für die Aktion.
Alle Provisionen zusammengerechnet ergeben die Gesamtprovision.
### created_deal
Der Zeitpunkt, zu dem der Deal-Datensatz erstellt wurde.
### updated_deal
Der Zeitpunkt, zu dem der Deal zuletzt aktualisiert wurde.
---
## Option (option_opti)
Diese Tabelle erfasst spezifische Optionen zu Tarifen, die zusätzliche Leistungen oder Konfigurationen darstellen. Optionen ergänzen die Basistarife und ermöglichen erweiterte Angebotsvarianten.
### id_opti
Der Primärschlüssel der Tabelle, der jede Option eindeutig identifiziert.
### base_opti
Dieses Feld fungiert als Fremdschlüssel und verweist auf den zugehörigen Tarif in der Tabelle **Base (base_base)**.
### provisiongroup_opti
Dieses Feld definiert, unter welcher Provisionsgruppe dieser Tarif verwaltet wird.
### providercode_opti
Ein optionales Feld zur Speicherung eines zusätzlichen Provider-Codes.
### providercategory_opti
Dieses optionale Feld dient der Kategorisierung der Option (Providerkategorie).
### name_opti
Das Feld enthält den offiziellen Namen der Option, wie er vom Provider vorgegeben wird.
### alias_opti
Ein optionaler Alias, der den offiziellen Namen ergänzen oder ersetzen kann.
### price_opti
In diesem Feld wird der Preis der Option als Dezimalwert festgehalten (Netto).
### starts_opti
Das Datum und die Uhrzeit, ab wann die Option aktiv wird.
### stops_opti
Ein optionales Datum und eine Uhrzeit, bis zu denen die Option gültig ist.
### provision1_opti bis provision4_opti
Diese Felder enthalten verschiedene Provisionswerte für für die Option.
Alle Provisionen zusammengerechnet ergeben die Gesamtprovision.
### created_opti
Der Zeitpunkt, zu dem der Options-Datensatz erstellt wurde.
### updated_opti
Der Zeitpunkt, zu dem der Options-Datensatz zuletzt aktualisiert wurde.
---
## Provisiongroup (provisiongroup_pgro)
In dieser Tabelle werden Provisiongruppen verwaltet, die sowohl in Deals als auch in Optionen Anwendung finden. Eine Provisiongruppe legt fest, wie viel Provision prozentual für den Vertrieb freigegeben wird.
### id_pgro
Der Primärschlüssel der Tabelle, der jede Provisiongruppe eindeutig identifiziert.
### name_pgro
Hier wird der Name der Provisiongruppe gespeichert.
### percent_pgro
Dieser Dezimalwert legt den Provisionsprozentsatz fest. Standardmäßig beträgt dieser 0.00.
### created_pgro
Der Zeitpunkt, zu dem der Datensatz der Provisiongruppe erstellt wurde.
### updated_pgro
Ein optionales Feld, das den Zeitpunkt der letzten Aktualisierung der Provisiongruppe festhält.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment