520 Zeilen
22 KiB
Python
520 Zeilen
22 KiB
Python
"""
|
|
TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI
|
|
"""
|
|
|
|
import time
|
|
import re
|
|
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
|
|
|
from .tiktok_selectors import TikTokSelectors
|
|
from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button
|
|
from utils.logger import setup_logger
|
|
|
|
# Konfiguriere Logger
|
|
logger = setup_logger("tiktok_ui_helper")
|
|
|
|
class TikTokUIHelper:
|
|
"""
|
|
Hilfsmethoden für die Interaktion mit der TikTok-Benutzeroberfläche.
|
|
Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen.
|
|
"""
|
|
|
|
def __init__(self, automation):
|
|
"""
|
|
Initialisiert den TikTok-UI-Helper.
|
|
|
|
Args:
|
|
automation: Referenz auf die Hauptautomatisierungsklasse
|
|
"""
|
|
self.automation = automation
|
|
# Browser wird direkt von automation verwendet
|
|
self.selectors = TikTokSelectors()
|
|
|
|
# Initialisiere TextSimilarity für Fuzzy-Matching
|
|
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
|
|
|
logger.debug("TikTok-UI-Helper initialisiert")
|
|
|
|
def _ensure_browser(self) -> bool:
|
|
"""
|
|
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
|
|
|
Returns:
|
|
bool: True wenn Browser verfügbar, False sonst
|
|
"""
|
|
if self.automation.browser is None:
|
|
logger.error("Browser-Referenz nicht verfügbar")
|
|
return False
|
|
|
|
return True
|
|
|
|
def fill_field_fuzzy(self, field_labels: Union[str, List[str]],
|
|
value: str, fallback_selector: str = None,
|
|
threshold: float = 0.7, timeout: int = 5000) -> bool:
|
|
"""
|
|
Füllt ein Formularfeld mit Fuzzy-Text-Matching aus.
|
|
|
|
Args:
|
|
field_labels: Bezeichner oder Liste von Bezeichnern des Feldes
|
|
value: Einzugebender Wert
|
|
fallback_selector: CSS-Selektor für Fallback
|
|
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
|
timeout: Zeitlimit für die Suche in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Normalisiere field_labels zu einer Liste
|
|
if isinstance(field_labels, str):
|
|
field_labels = [field_labels]
|
|
|
|
# Versuche, das Feld mit Fuzzy-Matching zu finden
|
|
element = fuzzy_find_element(
|
|
self.automation.browser.page,
|
|
field_labels,
|
|
selector_type="input",
|
|
threshold=threshold,
|
|
wait_time=timeout
|
|
)
|
|
|
|
if element:
|
|
# Versuche, das Feld zu fokussieren und den Wert einzugeben
|
|
element.focus()
|
|
time.sleep(0.1)
|
|
element.fill("") # Leere das Feld zuerst
|
|
time.sleep(0.2)
|
|
|
|
# Text menschenähnlich eingeben
|
|
for char in value:
|
|
element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000)
|
|
time.sleep(0.01)
|
|
|
|
logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}")
|
|
return True
|
|
|
|
# Fuzzy-Matching fehlgeschlagen, versuche über Attribute
|
|
if fallback_selector:
|
|
field_success = self.automation.browser.fill_form_field(fallback_selector, value)
|
|
if field_success:
|
|
logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}")
|
|
return True
|
|
|
|
# Versuche noch alternative Selektoren basierend auf field_labels
|
|
for label in field_labels:
|
|
# Versuche aria-label Attribut
|
|
aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']"
|
|
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
|
if self.automation.browser.fill_form_field(aria_selector, value):
|
|
logger.info(f"Feld über aria-label gefüllt: {label}")
|
|
return True
|
|
|
|
# Versuche placeholder Attribut
|
|
placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']"
|
|
if self.automation.browser.is_element_visible(placeholder_selector, timeout=1000):
|
|
if self.automation.browser.fill_form_field(placeholder_selector, value):
|
|
logger.info(f"Feld über placeholder gefüllt: {label}")
|
|
return True
|
|
|
|
# Versuche name Attribut
|
|
name_selector = f"input[name='{label.lower().replace(' ', '')}']"
|
|
if self.automation.browser.is_element_visible(name_selector, timeout=1000):
|
|
if self.automation.browser.fill_form_field(name_selector, value):
|
|
logger.info(f"Feld über name-Attribut gefüllt: {label}")
|
|
return True
|
|
|
|
logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}")
|
|
return False
|
|
|
|
def click_button_fuzzy(self, button_texts: Union[str, List[str]],
|
|
fallback_selector: str = None, threshold: float = 0.7,
|
|
timeout: int = 5000) -> bool:
|
|
"""
|
|
Klickt einen Button mit Fuzzy-Text-Matching.
|
|
|
|
Args:
|
|
button_texts: Text oder Liste von Texten des Buttons
|
|
fallback_selector: CSS-Selektor für Fallback
|
|
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
|
timeout: Zeitlimit für die Suche in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Normalisiere button_texts zu einer Liste
|
|
if isinstance(button_texts, str):
|
|
button_texts = [button_texts]
|
|
|
|
# Logging der Suche
|
|
logger.info(f"Suche nach Button mit Texten: {button_texts}")
|
|
|
|
if not button_texts or button_texts == [[]]:
|
|
logger.warning("Leere Button-Text-Liste angegeben!")
|
|
return False
|
|
|
|
# TikTok-spezifische Selektoren zuerst prüfen
|
|
# Diese Selektoren sind häufig in TikTok's UI zu finden
|
|
tiktok_button_selectors = [
|
|
"button[type='submit']",
|
|
"button[data-e2e='send-code-button']",
|
|
"button.e1w6iovg0",
|
|
"button.css-10nhlj9-Button-StyledButton"
|
|
]
|
|
|
|
for selector in tiktok_button_selectors:
|
|
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
|
button_element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
|
if button_element:
|
|
button_text = button_element.inner_text().strip()
|
|
|
|
# Überprüfe, ob der Button-Text mit einem der gesuchten Texte übereinstimmt
|
|
for text in button_texts:
|
|
if self.text_similarity.is_similar(text, button_text, threshold=threshold):
|
|
logger.info(f"Button mit passendem Text gefunden: '{button_text}'")
|
|
button_element.click()
|
|
return True
|
|
|
|
# Die allgemeine fuzzy_click_button-Funktion verwenden
|
|
result = click_fuzzy_button(
|
|
self.automation.browser.page,
|
|
button_texts,
|
|
threshold=threshold,
|
|
timeout=timeout
|
|
)
|
|
|
|
if result:
|
|
logger.info(f"Button mit Fuzzy-Matching geklickt")
|
|
return True
|
|
|
|
# Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector
|
|
if fallback_selector:
|
|
logger.info(f"Versuche Fallback-Selektor: {fallback_selector}")
|
|
if self.automation.browser.click_element(fallback_selector):
|
|
logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}")
|
|
return True
|
|
|
|
# Versuche alternative Methoden
|
|
|
|
# 1. Versuche über aria-label
|
|
for text in button_texts:
|
|
if not text:
|
|
continue
|
|
|
|
aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']"
|
|
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
|
if self.automation.browser.click_element(aria_selector):
|
|
logger.info(f"Button über aria-label geklickt: {text}")
|
|
return True
|
|
|
|
# 2. Versuche über role='button' mit Text
|
|
for text in button_texts:
|
|
if not text:
|
|
continue
|
|
|
|
xpath_selector = f"//div[@role='button' and contains(., '{text}')]"
|
|
if self.automation.browser.is_element_visible(xpath_selector, timeout=1000):
|
|
if self.automation.browser.click_element(xpath_selector):
|
|
logger.info(f"Button über role+text geklickt: {text}")
|
|
return True
|
|
|
|
# 3. Versuche über Link-Text
|
|
for text in button_texts:
|
|
if not text:
|
|
continue
|
|
|
|
link_selector = f"//a[contains(text(), '{text}')]"
|
|
if self.automation.browser.is_element_visible(link_selector, timeout=1000):
|
|
if self.automation.browser.click_element(link_selector):
|
|
logger.info(f"Link mit passendem Text geklickt: {text}")
|
|
return True
|
|
|
|
# 4. Als letzten Versuch, klicke auf einen beliebigen Button
|
|
logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken")
|
|
buttons = self.automation.browser.page.query_selector_all("button")
|
|
if buttons and len(buttons) > 0:
|
|
for button in buttons:
|
|
visible = button.is_visible()
|
|
if visible:
|
|
logger.info("Klicke auf beliebigen sichtbaren Button")
|
|
button.click()
|
|
return True
|
|
|
|
logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}")
|
|
return False
|
|
|
|
def select_dropdown_option(self, dropdown_selector: str, option_value: str,
|
|
option_type: str = "text", timeout: int = 5000) -> bool:
|
|
"""
|
|
Wählt eine Option aus einer Dropdown-Liste aus.
|
|
|
|
Args:
|
|
dropdown_selector: Selektor für das Dropdown-Element
|
|
option_value: Wert oder Text der auszuwählenden Option
|
|
option_type: "text" für Text-Matching, "value" für Wert-Matching
|
|
timeout: Zeitlimit in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Auf Dropdown-Element klicken, um die Optionen anzuzeigen
|
|
dropdown_element = self.automation.browser.wait_for_selector(dropdown_selector, timeout=timeout)
|
|
if not dropdown_element:
|
|
logger.warning(f"Dropdown-Element nicht gefunden: {dropdown_selector}")
|
|
return False
|
|
|
|
# Dropdown öffnen
|
|
dropdown_element.click()
|
|
time.sleep(0.5) # Kurz warten, damit die Optionen angezeigt werden
|
|
|
|
# Optionen suchen
|
|
option_selector = "div[role='option']"
|
|
options = self.automation.browser.page.query_selector_all(option_selector)
|
|
|
|
if not options or len(options) == 0:
|
|
logger.warning(f"Keine Optionen gefunden für Dropdown: {dropdown_selector}")
|
|
return False
|
|
|
|
# Option nach Text oder Wert suchen
|
|
selected = False
|
|
for option in options:
|
|
option_text = option.inner_text().strip()
|
|
|
|
if option_type == "text":
|
|
if option_text == option_value or self.text_similarity.is_similar(option_text, option_value, threshold=0.9):
|
|
option.click()
|
|
selected = True
|
|
break
|
|
elif option_type == "value":
|
|
option_val = option.get_attribute("value") or ""
|
|
if option_val == option_value:
|
|
option.click()
|
|
selected = True
|
|
break
|
|
|
|
if not selected:
|
|
logger.warning(f"Keine passende Option für '{option_value}' gefunden")
|
|
return False
|
|
|
|
logger.info(f"Option '{option_value}' im Dropdown ausgewählt")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Auswahl der Dropdown-Option: {e}")
|
|
return False
|
|
|
|
def check_for_error(self, error_selectors: List[str] = None,
|
|
error_texts: List[str] = None) -> Optional[str]:
|
|
"""
|
|
Überprüft, ob Fehlermeldungen angezeigt werden.
|
|
|
|
Args:
|
|
error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen
|
|
error_texts: Liste mit typischen Fehlertexten
|
|
|
|
Returns:
|
|
Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden
|
|
"""
|
|
if not self._ensure_browser():
|
|
return None
|
|
|
|
try:
|
|
# Standardselektoren verwenden, wenn keine angegeben sind
|
|
if error_selectors is None:
|
|
error_selectors = [
|
|
"div[role='alert']",
|
|
"p[class*='error']",
|
|
"span[class*='error']",
|
|
".error-message"
|
|
]
|
|
|
|
# Standardfehlertexte verwenden, wenn keine angegeben sind
|
|
if error_texts is None:
|
|
error_texts = TikTokSelectors.get_error_indicators()
|
|
|
|
# 1. Nach Fehlerselektoren suchen
|
|
for selector in error_selectors:
|
|
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
|
if element:
|
|
error_text = element.text_content()
|
|
if error_text and len(error_text.strip()) > 0:
|
|
logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}")
|
|
return error_text.strip()
|
|
|
|
# 2. Alle Texte auf der Seite durchsuchen
|
|
page_content = self.automation.browser.page.content()
|
|
|
|
for error_text in error_texts:
|
|
if error_text.lower() in page_content.lower():
|
|
# Versuche, den genauen Fehlertext zu extrahieren
|
|
matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower())
|
|
if matches:
|
|
full_error = matches[0].strip()
|
|
logger.info(f"Fehlermeldung gefunden (Text): {full_error}")
|
|
return full_error
|
|
else:
|
|
logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}")
|
|
return error_text
|
|
|
|
# 3. Nach weiteren Fehlerelementen suchen
|
|
elements = self.automation.browser.page.query_selector_all("p, div, span")
|
|
|
|
for element in elements:
|
|
element_text = element.inner_text()
|
|
if not element_text:
|
|
continue
|
|
|
|
element_text = element_text.strip()
|
|
|
|
# Prüfe Textähnlichkeit mit Fehlertexten
|
|
for error_text in error_texts:
|
|
if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \
|
|
self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7):
|
|
logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}")
|
|
return element_text
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}")
|
|
return None
|
|
|
|
def check_for_captcha(self) -> bool:
|
|
"""
|
|
Überprüft, ob ein Captcha angezeigt wird.
|
|
|
|
Returns:
|
|
bool: True wenn Captcha erkannt, False sonst
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Selektoren für Captcha-Erkennung
|
|
captcha_selectors = [
|
|
"div[data-testid='captcha']",
|
|
"iframe[src*='captcha']",
|
|
"iframe[title*='captcha']",
|
|
"iframe[title*='reCAPTCHA']"
|
|
]
|
|
|
|
# Captcha-Texte für textbasierte Erkennung
|
|
captcha_texts = [
|
|
"captcha", "recaptcha", "sicherheitsüberprüfung", "security check",
|
|
"i'm not a robot", "ich bin kein roboter", "verify you're human",
|
|
"bestätige, dass du ein mensch bist"
|
|
]
|
|
|
|
# Nach Selektoren suchen
|
|
for selector in captcha_selectors:
|
|
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
|
logger.warning(f"Captcha erkannt (Selektor): {selector}")
|
|
return True
|
|
|
|
# Nach Texten suchen
|
|
page_content = self.automation.browser.page.content().lower()
|
|
|
|
for text in captcha_texts:
|
|
if text in page_content:
|
|
logger.warning(f"Captcha erkannt (Text): {text}")
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Captcha-Erkennung: {e}")
|
|
return False
|
|
|
|
def wait_for_element(self, selectors: Union[str, List[str]],
|
|
timeout: int = 10000, check_interval: int = 500) -> Optional[Any]:
|
|
"""
|
|
Wartet auf das Erscheinen eines Elements.
|
|
|
|
Args:
|
|
selectors: CSS-Selektor oder Liste von Selektoren
|
|
timeout: Zeitlimit in Millisekunden
|
|
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
|
|
|
Returns:
|
|
Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist
|
|
"""
|
|
if not self._ensure_browser():
|
|
return None
|
|
|
|
try:
|
|
# Normalisiere selectors zu einer Liste
|
|
if isinstance(selectors, str):
|
|
selectors = [selectors]
|
|
|
|
start_time = time.time()
|
|
end_time = start_time + (timeout / 1000)
|
|
|
|
while time.time() < end_time:
|
|
for selector in selectors:
|
|
element = self.automation.browser.wait_for_selector(selector, timeout=check_interval)
|
|
if element:
|
|
logger.info(f"Element mit Selektor '{selector}' gefunden")
|
|
return element
|
|
|
|
# Kurze Pause vor der nächsten Prüfung
|
|
time.sleep(check_interval / 1000)
|
|
|
|
logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Warten auf Element: {e}")
|
|
return None
|
|
|
|
def is_registration_successful(self) -> bool:
|
|
"""
|
|
Überprüft, ob die Registrierung erfolgreich war.
|
|
|
|
Returns:
|
|
bool: True wenn erfolgreich, False sonst
|
|
"""
|
|
try:
|
|
# Erfolgsindikatoren überprüfen
|
|
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
|
|
|
for selector in success_indicators:
|
|
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
|
logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})")
|
|
return True
|
|
|
|
# URL überprüfen
|
|
current_url = self.automation.browser.page.url
|
|
if "/foryou" in current_url or "tiktok.com/explore" in current_url:
|
|
logger.info("Registrierung erfolgreich (Erfolgreiche Navigation erkannt)")
|
|
return True
|
|
|
|
# Überprüfen, ob Fehler angezeigt werden
|
|
error_message = self.check_for_error()
|
|
if error_message:
|
|
logger.warning(f"Registrierung nicht erfolgreich: {error_message}")
|
|
return False
|
|
|
|
logger.warning("Konnte Registrierungserfolg nicht bestätigen")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Überprüfung des Registrierungserfolgs: {e}")
|
|
return False |