From ca961320e76ba13c3674065743d15f463e4c2241 Mon Sep 17 00:00:00 2001 From: sherl Date: Sat, 31 May 2025 05:45:27 +0200 Subject: [PATCH] feat: scraper usage example, fixed session handling --- FlaskWebProject/FlaskWebProject/fs_scraper.py | 118 +++++++++++++++++- FlaskWebProject/FlaskWebProject/lewy.py | 6 +- .../FlaskWebProject/lewy_api_v1.py | 1 + FlaskWebProject/FlaskWebProject/lewy_db.py | 45 ++++--- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/FlaskWebProject/FlaskWebProject/fs_scraper.py b/FlaskWebProject/FlaskWebProject/fs_scraper.py index c37216d..1e93c82 100644 --- a/FlaskWebProject/FlaskWebProject/fs_scraper.py +++ b/FlaskWebProject/FlaskWebProject/fs_scraper.py @@ -1,7 +1,20 @@ from lewy_db import baza as ldb +from lewy_globals import colors as c import json import lewy_globals import requests +import time + +def safe_traverse(obj: dict, path: list, default=None): + result = obj + try: + for x in path: + result = result[x] + except KeyError: + result = default + # print(f"error reading: {' -> '.join(path)} - returning: {default}") + finally: + return result class scraper: @@ -12,7 +25,7 @@ class scraper: db = None def __init__(self): - db = lewy_globals.getDb() + self.db = lewy_globals.getDb() pass def pobierzDaneNajlepszegoSportowcaNaSwiecie(self) -> dict: @@ -25,10 +38,105 @@ class scraper: response = requests.get(f'https://3.flashscore.ninja/3/x/feed/plm_{zewnetrzne_id_sportowca}_{nr_strony}', headers=self.headers) return json.loads(response.text) - def czy_mecz_istnieje(self, zewnetrzne_id_meczu: str): - mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu).first - if mecz is not None: - return mecz + def __czy_x_istnieje(self, typ, **id): + rekord = self.db.simple_select_all(typ, **id) + if rekord is not None and rekord != []: + return True else: return False + def czy_mecz_istnieje(self, zewnetrzne_id_meczu: str): + # mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu) + # if mecz is not None and mecz != []: + # return True + # else: + # return False + return self.__czy_x_istnieje("mecze", zewnetrzne_id_meczu=zewnetrzne_id_meczu) + + def czy_klub_istnieje(self, id_klubu: str): + # mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu) + # if mecz is not None and mecz != []: + # return True + # else: + # return False + return self.__czy_x_istnieje("kluby", id_klubu=id_klubu) + + def aktualizuj_dane_sportowca(self, zewnetrzne_id_sportowca: str = "MVC8zHZD"): + stop_scraping = False + matches_to_add = [] + + # TODO: Sprawdź, czy sportowiec istnieje w bazie. + # Jeśli nie, dodaj go w podobny sposób, jak + # w sample_data_init() (w lewy_db.py). + + page = 0 + match_num = 0 + while not stop_scraping: + + retrieved_page = self.pobierz_pojedyncza_strone(zewnetrzne_id_sportowca=zewnetrzne_id_sportowca, nr_strony=page) + if not safe_traverse(retrieved_page, ["hasMoreLastMatches"], default=False): + stop_scraping = True + break + + print(f"{c.WARNING}Pobrano nową stronę {page}{c.ENDC}:\n{retrieved_page}") + + retrieved_matches = safe_traverse(retrieved_page, ["lastMatches"], default=[]) + for match in retrieved_matches: + + match_id = safe_traverse(match, ["eventEncodedId"], default="non-existent-match-id") + home_club_id = safe_traverse(match, ["homeParticipantUrl"], default="non-existent-club-id") + away_club_id = safe_traverse(match, ["awayParticipantUrl"], default="non-existent-club-id") + + # Sprawdź, czy mecz nie znajduje się już w bazie + if self.czy_mecz_istnieje(zewnetrzne_id_meczu=match_id): + stop_scraping = True + break + + # Sprawdź, czy klub znajduje się już w bazie. Jeśli nie, + # trzeba go dodać przed meczem. + if not self.czy_klub_istnieje(id_klubu=home_club_id): + print(f"{c.OKCYAN}Nowy klub{c.ENDC}: {home_club_id}") + self.db.simple_insert_one("kluby", + id_klubu=home_club_id, + pelna_nazwa=safe_traverse(match, ["homeParticipantName"]), + skrocona_nazwa=safe_traverse(match, ["homeParticipant3CharName"])) + if not self.czy_klub_istnieje(id_klubu=away_club_id): + print(f"{c.OKCYAN}Nowy klub{c.ENDC}: {away_club_id}") + self.db.simple_insert_one("kluby", + id_klubu=away_club_id, + pelna_nazwa=safe_traverse(match, ["awayParticipantName"]), + skrocona_nazwa=safe_traverse(match, ["awayParticipant3CharName"])) + + # TODO: (opcjonalnie) zamień *słownik match* na *obiekt mecz* + + + + # TODO: dodaj obiekt mecz do bazy (simple_insert_one(), simple_insert_many()) + print(f"{c.OKCYAN}Nowy mecz ({match_num}){c.ENDC}: {match}") + match_num += 1 + + + # TODO: Zaktualizuj statystyki sportowca + + + # Opcjonalnie: odczekaj kilka sekund (?) + # Problem w tym, że time.sleep() jest blokujące, + # a asyncio i flask nie idą ze sobą w parze. + # Można to załatwić osobnym skryptem, ale + # martwmy się tym dopiero, gdy dostaniemy + # rate limita. - sherl + + page += 1 + time.sleep(15) + + + def aktualizuj_dane(self): + """ + Pobiera mecze dla każdego sportowca wymienionego + w pliku konfiguracyjnym. + """ + + for id_sportowca in lewy_globals.config['sportsmen']['tracked_ids']: + self.aktualizuj_dane_sportowca(zewnetrzne_id_sportowca=id_sportowca) + time.sleep(15) + diff --git a/FlaskWebProject/FlaskWebProject/lewy.py b/FlaskWebProject/FlaskWebProject/lewy.py index 6f569e2..391eeb7 100644 --- a/FlaskWebProject/FlaskWebProject/lewy.py +++ b/FlaskWebProject/FlaskWebProject/lewy.py @@ -1,7 +1,7 @@ from argparse import ArgumentParser from flask import Flask, Response, render_template from flask_apscheduler import APScheduler -from fs_scraper import scraper +from fs_scraper import scraper as scr from lewy_globals import colors as c import lewy_api import lewy_db @@ -73,7 +73,7 @@ def setup(): app.add_url_rule('/api/', view_func=lewy_api.api_global_catchall) db = lewy_globals.setupDb(app, config) - scrape = scraper() + scraper = scr() with app.app_context(): db.create_all() @@ -92,7 +92,7 @@ def every5seconds(): def every2hours(): # zaktualizuj bazę danych scrapując FS # ... - # print(scrape.pobierz_pojedyncza_strone()) + # scraper.aktualizuj_dane() return @app.route('/', methods=['GET']) diff --git a/FlaskWebProject/FlaskWebProject/lewy_api_v1.py b/FlaskWebProject/FlaskWebProject/lewy_api_v1.py index e428442..efa0d63 100644 --- a/FlaskWebProject/FlaskWebProject/lewy_api_v1.py +++ b/FlaskWebProject/FlaskWebProject/lewy_api_v1.py @@ -4,6 +4,7 @@ # - json with appropriate data from datetime import datetime from flask_sqlalchemy import SQLAlchemy +from fs_scraper import scraper from functools import wraps from lewy_globals import getDb, colors as c import flask, json, time diff --git a/FlaskWebProject/FlaskWebProject/lewy_db.py b/FlaskWebProject/FlaskWebProject/lewy_db.py index 5c7a99f..df6e0d9 100644 --- a/FlaskWebProject/FlaskWebProject/lewy_db.py +++ b/FlaskWebProject/FlaskWebProject/lewy_db.py @@ -4,6 +4,7 @@ from functools import wraps from sqlalchemy import ForeignKey, select, insert, update from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, Session, relationship from typing import List +import time import toml global db @@ -39,18 +40,18 @@ class baza(): nazwisko: Mapped[ str] = mapped_column() data_urodzenia: Mapped[ str] = mapped_column() czy_aktywny: Mapped[ bool] = mapped_column() - klub_id: Mapped[ List[str]] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu")) + klub_id: Mapped[ List[str]] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu"), nullable=True) klub: Mapped[ List["kluby"]] = relationship(back_populates="sportowcy_w_klubie", foreign_keys=[klub_id]) narodowosc: Mapped[ str] = mapped_column() ilosc_trofeow: Mapped[ int] = mapped_column() ostatnie_trofeum_id: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}trofea.id_trofeum"), nullable=True) ostatnie_trofeum: Mapped[ "trofea"] = relationship(back_populates="zawodnik", foreign_keys=[ostatnie_trofeum_id]) - pierwszy_mecz_id: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}mecze.id_meczu")) + pierwszy_mecz_id: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}mecze.id_meczu"), nullable=True) pierwszy_mecz: Mapped[ "mecze"] = relationship() wycena: Mapped[ int] = mapped_column() - ostatni_gol_dla_id: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu")) + ostatni_gol_dla_id: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu"), nullable=True) ostatni_gol_dla: Mapped[ "kluby"] = relationship(back_populates="sportowcy_ostatni_gol", foreign_keys=[ostatni_gol_dla_id]) - statystyki_id: Mapped[ List[int]] = mapped_column(ForeignKey(f"{tnp}statystyki_sportowcow.id_statystyki")) + statystyki_id: Mapped[ List[int]] = mapped_column(ForeignKey(f"{tnp}statystyki_sportowcow.id_statystyki"), nullable=True) statystyki: Mapped[List["statystyki_sportowcow"]] = relationship(back_populates="sportowiec") trofea: Mapped[ List["trofea"]] = relationship(back_populates="zawodnik", foreign_keys="[trofea.id_zawodnika]") @@ -168,7 +169,7 @@ class baza(): except: self.session.rollback() self.session.close() - self.refresh_session() + self.refresh_session() return return_val return wrapper @@ -186,6 +187,8 @@ class baza(): if not isinstance(entity_type, str): entity_type = entity_type.__name__ + print(f"[{round(time.time())}] SELECT") + results = ( self.session. query(self.entities[entity_type]). @@ -193,10 +196,12 @@ class baza(): all() ) + print(f"[{round(time.time())}] SELECT RESULTS: {results}") + return results @exit_gracefully - def simple_insert_one(self, type, **kwargs): + def simple_insert_one(self, entity_type, **kwargs): """ Użycie: simple_insert_one(ldb.kluby, id_klubu="polska", pelna_nazwa="Reprezentacja Polski", skrocona_nazwa="PL") @@ -204,12 +209,18 @@ class baza(): https://docs.sqlalchemy.org/en/20/tutorial/data_insert.html https://docs.sqlalchemy.org/en/20/orm/session_basics.html """ - obj = type(**kwargs) - with Session(self.db.engine) as session: - session.add(obj) - session.commit() - return 0 - return 1 + + if not isinstance(entity_type, str): + entity_type = entity_type.__name__ + + print(f"[{round(time.time())}] INSERT") + + obj = self.entities[entity_type](**kwargs) + #with Session(self.db.engine) as session: + self.session.add(obj) + self.session.commit() + return 0 + #return 1 @exit_gracefully def simple_insert_many(self, objs_list): @@ -220,11 +231,11 @@ class baza(): https://docs.sqlalchemy.org/en/20/tutorial/data_insert.html https://docs.sqlalchemy.org/en/20/orm/session_basics.html """ - with Session(self.db.engine) as session: - session.add_all(objs_list) - session.commit() - return 0 - return 1 + #with Session(self.db.engine) as session: + self.session.add_all(objs_list) + self.session.commit() + return 0 + #return 1 @exit_gracefully def sample_data_init(self, override_safety_check=False):