Fortune
SQLite
Matplotlib
Selenium
Python
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ąQSettingsi 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
.dbdla 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ą zapytaniaSELECT, 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)