120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
import requests
|
||
from bs4 import BeautifulSoup
|
||
from typing import List, Dict
|
||
|
||
BASE_URL = 'https://swrailway.gov.ua/timetable/eltrain/?rid=2'
|
||
LOCAL_HTML = 'rozklad.html'
|
||
|
||
# Індекс днів тижня: понеділок=0, ..., неділя=6
|
||
DAY_INDEX = {
|
||
'пн': 0, 'вт': 1, 'ср': 2,
|
||
'чт': 3, 'пт': 4, 'сб': 5,
|
||
'нд': 6
|
||
}
|
||
|
||
def parse_days(text: str) -> str:
|
||
"""Перетворює текст днів курсування на бінарний рядок довжиною 7."""
|
||
text = text.lower().strip()
|
||
if 'щоденно' in text:
|
||
return '1111111'
|
||
if text.startswith('крім'):
|
||
days = [d.strip(' .') for d in text.split('крім', 1)[1].split(',')]
|
||
mask = [1] * 7
|
||
for d in days:
|
||
idx = DAY_INDEX.get(d)
|
||
if idx is not None:
|
||
mask[idx] = 0
|
||
return ''.join(str(b) for b in mask)
|
||
if text.startswith('по'):
|
||
days = [d.strip(' .') for d in text.split('по', 1)[1].split(',')]
|
||
mask = [0] * 7
|
||
for d in days:
|
||
idx = DAY_INDEX.get(d)
|
||
if idx is not None:
|
||
mask[idx] = 1
|
||
return ''.join(str(b) for b in mask)
|
||
return '1111111'
|
||
|
||
|
||
def fetch_schedule(tab: int = 1, use_local: bool = False) -> List[Dict]:
|
||
"""
|
||
Парсить вкладку розкладу:
|
||
tab=1 — Київ→Ніжин,
|
||
tab=2 — Ніжин→Київ.
|
||
Повертає список поїздів з полями:
|
||
'tid', 'train_number', 'days', 'route', 'times'.
|
||
"""
|
||
# Завантаження HTML
|
||
if use_local:
|
||
with open(LOCAL_HTML, 'r', encoding='utf-8') as f:
|
||
html = f.read()
|
||
else:
|
||
resp = requests.get(BASE_URL, timeout=10)
|
||
resp.raise_for_status()
|
||
html = resp.text
|
||
|
||
soup = BeautifulSoup(html, 'html.parser')
|
||
prefix = f'div#tabs-trains{tab}'
|
||
|
||
# Таблиця розкладу
|
||
times_table = soup.select_one(f'{prefix} table.td_center')
|
||
if not times_table:
|
||
raise RuntimeError(f'Не знайдено таблицю розкладу для tab={tab}')
|
||
|
||
# Список станцій (35), очищаємо префікс 'з.п. '
|
||
station_tags = soup.select(
|
||
f'{prefix} table.left tr.on a.et, '
|
||
f'{prefix} table.left tr.onx a.et'
|
||
)
|
||
stations = []
|
||
for a in station_tags:
|
||
raw = a.get_text(strip=True)
|
||
# Видаляємо 'з.п. ' на початку
|
||
clean = raw
|
||
if clean.startswith('з.п. '):
|
||
clean = clean[len('з.п. '):]
|
||
stations.append(clean)
|
||
|
||
# Рядки таблиці
|
||
trs = times_table.find_all('tr')
|
||
header_row = next(r for r in trs if r.find('td', class_='on_right_t'))
|
||
train_cells = header_row.find_all('td', class_='on_right_t')
|
||
|
||
# Маршрути для кожного поїзда
|
||
route_tags = times_table.select('td.course')
|
||
routes = [tag.get_text(strip=True) for tag in route_tags[:len(train_cells)]]
|
||
|
||
entries: List[Dict] = []
|
||
for idx, cell in enumerate(train_cells):
|
||
a_tag = cell.find('a', class_='et')
|
||
href = a_tag['href']
|
||
tid = href.split('tid=')[-1]
|
||
|
||
parts = cell.get_text(separator='|', strip=True).split('|')
|
||
num = parts[0].rstrip(',').strip()
|
||
days = parse_days(parts[1] if len(parts) > 1 else 'щоденно')
|
||
route = routes[idx] if idx < len(routes) else ''
|
||
entries.append({
|
||
'tid': tid,
|
||
'train_number': num,
|
||
'days': days,
|
||
'route': route,
|
||
'times': []
|
||
})
|
||
|
||
# Рядки з часами руху (повинно бути 35)
|
||
time_rows = [r for r in trs if r.find('td', class_='q0') or r.find('td', class_='q1')]
|
||
for idx, entry in enumerate(entries):
|
||
base = idx * 3
|
||
for si, row in enumerate(time_rows):
|
||
cells = row.find_all('td')
|
||
arrival = cells[base+1].get_text(strip=True) if base+1 < len(cells) else ''
|
||
departure = cells[base+2].get_text(strip=True) if base+2 < len(cells) else ''
|
||
entry['times'].append({
|
||
'station': stations[si],
|
||
'arrival': arrival,
|
||
'departure': departure
|
||
})
|
||
|
||
return entries
|