Initial commit
Dieser Commit ist enthalten in:
520
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
520
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
@ -0,0 +1,520 @@
|
||||
"""
|
||||
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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren