Files
WFL/parsers/parser_schedule.py
2026-05-25 11:42:51 +03:00

223 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import requests
from bs4 import BeautifulSoup
from datetime import datetime
from zoneinfo import ZoneInfo
from services.schedule_service import sync_matches
from repositories.team_repository import get_team_external_id_by_name, get_team_id_by_external_id
MONTHS_RU = {
"янв.": 1,
"февр.": 2,
"мар.": 3,
"апр.": 4,
"мая": 5,
"июн.": 6,
"июл.": 7,
"авг.": 8,
"сент.": 9,
"окт.": 10,
"нояб.": 11,
"дек.": 12,
}
TZ = ZoneInfo("Europe/Moscow")
URL_SCHEDULE = (
"https://wfl.rfs.ru/tournament/1061879/calendar?round_id=1117550&type=tours"
)
SEASON = "2025/2026"
def parse_russian_date(date_str: str, year: int | None = None) -> datetime:
# пример: "07 мар., пт, 13:00"
if year is None:
year = datetime.now(TZ).year
parts = [p.strip() for p in date_str.split(",")]
if len(parts) < 3:
raise ValueError(f"Некорректный формат даты: {date_str}")
day_month = parts[0]
time_part = parts[2]
day, month_str = day_month.split()
month = MONTHS_RU[month_str]
hh, mm = time_part.split(":")
return datetime(year, month, int(day), int(hh), int(mm), tzinfo=TZ)
class SiteUnavailableError(Exception):
pass
def fetch_html(url: str) -> str:
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "*/*"
}
try:
response = requests.get(url, headers=headers, timeout=20)
response.raise_for_status()
return response.text
except requests.exceptions.Timeout:
raise SiteUnavailableError("Сайт слишком долго отвечает")
except requests.exceptions.ConnectionError:
raise SiteUnavailableError("Не удалось подключиться к сайту")
except requests.exceptions.HTTPError as e:
raise SiteUnavailableError(f"Ошибка HTTP: {e.response.status_code}")
except requests.exceptions.TooManyRedirects:
raise SiteUnavailableError("Слишком много редиректов")
except requests.exceptions.InvalidURL:
raise SiteUnavailableError("Неверный URL")
except requests.exceptions.MissingSchema:
raise SiteUnavailableError("Отсутствует схема URL, например https://")
except requests.exceptions.SSLError:
raise SiteUnavailableError("Ошибка SSL сертификата")
except requests.exceptions.RequestException as e:
raise SiteUnavailableError(f"Неизвестная ошибка запроса: {e}")
def normalize_status(score1: str | None, score2: str | None, dt: datetime) -> str:
now = datetime.now(TZ)
if score1 is not None and score2 is not None:
return "finished"
if dt <= now:
return "live"
return "scheduled"
def safe_int(value: str | None) -> int | None:
if value is None:
return None
value = value.strip()
return int(value) if value.isdigit() else None
def parse_schedule(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
matches_data: list[dict] = []
table_div = soup.find("div", class_="timetable__main")
if not table_div:
return matches_data
rows = table_div.find_all("div", class_="timetable__unit js-schedule-games-cont")
for row in rows:
tour_el = row.find("span", class_="timetable__head-text")
tour = tour_el.get_text(strip=True) if tour_el else None
matches = row.find_all("li", class_="timetable__item")
for match in matches:
score_link = match.find("a", class_="timetable__score")
href = score_link.get("href") if score_link else ""
match_id = href.split("/")[-1] if href else None
time_el = match.find("span", class_="timetable__time")
time_site = time_el.get_text(strip=True) if time_el else None
if not time_site or not match_id:
continue
try:
dt = parse_russian_date(time_site)
except Exception as exc:
print(f"[parser_schedule] Ошибка даты для матча {match_id}: {exc}")
continue
teams = match.find_all("div", class_="timetable__team-name")
team1_name = teams[0].get_text(strip=True) if len(teams) > 0 else None
team2_name = teams[1].get_text(strip=True) if len(teams) > 1 else None
if not team1_name or not team2_name:
print(f"[parser_schedule] Пропуск матча {match_id}: нет названий команд")
continue
home_team_external_id = get_team_external_id_by_name(team1_name)
away_team_external_id = get_team_external_id_by_name(team2_name)
if not home_team_external_id or not away_team_external_id:
print(
f"[parser_schedule] Пропуск матча {match_id}: "
f"не найдены команды в БД ({team1_name} vs {team2_name})"
)
continue
score1 = score2 = None
score_main_el = match.find("div", class_="timetable__score-main")
if score_main_el:
score_main = score_main_el.get_text(strip=True)
if "-" in score_main and "- : -" not in score_main:
parts = [s.strip() for s in score_main.split("-")]
if len(parts) == 2:
score1, score2 = parts[0], parts[1]
home_score = safe_int(score1)
away_score = safe_int(score2)
status = normalize_status(home_score, away_score, dt)
place_el = match.find("span", class_="timetable__place-name")
place = place_el.get_text(strip=True) if place_el else None
score_add_el = match.find("div", class_="timetable__score-additional")
score_add = score_add_el.get_text(strip=True) if score_add_el else None
matches_data.append(
{
"external_id": str(match_id),
"home_team_external_id": str(home_team_external_id),
"away_team_external_id": str(away_team_external_id),
"match_date": dt.strftime("%Y-%m-%d %H:%M:%S"),
"status": status,
"home_score": home_score,
"away_score": away_score,
"tour": tour,
"season": SEASON,
"place": place,
"date_raw": time_site,
"score_add": score_add,
}
)
matches_data.sort(key=lambda x: x["match_date"] or "")
return matches_data
def run_parser_schedule() -> dict:
try:
html = fetch_html(URL_SCHEDULE)
matches_data = parse_schedule(html)
sync_matches(matches_data)
return {
"success": True,
"message": f"Матчи синхронизированы: {len(matches_data)}",
"count": len(matches_data),
}
except SiteUnavailableError as e:
return {
"success": False,
"message": f"Сайт недоступен: {e}",
}
except Exception as e:
return {
"success": False,
"message": f"Ошибка парсера: {e}",
}
if __name__ == "__main__":
run_parser_schedule()