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
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
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
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
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)
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
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
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]
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
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
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