Beispiel #1
0
def get_champ_calendar(
        current_champ_link: str, matchweek: int,
        teams: List[str]) -> Tuple[pd.DataFrame, int, List[str]]:
    calendar_link = current_champ_link + 'calendar/'
    text, soup = request_text_soup(calendar_link)

    months = soup.find('div', class_='months')
    links = dict(map(lambda x: (x.text, x['href']), months.find_all('a')))

    full_matches = []
    postponed_matches = []
    # TODO:можно оптимизировать - обрабатывать, начиная с текущего месяца, иначе пропускать
    # обрабатывать условные 3 месяца, не больше
    for month, link in links.items():
        text, soup = request_text_soup(link)
        # номера всех туров на странице
        week_numbers = list(
            map(lambda x: int(x.text.split(' ')[0]), soup.find_all('h3')))
        # матчи с каждого тура на странице
        weeks_with_matches = soup.find_all('table', class_="stat-table")
        matches = list(map(lambda x: x.find_all('tr')[1:], weeks_with_matches))
        # обработка каждого матча + последним элементом крепим номер тура
        for i, week in enumerate(matches):
            ms = list(map(lambda x: match_proc(x) + [week_numbers[i]], week))
            full_matches.extend([x for x in ms if len(x) == 5])
            postponed_matches.extend([
                '{} - {}, {}тур'.format(x[0], x[2], x[3]) for x in ms
                if len(x) != 5
            ])

    sorted_matches = sorted(
        full_matches,
        key=lambda x: datetime.datetime.strptime(x[0], '%d.%m.%Y'))
    # получение номера фентези-тура из номера тура (рассматриваем и учитываем спарки)
    true_matches = proc_matchweek(sorted_matches)
    d = {}
    current_match_num = 0
    for current_week in range(matchweek, matchweek + 5):
        if current_week == matchweek:
            current_match_num = len(
                [x for x in true_matches if x[-1] == current_week])
        d[current_week] = {key: '' for key in teams}
        current_week_matches = filter(lambda x: x[-1] == current_week,
                                      true_matches)
        # выгрузка количества матчей в ближайшем туре
        for match in current_week_matches:
            home_team = match[1]
            away_team = match[3]
            for team in [home_team, away_team]:
                if d[current_week][team]:
                    d[current_week][team] += ' + '
            d[current_week][home_team] += away_team + '(д)'
            d[current_week][away_team] += home_team + '(г)'
    return pd.DataFrame(d), current_match_num, postponed_matches
Beispiel #2
0
def table_processing(current_champ: str, champ_link: str) -> Dict[str, float]:
    table_link = champ_link + 'table/'
    # получаем страницу с таблицей обрабатываемого чемпионата
    _, table_soup = request_text_soup(table_link)
    # выделение таблицы со страницы
    table_body = table_soup.find('tbody')
    team_body_list = table_body.find_all('tr')

    #team_links = {}
    stats = {}
    # для каждой команды из таблицы сохраним ссылку на профиль команды, все численные атрибуты
    for team in team_body_list:
        team_name = team.find('a', class_='name').get('title')
        #team_links[team_name] = team.find('a', class_='name').get('href')
        # если тур больше данной константы - обрабатываем численную статистику из таблицы - в противном случае будет {}
        team_numbers = team.find_all('td', class_=None)
        for j, n in enumerate(team_numbers):
            # для каждой команды обрабатываем каждое число-статистику из таблицы
            team_numbers[j] = int(n.text)
        team_stats = dict(zip(SPORTS_TABLE_COLS, team_numbers))
        team_stats['avg_g_scored'] = team_stats['g_scored'] / team_stats[
            'games'] if team_stats['games'] else 0
        team_stats['avg_g_against'] = team_stats['g_against'] / team_stats[
            'games'] if team_stats['games'] else 0
        stats[team_name] = team_stats['avg_g_scored'] - team_stats[
            'avg_g_against']
    logging.info('{}: таблица чемпионата обработана'.format(current_champ))
    return stats
Beispiel #3
0
def table_processing(current_champ, champ_link, matchweek):
    table_link = champ_link + 'table/'
    # получаем страницу с таблицей обрабатываемого чемпионата
    _, table_soup = request_text_soup(table_link)
    # выделение таблицы со страницы
    table_body = table_soup.tbody
    team_body_list = table_body.find_all('tr')
    # колонки футбольных таблиц на sports.ru - численные атрибуты каждой команды
    table_columns = ['games', 'won', 'draw', 'lost', 'g_scored', 'g_against', 'points']
    team_links = {}
    # работаем с глобальным словарем tableStats
    global tableStats
    tableStats = {}
    # для каждой команды из таблицы сохраним ссылку на профиль команды, все численные атрибуты
    for team in team_body_list:
        team_name = team.find('a', class_='name').get('title')
        team_links[team_name] = team.find('a', class_='name').get('href')
        # если тур больше данной константы - обрабатываем численную статистику из таблицы - в противном случае будет {}
        if matchweek > MATCHES_ENOUGH_TO_USE_TABLE_STATS:
            team_numbers = team.find_all('td', class_=None)
            for j, n in enumerate(team_numbers):
                team_numbers[j] = int(n.text)
            tableStats[team_name] = dict(zip(table_columns, team_numbers))
            tableStats[team_name]['avg_g_scored'] = tableStats[team_name]['g_scored'] / tableStats[team_name][
                'games'] if tableStats[team_name]['games'] else 0
            tableStats[team_name]['avg_g_against'] = tableStats[team_name]['g_against'] / tableStats[team_name][
                'games'] if tableStats[team_name]['games'] else 0
    # транспонируем этот словарь, чтобы иметь доступ в другом порядке
    # (вместо team_name -> games -> 5 получаем games -> team_name -> 5)
    # для случая, когда мы не хотим обрабатывать таблицу из-за малого количества туров, получим {}
    tableStats = dict(pd.DataFrame(tableStats).transpose())
    logging.info('{}: таблица чемпионата обработана'.format(current_champ))
    return team_links
Beispiel #4
0
def pull_understat_json(link: str, table_name: str):
    main_text, _ = request_text_soup(link)
    players_data_dirty = re.findall(table_name + r"([^;]*);", main_text)[0]
    players_data_json = re.findall(r"JSON.parse\('([^']*)'\)", players_data_dirty)[0]
    json_encoded = html.unescape(
        players_data_json.encode('utf8').decode('unicode_escape'))
    res = json.loads(json_encoded)
    return res
Beispiel #5
0
def update_h2h():
    for current_champ, current_link in H2H_LINKS.items():
        if not current_link:
            continue
        link = current_link + SUFFIX_QUERY
        offset = 0

        # cycle to pull all data - in every update we are getting some numbers of players
        keys = []
        player_data = []
        while True:
            _, soup = request_text_soup(link.format(offset),
                                        func=lambda x: json.loads(x)['data'])
            # getting keys
            if offset == 0:
                for t in soup.find_all('tr')[1].find_all('th'):
                    keys.append(t.text.strip())

            all_players = soup.find_all('tr')[2:]
            # if no update available with new offset - we just skip this step and next go out from cycle
            if not all_players:
                break
            # if any updates available - parse every available cell
            for player in all_players:
                cells = player.find_all('td')
                p = []
                for i, t in enumerate(cells):
                    p.append(t.text.strip())
                # link hack - "переобработка для последнего" - берем только айди, либо текстовый айди
                p[-1] = t.find(
                    'a',
                    title="Профиль игрока").get('href').strip().split('/')[-2]
                player_data.append(p)

            offset = offset + THRESHOLD

        # создание датафрейма
        df = pd.DataFrame(dict(zip(keys, list(map(list, zip(*player_data))))))
        df = df.rename(columns={'': 'sports_id', 'А': 'Амплуа'})
        # чтобы убрать игроков, по которым нет полной информации - нет меты, нет ссылки на профиль
        df = df[df['$'] != '0']
        df['$'] = df['$'].apply(pd.to_numeric, errors='coerce')
        dfs = {current_champ: df[H2H_COLUMNS]}

        # todo: СДЕЛАТЬ НОРМАЛЬНОЕ СОХРАНЕНИЕ, ВЫНЕСТИ ПРОВЕРКУ ДИРЕКТОРИИ В КОММОН путь в конфиг, проверять папку на наличие. КСТАТИ ПАПКУ ЛОГОВ МЫ ТОЖЕ НЕ ПРОВЕРЯЕМ
        if not os.path.isdir(H2H_DIR):
            os.makedirs(H2H_DIR)
        path = H2H_DIR + current_champ + ".xlsx"
        save_dfs_to_xlsx(dfs, path)
Beispiel #6
0
def update_champ_meta(current_champ):
    link = CHAMP_LINKS[current_champ]['sportsFantasy']
    if link:
        # запрос страницы фентези команды на спортс ру
        sports_fantasy_text, sports_fantasy_soup = request_text_soup(link)
        # вычисление даты дедлайна - пока что время дедлайна не используется
        # на спортс дата дедлайна в виде "15 Апр 18:00"
        deadline = re.findall(r'Дедлайн</th>\n<td>([^<]*)[^\d]*(\d{2}:\d{2})',
                              sports_fantasy_text)[0]
        deadline_text = ' '.join(deadline)
        # конвертируем в datetime часть даты вида "15 Апр"
        deadline_date = rus_date_convert(deadline[0])
        match_week = re.findall(r'<td>тур ([\d]*)', sports_fantasy_text)[0]
        match_week = int(match_week)
        # вычисление количества матчей в туре с помощью страницы фентези команды на спортс ру
        match_table = sports_fantasy_soup.find('table',
                                               class_='stat-table with-places')
        # в некоторых случаях данной таблицы вообще не будет на странице, например, когда дата следующего тура неясна
        match_num = len(match_table.find_all('tr')) - 1 if match_table else 0
        if deadline_date < date.today():
            logging.info(
                '{}: Нет даты дедлайна, чемпионат пропускается...'.format(
                    current_champ))
        elif -1 < (deadline_date -
                   date.today()).days > daysBeforeDeadlineLimit:
            logging.info(
                '{}: До дедлайна больше {} дней, чемпионат пропускается...'.
                format(current_champ, daysBeforeDeadlineLimit))
        elif match_num == 0:
            logging.warning(
                '{}: На спортс.ру не указаны матчи на ближайший тур, несмотря на то, что дедлайн близко'
                .format(current_champ))
        else:
            CHAMP_LINKS[current_champ]['matchweek'] = match_week
            CHAMP_LINKS[current_champ]['deadline_text'] = deadline_text
            CHAMP_LINKS[current_champ]['deadline_date'] = deadline_date
            CHAMP_LINKS[current_champ]['match_num'] = match_num
            logging.info('{}: метаданные обработаны'.format(current_champ))
    return
Beispiel #7
0
def find_xbet_link(current_champ: str) -> Union[str, None]:
    logging.info(
        '{}: Выгрузка ссылки на линии победителей чемпионатов с 1xbet'.format(
            current_champ))
    link = XBET_CHAMP_LINKS[current_champ]['link']
    _, soup = request_text_soup(link)
    # TODO: переписать - сейчас не работает для Англии
    json_events = soup.find('script', type="application/ld+json")
    if json_events is None:
        logging.warning(
            '{}: нет событий по ссылке из конфиг-файла'.format(current_champ))
        return
    json_events_text = json_events.text
    list_line = json.loads(json_events_text)

    for link_meta in list_line:
        if link_meta['name'] != XBET_CHAMP_LINKS[current_champ]['name']:
            continue
        target_link = link_meta['url']
        logging.info(
            '{}: ссылка на линию на чемпиона получена'.format(current_champ))
        return target_link
    logging.warning('{}: нет подходящего события'.format(current_champ))
    return
Beispiel #8
0
def pull_champ_meta(current_champ: str) -> Union[list, None]:
    link = CHAMP_LINKS[current_champ]['sportsFantasy']
    if not link:
        logging.warning('{}: Отсутствует ссылка на метаданные')
        return
    # запрос страницы фентези команды на спортс ру
    _, sports_fantasy_soup = common.request_text_soup(link)
    # взятие элемента с дедлайном и номером тура
    target_elem = sports_fantasy_soup.find('div', class_='team-info-block')
    if target_elem is None:
        logging.error(
            '{}: Ошибка в формате страницы на спортс.ру'.format(current_champ))
        return

    td = target_elem.find_all('td')
    if len(td) < 2:
        logging.error(
            '{}: Ошибка в формате страницы на спортс.ру'.format(current_champ))
        return

    match_week = td[0].text.split()[1].strip('.')
    if not match_week.isdigit():
        logging.info(
            '{}: Текущий сезон чемпионата завершен, чемпионат пропускается...'.
            format(current_champ))
        return

    match_week = int(match_week)
    # на спортс дата дедлайна в виде "15 Апр 18:00" - пока что время дедлайна не используется
    deadline_text = td[1].text
    # конвертируем в datetime часть даты вида "15 Апр"
    deadline_date = common.rus_date_convert(deadline_text.split('|')[0])

    # найдет все элементы с CSS тэгом "stat-table", последний - таблица с играми ближайшего тура
    match_table = sports_fantasy_soup.find_all('table',
                                               class_='stat-table')[-1]
    # вычисление количества матчей в туре с помощью страницы фентези команды на спортс ру
    # в некоторых случаях данной таблицы вообще не будет на странице, например, когда дата следующего тура неясна
    if match_table:
        match_num = len(match_table.find_all('tr')) - 1
        max_date = match_table.find_all('tr')[-1].td.text
        day, month = map(int, max_date.split('|')[0].split('.'))
        year = date.today().year
        max_date = date(year, month, day)
    else:
        match_num = 0
        max_date = None

    if deadline_date < date.today():
        logging.info('{}: Нет даты дедлайна, чемпионат пропускается...'.format(
            current_champ))
        return
    elif (deadline_date - date.today()).days > DAYS_BEFORE_DEADLINE:
        logging.info(
            '{}: До дедлайна({}) больше {} дней, чемпионат пропускается...'.
            format(current_champ, deadline_date, DAYS_BEFORE_DEADLINE))
        return
    elif match_num == 0:
        logging.warning(
            '{}: На спортс.ру не указаны матчи на ближайший тур'.format(
                current_champ))
        return
    logging.info('{}: Метаданные обработаны'.format(current_champ))
    return [deadline_date, max_date, deadline_text, match_week, match_num]
Beispiel #9
0
def marathon_processing(current_champ: str, current_champ_links: Tuple[str],
                        deadline_date: date, max_date: date, match_num: int):
    # фиксирование времени по каждому чемпионату, логирование обработки каждого чемпионата
    champ_start_time = time.time()
    # запрос страницы с матчами по текущему чемпионату
    # марафон начал публиковать за 24 по дефолту, указываем параметр - за все время
    link = current_champ_links['marathon'] + '?interval=ALL_TIME'
    if not link:
        logging.error(
            'Пустая ссылка на марафон, несмотря на то, что дедлайн близко')
        return
    _, marathon_soup = request_text_soup(link)
    # выделение ссылки на каждый матч,
    # выделение домашней и гостевой команд, сохранение в массив словарей по каждой игре
    matches = []
    for elem in marathon_soup.find_all('div', class_='bg coupon-row'):
        # проверка даты матча
        match_date_text = elem.find('td', class_='date').text.strip()
        match_date = rus_date_convert(match_date_text)
        # обрабатываем только те матчи, которые проходят не раньше дня дедлайна по чемпионату
        if deadline_date <= match_date <= max_date:
            home_team, guest_team = elem.get('data-event-name').split(' - ')
            matches.append({
                'link': elem.get('data-event-path'),
                'home': home_team.strip(),
                'guest': guest_team.strip()
            })
    # срез только тех матчей, которые принадлежат ближайшему туру на основании метаданных тура
    match_links = matches[:match_num]
    # обработка возможных исключений
    if not match_links:
        logging.error(
            '{}: На марафоне не обнаружено матчей'.format(current_champ))
        return
    if len(match_links) != match_num:
        logging.warning(
            '{}: На марафоне обнаружено меньше матчей, чем ожидалось'.format(
                current_champ))

    # подсчет матожидания голов и вероятности клиншита для каждого матча - занесение всей статистики в дикту
    week_stats = {'team': [], 'cleansheet': [], 'goals': [], 'opponent': []}
    for match in match_links:
        match_link = PREFIX_MARATHON + match['link']
        _, match_soup = request_text_soup(match_link)
        expected_score_home, expected_score_away, cs_prob_home, cs_prob_away = score_cleansheet_expected(
            match_soup)
        # расширяем дикту
        week_stats['team'].extend([match['home'], match['guest']])
        week_stats['cleansheet'].extend([cs_prob_home, cs_prob_away])
        week_stats['goals'].extend([expected_score_home, expected_score_away])
        week_stats['opponent'].extend(
            [match['guest'] + '[д]', match['home'] + '[г]'])
    '''
    раскрашиваем и сортируем датафрейм, чтобы получить корректную раскраску, а потом, выцепив эту раскраску,
    применяем ее к датафрейму, основанному на тех же данных, но содержащий текстовые данные в колонках, чтобы
    можно было четче выделять спаренные матчи в игровом туре
    '''
    color_scheme, team_order = get_style_params(week_stats)
    s = set_style(week_stats, color_scheme, team_order)
    # логирование информации о скорости обработки каждого турнира
    logging.info('{}: линия марафон обработана, время обработки: {}s'.format(
        current_champ, round(time.time() - champ_start_time, 3)))
    return s
Beispiel #10
0
def marathon_processing(current_champ, current_champ_links, deadline_date,
                        match_num):
    # фиксирование времени по каждому чемпионату, логирование обработки каждого чемпионата
    champ_start_time = time.time()
    # запрос страницы с матчами по текущему чемпионату
    link = current_champ_links['marathon']
    if link:
        _, marathon_soup = request_text_soup(link)
    else:
        logging.error(
            'Пустая ссылка на марафон, несмотря на то, что дедлайн близко')
        return pd.DataFrame({}).style
    # выделение ссылки на каждый матч,
    # выделение домашней и гостевой команд, сохранение в массив словарей по каждой игре
    matches = []
    for elem in marathon_soup.find_all('div', class_='bg coupon-row'):
        # проверка даты матча
        match_date_text = elem.find('td', class_='date').text.strip()
        match_date = rus_date_convert(match_date_text)
        # обрабатываем только те матчи, которые проходят не раньше дня дедлайна по чемпионату
        if match_date >= deadline_date:
            home_team, guest_team = elem.get('data-event-name').split(' - ')
            matches.append({
                'link': elem.get('data-event-path'),
                'home': home_team.strip(),
                'guest': guest_team.strip()
            })
    # срез только тех матчей, которые принадлежат ближайшему туру на основании матчей, указанных на спортс.ру
    match_links = matches[:match_num]
    # подсчет матожидания голов и вероятности клиншита для каждого матча - занесение всей статистики в дикту
    week_stats = {'team': [], 'cleansheet': [], 'goals': []}
    for match in match_links:
        match_link = prefixMarathon + match['link']
        _, match_soup = request_text_soup(match_link)
        expected_score_home, cs_prob_away = score_cleansheet_expected(
            'First', match_soup)
        expected_score_away, cs_prob_home = score_cleansheet_expected(
            'Second', match_soup)
        # расширяем дикту
        week_stats['team'].extend([match['home'], match['guest']])
        week_stats['cleansheet'].extend([cs_prob_home, cs_prob_away])
        week_stats['goals'].extend([expected_score_home, expected_score_away])
    '''
    раскрашиваем и сортируем датафрейм, чтобы получить корректную раскраску, а потом, выцепив эту раскраску,
    применяем ее к датафрейму, основанному на тех же данных, но содержащий текстовые данные в колонках, чтобы
    можно было четче выделять спаренные матчи в игровом туре
    '''
    color_df = pd.DataFrame(week_stats, index=None)
    # суммирование покомандно - для ситуаций, где у какой-либо команды в одном туре будет несколько матчей
    color_df = color_df.groupby(color_df['team'], as_index=False).sum()
    color_df = color_df.sort_values(by=['goals', 'cleansheet'],
                                    ascending=[0, 0])
    # установка стиля (раскраска)
    cm = sns.diverging_palette(25, 130, as_cmap=True)
    color_s = color_df.style.background_gradient(
        cmap=cm, subset=['cleansheet', 'goals'])
    # для обновления параметра ctx в дикте styler объекта s - так сказать, применения раскраски
    color_s.render()

    # а теперь уже готовим датафрейм, который и пойдет на выход
    df = pd.DataFrame(week_stats, index=None)
    # округления для улучшения зрительного восприятия
    df.cleansheet = df.cleansheet.round(2)
    df.goals = df.goals.round(1)
    # группировка данных по командам + форматирование данных в каждой ячейке
    df = df.groupby(df['team'], as_index=False).agg({
        'cleansheet':
        lambda x: '{:.2f}'.format(sum(x)) +
        (' ({})'.format('+'.join(map(str, map(lambda y: round(y, 2), x))))
         if len(x) > 1 else ''),
        'goals':
        lambda x: '{:.1f}'.format(sum(x)) +
        (' ({})'.format('+'.join(map(str, map(lambda y: round(y, 1), x))))
         if len(x) > 1 else '')
    })
    # применяем индексы, полученные в ходе сортировки числовых данных
    df = df.loc[color_df.index]
    # прячем индекс, который не нужен на выходе
    s = df.style.hide_index()
    # редактирование тонкостей оформления в колонках
    s = s.set_properties(subset=['cleansheet', 'goals'],
                         **{
                             'width': '120px',
                             'text-align': 'center'
                         })
    s = s.set_properties(subset=['team'],
                         **{
                             'width': '210px',
                             'text-align': 'center',
                             'font-weight': 'bold'
                         })
    # и, наконец, подкрутка расцветки, а именно, взятие ее из обработанного датафрейма с числовыми данными
    s.ctx = color_s.ctx

    # логирование информации о скорости обработки каждого турнира
    logging.info('{}: линия марафон обработана, время обработки: {}s'.format(
        current_champ, round(time.time() - champ_start_time, 3)))
    return s
Beispiel #11
0
def calendar_processing(current_champ, current_champ_links, matchweek):
    # логирование информации о времени обработки каждого чемпионата
    champ_start_time = time.time()
    current_champ_link = current_champ_links['sports']
    if not current_champ_link:
        logging.warning('Для данного чемпионата календарь недоступен, обработка календаря пропускается...')
        return None
    # обработка таблицы
    team_links = table_processing(current_champ, current_champ_link, matchweek)
    # обработка букмекерской линии на победителя чемпионата
    global championProbs
    championProbs = champ_winner_probs(current_champ)
    # обработка календаря для каждой команды
    champ_calendar_dict = {}
    for team, team_link in team_links.items():
        team_link = team_link + 'calendar'
        # запрос страницы с календарем для каждой команды
        ''' (в теории можно обрабатывать через календарь соревнования)
        (НО - на спортс в таком случае не обязательна сортировка по дате 
        - используется сортировка по ФОРМАЛЬНОМУ туру)'''
        _,  calendar_team_soup = request_text_soup(team_link)
        # выцепление самой таблицы с календарем матчей
        calendar_team_body = calendar_team_soup.find('tbody')
        # получение списка матчей
        match_team_list = calendar_team_body.find_all('tr')

        date = []
        competition = []
        opponent = []
        side = []
        result = []
        for match in match_team_list:
            # убираем из рассмотрения матчи с пометкой "перенесен" вместо даты
            if 'перенесен' not in str(match):
                date.append(match.find('a').text.split('|')[0].strip())
                competition.append(match.div.a.get('href'))
                opponent.append(match.find_all('div')[1].text.strip())
                side.append(sideMap[match.find('td', class_='alRight padR20').text])
                result.append(match.find('a', class_='score').text.strip())

        # формирование календаря в формате лист в листе
        team_calendar = [[date[i] + ' ' + opponent[i] + side[i],
                         competition[i]] for i in range(0, len(opponent))]
        # выделение календаря будущих игр - их мы находим по наличию ссылки на превью вместо счета
        future_team_calendar = team_calendar[result.index('превью'):]
        # берем из списка будущих игр только те, которые будут проходить в интересующем нас чемпионате
        # для проверки используем ссылку на чемпионат из нашего маппинга, сравнивая ее со ссылкой на
        # чемпионат, который соответствует обрабатываемой игре
        future_team_calendar_champ = list(filter(lambda x: current_champ_link in x[1], future_team_calendar))
        # взятие ближайших 5 игр для каждой команды
        champ_calendar_dict[team] = dict(
            zip(['1', '2', '3', '4', '5'], get_first_matches(future_team_calendar_champ, 5)))
    # транспонирование таблицы
    champ_calendar = pd.DataFrame(champ_calendar_dict).transpose()
    # оформление и сохранение (если tableStats и championProbs пустые, то без оформления)
    champ_calendar_style = champ_calendar.style.apply(colorize_calendar) if (tableStats or championProbs)\
        else champ_calendar.style
    # логирование времени обработки каждого чемпионата
    logging.info('{}: календарь чемпионата обработан, время обработки: {}s'.format(current_champ, round(
        time.time() - champ_start_time, 3)))
    return champ_calendar_style