Files
polit-scraper/web_check.py
2026-03-25 12:54:17 +01:00

329 lines
11 KiB
Python

import cloudscraper
import json
import os
import sqlite3
import subprocess
import difflib
import sys
import re
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from datetime import datetime
from dotenv import load_dotenv
from requests import models
# --- KONFIGURATION LADEN ---
load_dotenv()
ADMIN_USER = os.getenv("ADMIN_USER")
ADMIN_PW = os.getenv("ADMIN_PW")
NTFY_BASE = os.getenv("NTFY_BASE")
DEBUG_LOG = os.getenv("DEBUG_LOG", "false").lower() == "true"
MIN_RELEVANZ_SCORE = int(os.getenv("MIN_RELEVANZ_SCORE", "30"))
GEMINI_MODELu = "gemini-3.1-flash-lite-preview"
GEMINI_SESSION = "1"
TOPIC_PUBLIC = "polit-scraper-public"
TOPIC_ADMIN = "polit-scraper-admin"
sys.stdout.reconfigure(encoding="utf-8")
scraper = cloudscraper.create_scraper(
browser={"browser": "chrome", "platform": "windows", "desktop": True}
)
def debug(nachricht):
if DEBUG_LOG:
zeit = datetime.now().strftime("%H:%M:%S")
print(f" [DEBUG {zeit}] {nachricht}")
# --- DATENBANK SETUP ---
conn = sqlite3.connect("polit_scraper.db", timeout=20)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS seiten_stand (
url TEXT PRIMARY KEY,
inhalt TEXT,
zeit TEXT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS artikel (
artikel_url TEXT PRIMARY KEY,
status TEXT
)
""")
conn.commit()
def analysiere_mit_gemini(text):
"""KI-Analyse: Bewertet NUR den Text laut V8 (inkl. Begründung)."""
sicherer_text = text[:5000]
anweisung = f"WICHTIG: Analysiere NUR den folgenden Text laut deinen Panda-Regeln (V8).\n\nTEXT:\n{sicherer_text}"
befehl = ["gemini", "-m", GEMINI_MODEL, "-r", GEMINI_SESSION, "-p", anweisung]
try:
prozess = subprocess.run(
befehl, capture_output=True, text=True, encoding="utf-8", timeout=300
)
antwort_roh = prozess.stdout.strip()
start = antwort_roh.find("{")
ende = antwort_roh.rfind("}") + 1
if start != -1 and ende != -1:
return json.loads(antwort_roh[start:ende])
return {
"relevant": False,
"relevanz_score": 0,
"begruendung_score": "Fehler: Kein JSON gefunden",
}
except Exception as e:
print(f" [!] KI-Fehler: {e}")
return {
"relevant": False,
"relevanz_score": 0,
"begruendung_score": f"Systemfehler: {e}",
}
def sende_public(titel, nachricht, klick_url, category="info", priority=3):
"""Sendet Push-Nachricht an ntfy."""
if not NTFY_BASE:
return
url = f"{NTFY_BASE}/{TOPIC_PUBLIC}"
emoji_mapping = {
"demo": "rotating_light,loudspeaker",
"socialmedia": "detective,no_entry",
"polizei": "police_car,warning",
"stadt": "cityscape,newspaper",
"solidaritaet": "fist,heart",
"info": "newspaper",
}
tags = emoji_mapping.get(category.lower(), "newspaper")
headers = {
"Title": titel.encode("utf-8"),
"Tags": tags,
"Click": klick_url,
"Priority": str(priority),
}
try:
import requests
requests.post(
url,
data=nachricht.encode("utf-8"),
headers=headers,
auth=(ADMIN_USER, ADMIN_PW),
timeout=10,
)
except Exception as e:
print(f" [!] Fehler Public-Versand: {e}")
def sende_admin(titel, nachricht):
if not NTFY_BASE:
return
url = f"{NTFY_BASE}/{TOPIC_ADMIN}"
headers = {"Title": titel.encode("utf-8"), "Tags": "warning,skull", "Priority": "4"}
try:
import requests
requests.post(
url,
data=nachricht.encode("utf-8"),
headers=headers,
auth=(ADMIN_USER, ADMIN_PW),
timeout=10,
)
except Exception as e:
print(f" [!] Fehler Admin-Versand: {e}")
def bereinige_url(url_roh, basis_url):
clean = url_roh.replace("", "").replace('"', "").replace("'", "").strip()
if clean.lower().startswith("http"):
return clean
return urljoin(basis_url, clean)
def extrahiere_links(soup, basis_url):
links = []
ignore_ext = (".pdf", ".jpg", ".jpeg", ".png", ".gif", ".zip", ".docx")
basis_domain = urlparse(basis_url).netloc
for a in soup.find_all("a", href=True):
full_url = bereinige_url(a["href"], basis_url)
if full_url.lower().endswith(ignore_ext):
continue
if urlparse(full_url).netloc == basis_domain:
if len(full_url) > len(basis_url.rstrip("/")) + 1:
links.append(full_url)
return list(set(links))
def hole_artikel_text(url):
try:
antwort = scraper.get(url, timeout=10)
unter_soup = BeautifulSoup(antwort.text, "html.parser")
bereich = (
unter_soup.find("main") or unter_soup.find("article") or unter_soup.body
)
text = bereich.get_text(separator=" ", strip=True)
if len(text) > 12000:
return text[:12000]
return text
except Exception as e:
return f"Fehler beim Laden: {e}"
def finde_link_fuer_text(text_snippet, soup, basis_url):
schnipsel = text_snippet.strip().lower()
if len(schnipsel) < 10:
return None
for a in soup.find_all("a", href=True):
link_text = a.get_text(separator=" ", strip=True).lower()
if schnipsel in link_text or link_text in schnipsel:
return bereinige_url(a["href"], basis_url)
return None
def verarbeite_website(url):
"""Hauptlogik mit Scoring-Logik und detaillierter Begründung im Debug-Modus."""
try:
antwort = scraper.get(url, timeout=15)
if antwort.status_code != 200:
sende_admin(
"Fehler bei Webseite", f"{url} lieferte Status {antwort.status_code}."
)
return
soup = BeautifulSoup(antwort.text, "html.parser")
hauptbereich = soup.find("main") or soup.find("article") or soup.body or soup
neuer_text = hauptbereich.get_text(separator=" ", strip=True)
cursor.execute("SELECT inhalt FROM seiten_stand WHERE url = ?", (url,))
zeile = cursor.fetchone()
alter_text = zeile[0] if zeile else ""
aktuelle_links = extrahiere_links(hauptbereich, url)
neue_artikel_links = []
for l in aktuelle_links:
cursor.execute("SELECT 1 FROM artikel WHERE artikel_url = ?", (l,))
if cursor.fetchone() is None and l not in neue_artikel_links:
neue_artikel_links.append(l)
if neue_artikel_links or (alter_text != neuer_text):
print(f" [NEU] Aktivität auf {url} erkannt.")
# FALL A: Neue Deep-Links
for link in neue_artikel_links[:5]:
print(f" [..] Analysiere neuen Artikel: {link}")
artikel_inhalt = hole_artikel_text(link)
analyse = analysiere_mit_gemini(artikel_inhalt)
score = analyse.get("relevanz_score", 0)
begruendung = analyse.get(
"begruendung_score", "Keine Begründung vorhanden"
)
if analyse.get("relevant") and score >= MIN_RELEVANZ_SCORE:
print(f" [!] RELEVANT (Score: {score}): {analyse.get('titel')}")
debug(f"Begründung: {begruendung}")
sende_public(
titel=analyse.get("titel", "Polit-Update"),
nachricht=analyse.get("nachricht", "Neu auf der Seite"),
klick_url=link,
category=analyse.get("category", "info"),
priority=analyse.get("priority", 3),
)
else:
debug(
f"Überspringe Artikel (Score {score} < {MIN_RELEVANZ_SCORE}). Begründung: {begruendung}"
)
cursor.execute(
"INSERT OR IGNORE INTO artikel (artikel_url, status) VALUES (?, ?)",
(link, "erledigt"),
)
conn.commit()
# FALL B: Textänderung auf Hauptseite
if alter_text != neuer_text:
diff = [
z[2:]
for z in difflib.ndiff(
alter_text.splitlines(), neuer_text.splitlines()
)
if z.startswith("+ ")
]
for line in diff:
if len(line.strip()) < 20:
continue
passender_link = finde_link_fuer_text(line[:40], hauptbereich, url)
target_url = passender_link if passender_link else url
cursor.execute(
"SELECT 1 FROM artikel WHERE artikel_url = ?", (target_url,)
)
if cursor.fetchone() is None:
if passender_link:
artikel_inhalt = hole_artikel_text(passender_link)
analyse = analysiere_mit_gemini(artikel_inhalt)
else:
analyse = analysiere_mit_gemini(line)
score = analyse.get("relevanz_score", 0)
begruendung = analyse.get(
"begruendung_score", "Keine Begründung vorhanden"
)
if analyse.get("relevant") and score >= MIN_RELEVANZ_SCORE:
print(
f" [!] RELEVANT (Score: {score}): {analyse.get('titel')}"
)
debug(f"Begründung: {begruendung}")
sende_public(
analyse.get("titel"),
analyse.get("nachricht"),
target_url,
analyse.get("category"),
analyse.get("priority"),
)
else:
debug(
f"Überspringe Änderung (Score {score} < {MIN_RELEVANZ_SCORE}). Begründung: {begruendung}"
)
cursor.execute(
"INSERT OR IGNORE INTO artikel (artikel_url, status) VALUES (?, ?)",
(target_url, "erledigt"),
)
conn.commit()
cursor.execute(
"INSERT OR REPLACE INTO seiten_stand (url, inhalt, zeit) VALUES (?, ?, ?)",
(url, neuer_text, str(datetime.now())),
)
conn.commit()
else:
debug(f"[OK] Keine Änderungen auf {url}.")
except Exception as e:
print(f" ❌ Fehler bei {url}: {e}")
# --- HAUPTPROGRAMM ---
with open("./urls.txt", "r", encoding="utf-8") as datei:
for zeile in datei:
adresse = zeile.strip()
if not adresse or adresse.startswith("#"):
continue
print(f"Ich prüfe: {adresse}")
verarbeite_website(adresse)
conn.close()
print("\nFertig. Gute Nacht!")