Projekty · Dodano: 19.01.2023

Fortune

SQLite Matplotlib Selenium Python
Fortune

Fortune to wielowątkowy system desktopowy służący do automatycznego monitorowania, pobierania (scrapowania) oraz wizualizowania kursów bukmacherskich i statystyk meczowych w czasie rzeczywistym. Program śledzi dynamicznie zmieniające się rynki (m.in. na Superbet i Flashscore), zapisuje historię zmian w bazach danych i rysuje wykresy, które pozwalają wyłapać anomalie kursowe i idealne momenty na wejście z zakładem. Wszystko zapakowane w klasyczny interfejs graficzny PyQt5.


🛠️ Stos Technologiczny (Tech Stack)

Zbudowane z komponentów, które miały być niezawodne, proste w konfiguracji i nie wymagać opłacania drogich API:

  • Język i GUI: **Python 3.10+****PyQt5** (interfejs graficzny i zarządzanie wątkami w tle)
  • Web Scraping & Automatyzacja: **Selenium****Chrome WebDriver****webdriver-manager** (żeby nie użerać się z ciągłym ręcznym aktualizowaniem sterowników)
  • Baza Danych: **SQLite** (lekka, lokalna baza w plikach .db – osobny plik dla każdej ligi piłkarskiej)
  • Wykresy i Analiza: **Matplotlib** (osadzony bezpośrednio w oknach PyQt5 do rysowania przebiegu kursów w locie)
  • Zarządzanie Stanem: **QSettings** (automatyczne zapisywanie ciasteczek sesyjnych i rozmiarów okien)
  • Logowanie zdarzeń: Customowy logger zapisujący historię działania botów do plików .log

📈 Po co to w ogóle powstało? (Zastosowanie i korzyści dla twórcy)

  • Oszczędność na drogich API: Zamiast płacić setki dolarów miesięcznie za dostęp do statystyk i kursów live u dostawców danych, aplikacja wyciąga je bezpośrednio z darmowych wersji webowych bukmacherów.
  • Analiza spadków i wzrostów kursów: Dzięki wykresom Matplotlib można zobaczyć, jak dany kurs (np. na liczbę rzutów rożnych czy bramki) reaguje na upływający czas meczu i wydarzenia boiskowe.
  • Brak zamarzania ekranu (Responsive GUI): Pełna wielowątkowość (QThreads) sprawia, że w tle może działać kilka przeglądarek Chrome scrapujących dane, a użytkownik może w tym samym czasie płynnie klikać po ligach i przeglądać historyczne wykresy.
  • Szybkie wykrywanie trendów: Konwerter czasu gry bez problemu radzi sobie z formatami typu "HT", "45'+2" czy "ZAK", poprawnie nanosząc statystyki na oś czasu (1-90 minut meczu).

💡 Architektura i Wyzwania Inżynieryjne (Czyli jak to działa pod maską)

Projekt uczy pokory wobec dynamicznego kodu stron bukmacherskich i pokazuje, jak radzić sobie z typowymi problemami aplikacji desktopowych.

1. Wielowątkowość (Multithreading) – żeby interfejs nie dostał zawału

  • Wyzwanie: Selenium z natury blokuje wątek, na którym działa. Otwieranie przeglądarki Chrome, czekanie na załadowanie elementów i klikanie banerów w głównym wątku aplikacji PyQt5 doprowadziłoby do natychmiastowego zawieszenia całego okna programu.
  • Rozwiązanie: Cała logika pobierania danych została oddelegowana do osobnych klas dziedziczących po QThread. Mamy tu wyspecjalizowanych robotników:
  • MatchesChecker – sprawdza, jakie mecze są aktualnie grane live lub zaczną się za chwilę.
  • UrlUpdater – kontroluje cykliczne odświeżanie listy linków do meczów.
  • Runer – zarządza kolejką aktywnych scraperów i dba, by nie przekroczyć maksymalnego limitu otwartych okien Chrome na raz.
  • MatchUpdater – pojedynczy wątek dedykowany do obsługi konkretnego meczu.

2. Ciasteczka i Banery – walka z zabezpieczeniami i wyskakującymi okienkami

  • Wyzwanie: Bukmacherzy wyświetlają mnóstwo banerów o ciasteczkach, promocjach i zgodach marketingowych. Jeśli bot nie zamknie takiego okienka, nie doklika się do kursów. Co więcej, częste otwieranie przeglądarki bez ciasteczek wygląda podejrzanie i może skutkować banem IP.
  • Rozwiązanie: Wdrożono automatyczne omijanie banerów przy użyciu skryptów JavaScript wstrzykiwanych przez Selenium (execute_script). Ciasteczka po pomyślnym zaakceptowaniu są zapisywane w systemowym rejestrze/pliku konfiguracyjnym za pomocą QSettings i automatycznie wczytywane przy kolejnych uruchomieniach przeglądarki. Dzięki temu bot „pamięta” sesję.

3. Architektura baz danych per liga piłkarska

  • Wyzwanie: Zapisywanie tysięcy kursów na minutę do jednej wielkiej bazy danych SQLite mogłoby prowadzić do jej zablokowania (database is locked), gdy wiele wątków próbuje zapisać dane w tym samym momencie.
  • Rozwiązanie: Zamiast jednej bazy, system automatycznie rozbija dane i tworzy osobne pliki .db dla poszczególnych lig (np. Premier_League.db, Ekstraklasa.db). Wewnątrz pliku każda tabela odpowiada konkretnemu rynkowi zakładów. Znacznie ułatwia to analizę i redukuje konflikty zapisu.

4. Prewencja duplikatów w bazie danych

  • Wyzwanie: Bot pobiera dane w pętli. Nie chcemy zapisywać dokładnie tego samego kursu dla tej samej minuty meczu kilkanaście razy, bo popsuje to strukturę bazy i zniekształci wykresy.
  • Rozwiązanie: Przed wykonaniem zapisu SQL (INSERT), system sprawdza za pomocą zapytania SELECT, czy istnieje już w bazie rekord o identycznych parametrach (data, zespoły, minuta gry, nazwa zakładu i jego kurs). Jeśli tak, rekord jest pomijany.

💻 Przykłady Kodu (Code Snippets)

1. Sesje i ciasteczka pod kontrolą oraz wstrzykiwanie JS do ukrywania banerów Fragment kodu klasy bazowej przeglądarki (`apps/modules/browser.py`) pokazujący zarządzanie cookies za pomocą `QSettings` i dynamiczne ukrywanie natrętnych elementów DOM za pomocą wstrzykiwania kodu JavaScript:
# apps/modules/browser.py
from selenium import webdriver
from PyQt5.QtCore import QSettings

class Base:
    def __init__(self, name, **kwargs):
        self.settings = QSettings(name)
        self.name = name
        self.browser = None
        # ... konfiguracja ...

    def check_cookies(self):
        """Wczytuje wcześniej zapisane ciasteczka z QSettings do przeglądarki."""
        try:
            local_cookies = self.settings.value("cookies")
            browser_cookies = self.browser.get_cookies()
            if local_cookies and type(local_cookies) is list:
                for cookie in local_cookies:
                    if cookie not in browser_cookies:
                        self.browser.add_cookie(cookie)
        except Exception as e:
            self.logs.add_log("w", "Błąd podczas dodawania cookies.")

    def update_local_cookies(self):
        """Zapisuje aktualne ciasteczka sesji, żeby bot przy kolejnym starcie wyglądał znajomo."""
        cookies = self.browser.get_cookies()
        if cookies:
            self.settings.setValue("cookies", cookies)

    @staticmethod
    def prepare_style_script_to_hide(by, web_attr):
        """Przygotowuje skrypt JS ukrywający elementy blokujące widok (np. banery)."""
        base = "document."
        if by.upper() == "CLASS":
            base += "getElementsByClassName"
            base += f"('{web_attr}')[0].style.display='none';"
        elif by.upper() == "ID":
            base += "getElementById"
            base += f"('{web_attr}').style.display='none';"
        else:
            return False
        return base
2. Wielowątkowy menedżer kolejki zadań i scraperów (Concurrency Runner) Fragment kodu z klasy `Runer` i `MatchUpdater` (`apps/superbet/main.py`) pokazujący, jak w tle działa pętla rozdzielająca mecze do wolnych scraperów na podstawie limitu konfiguracji, dbając o płynność interfejsu graficznego:
# apps/superbet/main.py
import time
from PyQt5.QtCore import QThread

class Runer(QThread):
    def __init__(self, cache):
        super().__init__()
        self.cache = cache
        self.browser_config = tools.load_json("app_config/browser.json")
        self.superbet_config = tools.load_json("app_config/superbet.json")
        self.updater = {}

    def run(self) -> None:
        """Pętla nadzorująca wątki pobierające dane o kursach live."""
        while True:
            if self.cache['matches']['all']:
                # Sprawdzenie limitu jednocześnie otwartych przeglądarek
                if len(self.cache['matches']['active']) <= self.browser_config['count']['superbet']:
                    match = self.cache['matches']['all'].pop()
                    if match.get('counter') is None:
                        match.update({'counter': self.superbet_config['counter']})

                    cur_counter = match.get('counter')
                    if cur_counter != 0:
                        self.cache['matches']['active'].append(match)
                        # Tworzenie i uruchomienie dedykowanego wątku dla pojedynczego meczu
                        self.updater.update({match['url']: MatchUpdater(self.cache, match)})
                        self.updater[match['url']].start()
            time.sleep(0.1)