aktualisiert
This commit is contained in:
182
web_check.py
182
web_check.py
@@ -7,142 +7,243 @@ import sys
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv, main
|
||||||
|
|
||||||
# --- KONFIGURATION ---
|
# --- KONFIGURATION LADEN ---
|
||||||
# Laden der Umgebungsvariablen aus .env Datei
|
# Sucht nach einer Datei namens .env und lädt deren Variablen in das System
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
# os.getenv liest eine Variable aus dem System (die zuvor aus der .env geladen wurde)
|
||||||
ADMIN_USER = os.getenv("ADMIN_USER")
|
ADMIN_USER = os.getenv("ADMIN_USER")
|
||||||
ADMIN_PW = os.getenv("ADMIN_PW")
|
ADMIN_PW = os.getenv("ADMIN_PW")
|
||||||
NTFY_BASE = os.getenv("NTFY_BASE")
|
NTFY_BASE = os.getenv("NTFY_BASE")
|
||||||
|
|
||||||
|
# Name der Datei, in der die alten Webseiten-Stände gespeichert werden
|
||||||
JSON_DATEI = "./results.json"
|
JSON_DATEI = "./results.json"
|
||||||
|
# Die ID der Gemini-Sitzung, die für die Analyse genutzt werden soll
|
||||||
GEMINI_SESSION = "1"
|
GEMINI_SESSION = "1"
|
||||||
|
|
||||||
# ntfy-Themen (Öffentlich)
|
# Namen der ntfy-Topics für öffentliche News und Admin-Fehler
|
||||||
TOPIC_PUBLIC = "polit-scraper-public"
|
TOPIC_PUBLIC = "polit-scraper-public"
|
||||||
TOPIC_ADMIN = "polit-scraper-admin"
|
TOPIC_ADMIN = "polit-scraper-admin"
|
||||||
|
|
||||||
# UTF-8 für Konsole erzwingen
|
# Stellt sicher, dass das Skript Umlaute (ä, ö, ü) korrekt im Terminal ausgibt
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
# Cloudscraper initialisieren (Chrome-Emulation)
|
# Erstellt ein Scraper-Objekt, das sich als Chrome-Browser unter Windows ausgibt
|
||||||
|
# Dies dient dazu, Sicherheitsabfragen wie Cloudflare zu umgehen
|
||||||
scraper = cloudscraper.create_scraper(
|
scraper = cloudscraper.create_scraper(
|
||||||
browser={'browser': 'chrome', 'platform': 'windows', 'desktop': True}
|
browser={"browser": "chrome", "platform": "windows", "desktop": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def analysiere_mit_gemini(text, links):
|
def analysiere_mit_gemini(text, links):
|
||||||
"""KI-Analyse: Text und Links an Gemini senden, JSON extrahieren."""
|
"""KI-Analyse: Sendet den Text und die gefundenen Links an Gemini."""
|
||||||
|
|
||||||
|
# Erklärung der folgenden Zeile (Ternärer Operator):
|
||||||
|
# Wenn die Liste 'links' nicht leer ist, wird ein String mit der Überschrift erstellt
|
||||||
|
# und alle Links mit Kommas getrennt (", ".join) dahintergehängt.
|
||||||
|
# Wenn 'links' leer ist (else), wird einfach ein leerer String ("") zugewiesen.
|
||||||
links_text = "\nRelevante Links: " + ", ".join(links) if links else ""
|
links_text = "\nRelevante Links: " + ", ".join(links) if links else ""
|
||||||
|
|
||||||
|
# Baut die endgültige Frage (Prompt) für die KI zusammen
|
||||||
anweisung = f"Analysiere diesen Text: {text}{links_text}"
|
anweisung = f"Analysiere diesen Text: {text}{links_text}"
|
||||||
|
|
||||||
|
# Befehl für das Terminal: gemini -r [Sitzung] -p [Frage]
|
||||||
befehl = ["gemini", "-r", GEMINI_SESSION, "-p", anweisung]
|
befehl = ["gemini", "-r", GEMINI_SESSION, "-p", anweisung]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prozess = subprocess.run(befehl, capture_output=True, text=True, encoding='utf-8')
|
# Führt den Terminal-Befehl aus und speichert die Antwort
|
||||||
|
# capture_output=True fängt den Text ab, text=True macht daraus einen Python-String
|
||||||
|
prozess = subprocess.run(
|
||||||
|
befehl, capture_output=True, text=True, encoding="utf-8"
|
||||||
|
)
|
||||||
antwort_roh = prozess.stdout.strip()
|
antwort_roh = prozess.stdout.strip()
|
||||||
start = antwort_roh.find('{')
|
|
||||||
ende = antwort_roh.rfind('}') + 1
|
# Sucht den Anfang ({) und das Ende (}) des JSON-Objekts in der Antwort
|
||||||
|
# Das ist nötig, falls die KI zusätzlichen Text um das JSON herum schreibt
|
||||||
|
start = antwort_roh.find("{")
|
||||||
|
ende = antwort_roh.rfind("}") + 1
|
||||||
|
|
||||||
if start != -1 and ende != -1:
|
if start != -1 and ende != -1:
|
||||||
|
# json.loads macht aus dem Text ein echtes Python-Dictionary (Objekt)
|
||||||
return json.loads(antwort_roh[start:ende])
|
return json.loads(antwort_roh[start:ende])
|
||||||
|
|
||||||
|
# Falls kein JSON gefunden wurde, wird die Nachricht als nicht relevant eingestuft
|
||||||
return {"relevant": False}
|
return {"relevant": False}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [!] KI-Fehler: {e}")
|
print(f" [!] KI-Fehler: {e}")
|
||||||
return {"relevant": False}
|
return {"relevant": False}
|
||||||
|
|
||||||
|
|
||||||
def sende_public(titel, nachricht, klick_url, category="info", priority=3):
|
def sende_public(titel, nachricht, klick_url, category="info", priority=3):
|
||||||
"""Öffentlicher ntfy-Versand mit Priorität und Klick-URL."""
|
"""Sendet eine Push-Nachricht an den öffentlichen ntfy-Kanal."""
|
||||||
if not NTFY_BASE: return
|
if not NTFY_BASE:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Baut die Ziel-Adresse für ntfy zusammen
|
||||||
url = f"{NTFY_BASE}/{TOPIC_PUBLIC}"
|
url = f"{NTFY_BASE}/{TOPIC_PUBLIC}"
|
||||||
|
|
||||||
|
# Zuordnung von Kategorien zu Emojis für die Anzeige auf dem Handy
|
||||||
emoji_mapping = {
|
emoji_mapping = {
|
||||||
"demo": "rotating_light,loudspeaker",
|
"demo": "rotating_light,loudspeaker",
|
||||||
"socialmedia": "detective,no_entry",
|
"socialmedia": "detective,no_entry",
|
||||||
"polizei": "police_car,warning",
|
"polizei": "police_car,warning",
|
||||||
"stadt": "cityscape,newspaper",
|
"stadt": "cityscape,newspaper",
|
||||||
"solidaritaet": "fist,heart",
|
"solidaritaet": "fist,heart",
|
||||||
"info": "newspaper"
|
"info": "newspaper",
|
||||||
}
|
}
|
||||||
tags = emoji_mapping.get(category.lower(), "newspaper")
|
tags = emoji_mapping.get(category.lower(), "newspaper")
|
||||||
|
|
||||||
|
# ntfy-Header: Metadaten der Nachricht (Titel, Icons, Klick-Aktion, Wichtigkeit)
|
||||||
headers = {
|
headers = {
|
||||||
"Title": titel.encode('utf-8'),
|
"Title": titel.encode("utf-8"),
|
||||||
"Tags": tags,
|
"Tags": tags,
|
||||||
"Click": klick_url,
|
"Click": klick_url,
|
||||||
"Priority": str(priority)
|
"Priority": str(priority),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
requests.post(url, data=nachricht.encode("utf-8"), headers=headers,
|
|
||||||
auth=(ADMIN_USER, ADMIN_PW), timeout=5)
|
# POST-Anfrage sendet die Daten an den ntfy-Server
|
||||||
|
requests.post(
|
||||||
|
url,
|
||||||
|
data=nachricht.encode("utf-8"),
|
||||||
|
headers=headers,
|
||||||
|
auth=(ADMIN_USER, ADMIN_PW),
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [!] Fehler Public-Versand: {e}")
|
print(f" [!] Fehler Public-Versand: {e}")
|
||||||
|
|
||||||
|
|
||||||
def sende_admin(titel, nachricht):
|
def sende_admin(titel, nachricht):
|
||||||
"""Admin-Benachrichtigung bei technischen Fehlern."""
|
"""Sendet Fehlermeldungen an den passwortgeschützten Admin-Kanal."""
|
||||||
if not NTFY_BASE: return
|
if not NTFY_BASE:
|
||||||
|
return
|
||||||
url = f"{NTFY_BASE}/{TOPIC_ADMIN}"
|
url = f"{NTFY_BASE}/{TOPIC_ADMIN}"
|
||||||
headers = {"Title": titel.encode('utf-8'), "Tags": "warning,skull", "Priority": "4"}
|
headers = {"Title": titel.encode("utf-8"), "Tags": "warning,skull", "Priority": "4"}
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
requests.post(url, data=nachricht.encode("utf-8"), headers=headers,
|
|
||||||
auth=(ADMIN_USER, ADMIN_PW), timeout=5)
|
requests.post(
|
||||||
|
url,
|
||||||
|
data=nachricht.encode("utf-8"),
|
||||||
|
headers=headers,
|
||||||
|
auth=(ADMIN_USER, ADMIN_PW),
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [!] Fehler Admin-Versand: {e}")
|
print(f" [!] Fehler Admin-Versand: {e}")
|
||||||
|
|
||||||
|
|
||||||
def lade_gedaechtnis():
|
def lade_gedaechtnis():
|
||||||
if not os.path.exists(JSON_DATEI): return {}
|
"""Liest das results.json-File ein und gibt den Inhalt als Dictionary zurück."""
|
||||||
with open(JSON_DATEI, "r", encoding='utf-8') as f:
|
if not os.path.exists(JSON_DATEI):
|
||||||
try: return json.load(f)
|
return {}
|
||||||
except: return {}
|
with open(JSON_DATEI, "r", encoding="utf-8") as f:
|
||||||
|
try:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def speichere_gedaechtnis(daten):
|
def speichere_gedaechtnis(daten):
|
||||||
with open(JSON_DATEI, "w", encoding='utf-8') as f:
|
"""Speichert das aktuelle Wissen (daten) im JSON-Format auf die Festplatte."""
|
||||||
|
with open(JSON_DATEI, "w", encoding="utf-8") as f:
|
||||||
|
# indent=4 sorgt für eine lesbare Struktur (Einrückung) der Datei
|
||||||
json.dump(daten, f, indent=4, ensure_ascii=False)
|
json.dump(daten, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
def extrahiere_links(soup, basis_url):
|
def extrahiere_links(soup, basis_url):
|
||||||
|
"""Sucht alle <a>-Tags im HTML und macht daraus absolute Internetadressen."""
|
||||||
links = []
|
links = []
|
||||||
for a in soup.find_all('a', href=True):
|
# find_all('a') sucht alle Hyperlinks im HTML-Dokument
|
||||||
links.append(urljoin(basis_url, a['href']))
|
for a in soup.find_all("a", href=True):
|
||||||
|
# urljoin kombiniert die Basis-URL (z.B. pnp.de) mit dem Link (z.B. /artikel1)
|
||||||
|
links.append(urljoin(basis_url, a["href"]))
|
||||||
|
# list(set(links)) entfernt doppelte Einträge
|
||||||
return list(set(links))
|
return list(set(links))
|
||||||
|
|
||||||
def verarbeite_website(url, gedaechtnis):
|
|
||||||
|
def hole_artikel_text(url):
|
||||||
|
"""Rufe eine Unterseite auf und extrahiert den Haupttext."""
|
||||||
try:
|
try:
|
||||||
|
# Scraper um die Unterseite zu laden
|
||||||
|
antwort = scraper.get(url, timeout=10)
|
||||||
|
# Verwandle die Antwort in durchsuchbares HTML
|
||||||
|
unter_soup = BeautifulSoup(antwort.text, "htpm.parser")
|
||||||
|
# Suche nach Hauptbereich (wie im Haptscript)
|
||||||
|
bereich = (
|
||||||
|
unter_soup.find("main") or unter_soup.find("article") or unter_soup.body
|
||||||
|
)
|
||||||
|
# Gibt den reinen Text ohne Leerzeichen-Salat zurück
|
||||||
|
return bereich.get_text(separator=" ", strip=True)
|
||||||
|
except Exception as e:
|
||||||
|
# Fals etwas schiefgeht, gib einen Hinweis zurück
|
||||||
|
return f"Fehler beim Laden des Artikels!: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def verarbeite_website(url, gedaechtnis):
|
||||||
|
"""Kern-Logik für eine einzelne Webseite."""
|
||||||
|
try:
|
||||||
|
# Lädt die Webseite mit dem Cloudscraper
|
||||||
antwort = scraper.get(url, timeout=15)
|
antwort = scraper.get(url, timeout=15)
|
||||||
antwort.encoding = 'utf-8'
|
antwort.encoding = "utf-8"
|
||||||
status = antwort.status_code
|
status = antwort.status_code
|
||||||
|
|
||||||
|
# Bei Server-Fehlern (z.B. 404 oder 500) wird der Admin informiert
|
||||||
if status != 200:
|
if status != 200:
|
||||||
print(f" [!] Seite fehlerhaft: {url} (Status: {status})")
|
print(f" [!] Seite fehlerhaft: {url} (Status: {status})")
|
||||||
sende_admin("Fehler bei Webseite", f"{url} lieferte Status {status}.")
|
sende_admin("Fehler bei Webseite", f"{url} lieferte Status {status}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Verwandelt den rohen HTML-Text in ein BeautifulSoup-Objekt zum Suchen
|
||||||
soup = BeautifulSoup(antwort.text, "html.parser")
|
soup = BeautifulSoup(antwort.text, "html.parser")
|
||||||
|
# Sucht nach dem Inhaltsbereich (main, article oder body)
|
||||||
hauptbereich = soup.find("main") or soup.find("article") or soup.body or soup
|
hauptbereich = soup.find("main") or soup.find("article") or soup.body or soup
|
||||||
|
# Extrahiert den reinen Text ohne HTML-Tags
|
||||||
neuer_text = hauptbereich.get_text(separator=" ", strip=True)
|
neuer_text = hauptbereich.get_text(separator=" ", strip=True)
|
||||||
|
|
||||||
|
# Holt den alten Text dieser URL aus dem Gedächtnis
|
||||||
alter_eintrag = gedaechtnis.get(url, {})
|
alter_eintrag = gedaechtnis.get(url, {})
|
||||||
alter_text = alter_eintrag.get("inhalt", "")
|
alter_text = alter_eintrag.get("inhalt", "")
|
||||||
|
|
||||||
|
# Prüft, ob sich der Text seit dem letzten Lauf verändert hat
|
||||||
if alter_text != neuer_text:
|
if alter_text != neuer_text:
|
||||||
print(f" [NEU] Änderung auf {url} gefunden. Ich frage die KI...")
|
print(f" [NEU] Änderung auf {url} gefunden. Ich frage die KI...")
|
||||||
|
# Zerlegt den Text in Zeilen
|
||||||
alt_z = alter_text.splitlines()
|
alt_z = alter_text.splitlines()
|
||||||
neu_z = neuer_text.splitlines()
|
neu_z = neuer_text.splitlines()
|
||||||
|
# Findet nur die Zeilen, die neu dazugekommen sind (+ )
|
||||||
diff = [z[2:] for z in difflib.ndiff(alt_z, neu_z) if z.startswith("+ ")]
|
diff = [z[2:] for z in difflib.ndiff(alt_z, neu_z) if z.startswith("+ ")]
|
||||||
text_fuer_ki = "\n".join(diff)
|
text_fuer_ki = "\n".join(diff)
|
||||||
|
|
||||||
|
# Sammelt alle Links aus dem Bereich für die KI
|
||||||
alle_links = extrahiere_links(hauptbereich, url)
|
alle_links = extrahiere_links(hauptbereich, url)
|
||||||
|
|
||||||
|
# Nur wenn auch wirklich neuer Text vorhanden ist
|
||||||
if text_fuer_ki.strip():
|
if text_fuer_ki.strip():
|
||||||
analyse = analysiere_mit_gemini(text_fuer_ki, alle_links)
|
analyse = analysiere_mit_gemini(text_fuer_ki, alle_links)
|
||||||
|
# Falls die KI die Änderung als politisch wichtig einstuft
|
||||||
if analyse.get("relevant") == True:
|
if analyse.get("relevant") == True:
|
||||||
print(f" [!] KI bestätigt Relevanz: {analyse.get('titel')}")
|
print(f" [!] KI bestätigt Relevanz: {analyse.get('titel')}")
|
||||||
|
# Nutzt den direkten Link der KI oder die Haupt-URL
|
||||||
klick_url = analyse.get("direct_link", url)
|
klick_url = analyse.get("direct_link", url)
|
||||||
sende_public(
|
sende_public(
|
||||||
titel=analyse.get("titel", "Polit-Update"),
|
titel=analyse.get("titel", "Polit-Update"),
|
||||||
nachricht=analyse.get("nachricht", "Neu auf der Seite"),
|
nachricht=analyse.get("nachricht", "Neu auf der Seite"),
|
||||||
klick_url=klick_url,
|
klick_url=klick_url,
|
||||||
category=analyse.get("category", "info"),
|
category=analyse.get("category", "info"),
|
||||||
priority=analyse.get("priority", 3)
|
priority=analyse.get("priority", 3),
|
||||||
)
|
)
|
||||||
|
|
||||||
gedaechtnis[url] = {"inhalt": neuer_text, "zeit": str(datetime.now()), "status": status}
|
# Speichert den neuen Stand dieser Webseite im lokalen Gedächtnis
|
||||||
|
gedaechtnis[url] = {
|
||||||
|
"inhalt": neuer_text,
|
||||||
|
"zeit": str(datetime.now()),
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
print(f" [OK] Keine Änderungen auf {url}.")
|
print(f" [OK] Keine Änderungen auf {url}.")
|
||||||
|
|
||||||
@@ -150,14 +251,19 @@ def verarbeite_website(url, gedaechtnis):
|
|||||||
print(f" ❌ Fehler bei {url}: {e}")
|
print(f" ❌ Fehler bei {url}: {e}")
|
||||||
sende_admin("Programmfehler", f"{url}: {e}")
|
sende_admin("Programmfehler", f"{url}: {e}")
|
||||||
|
|
||||||
|
|
||||||
# --- HAUPTPROGRAMM ---
|
# --- HAUPTPROGRAMM ---
|
||||||
|
# Startet den Prozess: Gedächtnis laden -> URLs prüfen -> Gedächtnis speichern
|
||||||
gedaechtnis = lade_gedaechtnis()
|
gedaechtnis = lade_gedaechtnis()
|
||||||
with open("./urls.txt", "r", encoding='utf-8') as datei:
|
with open("./urls.txt", "r", encoding="utf-8") as datei:
|
||||||
for zeile in datei:
|
for zeile in datei:
|
||||||
|
# Entfernt Leerzeichen und ignoriert Kommentarzeilen (#)
|
||||||
adresse = zeile.strip()
|
adresse = zeile.strip()
|
||||||
if not adresse or adresse.startswith("#"): continue
|
if not adresse or adresse.startswith("#"):
|
||||||
|
continue
|
||||||
print(f"Ich prüfe: {adresse}")
|
print(f"Ich prüfe: {adresse}")
|
||||||
verarbeite_website(adresse, gedaechtnis)
|
verarbeite_website(adresse, gedaechtnis)
|
||||||
|
|
||||||
|
# Speichert das aktualisierte Wissen für den nächsten Cronjob-Lauf
|
||||||
speichere_gedaechtnis(gedaechtnis)
|
speichere_gedaechtnis(gedaechtnis)
|
||||||
print("\nFertig. Gute Nacht!")
|
print("\nFertig. Gute Nacht!")
|
||||||
|
|||||||
Reference in New Issue
Block a user