CoderCastrov logo
CoderCastrov
Фэнтези Спорт

Ежедневный фэнтези-спорт - Парсинг данных о конкурсах

Ежедневный фэнтези-спорт - Парсинг данных о конкурсах
просмотров
7 мин чтение
#Фэнтези Спорт

Автоматизация скучных вещей.

Layout of FanDuel contests.

Введение

Одной из целей моего путешествия в мир ежедневного фэнтези-спорта было собрать огромное количество данных для анализа вне сезона. Результаты конкурсов являются ключевым элементом этой картины. С помощью этих данных можно сравнить составленные моделью составы с реальными составами, составленными участниками.

Обычные методы, такие как read_html() в pandas, не работают из-за ошибки 403. Поиск в интернете указывает на способы аутентификации. Однако FanDuel не одобряет этого и может применить запрет.

HTTPError: Ошибка HTTP 403: Запрещено

Желая собрать данные, я начал кликать по ста составам на каждый конкурс и вставлять данные в Excel. Во время сбора почти шестидесяти конкурсов, у меня возникли мысли об автоматизации процесса, чтобы избавиться от 20-минутной муки каждого конкурса, но я подумал, что это будет слишком сложно, учитывая, что сезон уже подходит к концу.

Затем я открыл для себя pyautogui. Нажмите здесь для моего полного блокнота.



Автоматизация

Сбор, очистка и проверка данных о конкурсах FanDuel возможны с помощью ручного труда, так как данные, хотя и неструктурированные, имеют определенную структуру. Любая структурированная и повторяющаяся задача является хорошим кандидатом для автоматизации.

После того, как пользователь вошел в систему FanDuel и перешел на страницу конкурса, все необходимые элементы находятся в одном месте. Щелчок по составу команды и копирование всех содержимого в буфер обмена (CTRL+A, CTRL+C) позволяет получить данные о пользователе, счете, выигрыше, отдельных игроках и их счетах. Затем достаточно просто перебирать каждый состав команды и страницу до тех пор, пока конкурс не будет исчерпан.


Настройка

Скрипт начинается с открытия URL-адреса в уже открытом браузере. (Обратите внимание, убедитесь, что сохраняете URL-адреса контестов, прежде чем они исчезнут из вкладки История). Для написания и нажатия клавиши Enter достаточно трех простых функций: click(), typewrite() и hotkey().

import pyautogui as gui

def openURL(url):
  """Открывает URL-адрес в браузере. Предполагается, что браузер открыт, а строка поиска находится в верхнем левом углу, и пользователь вошел в систему FanDuel."""
  # щелкнуть по строке поиска
  gui.click(200, 60)

  # вставить и нажать Enter
  gui.typewrite(url)
  gui.hotkey('enter', interval=.2)

Затем происходит определение ключевых точек на экране. Pyautogui имеет функциональность для считывания экрана и сравнения сохраненных изображений для нахождения местоположений. Вместо жесткого кодирования координат предпочтительно использовать изображения, но это очень чувствительно, так как оно сравнивает пиксели, и даже изменение масштабирования экрана может вызвать сбой.

Для участников на странице требуются координаты, а также кнопки для навигации по страницам. "Position" используется в качестве отправной точки для участников с заданным значением delta, определяющим расстояние между ними. Изображения для "First" и "Next" дают координаты навигации.

def locateKeys():
  """Возвращает рамки для кнопок '< First' и 'Next >', заголовка 'Position'. Чувствительно к точному совпадению пикселей с оригиналами."""
  FIRST = gui.locateOnScreen('images/first.png')
  NEXT = gui.locateOnScreen('images/next.png')
  POSITION = gui.locateOnScreen('images/position.png')

  return FIRST, NEXT, POSITION  

Парсинг

Теперь перейдем к основной части этого скрипта. Парсинг заключается в выборе участника, копировании всего текста, вставке и переходе к следующему участнику. Определены ключевые места, которые просто нужно пройти в цикле. Что касается сбора данных, функция hotkey() из пакета pyautogui легко копирует все данные, имитируя нажатие CTRL+A и CTRL+C. Для хранения данных буфера обмена в виде строки в python используется еще один пакет - pyperclip.

import pyperclip 

# копировать все
gui.hotkey('ctrl', 'a', interval=.1)
gui.hotkey('ctrl', 'c', interval=.1)

new_text = pyperclip.paste()

Python работает намного быстрее, чем время загрузки элементов в браузере. Переменная PATIENCE определяет время ожидания для загрузки. Данные буфера обмена также сравниваются с последним добавленным текстом, чтобы убедиться, что участник не был загружен слишком быстро.

Собирая все вместе, проходим по десяти участникам на каждой странице, а затем нажимаем кнопку "Далее".

# пройти по странице
for j in range(pages):
  
  # пройти по 'position'
  for i in range(10):
    # выбрать позицию, подождать загрузку
    gui.click(x=POSITION[0], y=(start + i * delta), interval=PATIENCE)

    try:
      if new_text != '':
          pass
    except:
        new_text = ''
    while new_text == old_text:
      # копировать все
      gui.hotkey('ctrl', 'a', interval=.1)
      gui.hotkey('ctrl', 'c', interval=.1)

      new_text = pyperclip.paste()

    # добавить буфер обмена к данным
    data.append(new_text)
    
    # сбросить старый текст
    old_text = new_text

  gui.click(NEXT[0] + 5, NEXT[1] + 5, interval=max(1, PATIENCE*3))


Преобразование данных в информацию

На первый взгляд, данные буфера обмена выглядят как неструктурированный мусор, который отличается от участника к участнику. При более близком рассмотрении можно обнаружить шаблон, где находится информация о составе команды и игроках. Строка данных очищается, разделяется на список и преобразуется в серию pandas. Информация о составе команды находится рядом с элементом "WON", относящимся к выигрышам участника. Имена игроков и их счета находятся рядом с элементами, содержащими сокращения позиций.

# очистка и разделение строки буфера обмена
ser = pd.Series(string.replace('\n', '').split('\r'))

# определение начала значимых данных
# (относительно последнего элемента 'WON')
start = ser[ser == 'WON'].index[-1] + 5

info = np.array([0, 1, 2, 4, 6]) + start

# индексы позиций
  positions = np.array([
      ser[ser == 'QB'].index[0],
      ser[ser == 'RB'].index[0],
      ser[ser == 'RB'].index[1],
      ser[ser == 'WR'].index[0],
      ser[ser == 'WR'].index[1],
      ser[ser == 'WR'].index[2],
      ser[ser == 'TE'].index[0],
      ser[ser == 'FLEX'].index[0],
      ser[ser == 'DEF'].index[0],
  ])

# индексы для информации о составе команды, имен игроков и их счетов
indices = np.concatenate([info, positions + 1, positions + 2])

Объединение каждой серии для ста участников приводит к конечному результату - созданию фрейма данных для данного соревнования.

df.info()

<class 'pandas.core.frame.DataFrame'>
Int64Index: 100 entries, 1 to 100
Data columns (total 23 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   Опыт        100 non-null    object 
 1   Имя пользователя    100 non-null    object 
 2   Позиция    100 non-null    object 
 3   Выигрыш    100 non-null    float64
 4   Счет       100 non-null    float64
 5   QB          100 non-null    object 
 6   RB1         100 non-null    object 
 7   RB2         100 non-null    object 
 8   WR1         100 non-null    object 
 9   WR2         100 non-null    object 
 10  WR3         100 non-null    object 
 11  TE          100 non-null    object 
 12  FLEX        100 non-null    object 
 13  DEF         100 non-null    object 
 14  P_QB        100 non-null    float64
 15  P_RB1       100 non-null    float64
 16  P_RB2       100 non-null    float64
 17  P_WR1       100 non-null    float64
 18  P_WR2       100 non-null    float64
 19  P_WR3       100 non-null    float64
 20  P_TE        100 non-null    float64
 21  P_FLEX      100 non-null    float64
 22  P_DEF       100 non-null    int64  
dtypes: float64(10), int64(1), object(12)
memory usage: 18.8+ KB

Валидация

Сбор данных вручную оказался разочаровывающим, потому что, даже при более медленном темпе, чем в Python, копирование буфера обмена часто захватывало предыдущего участника из-за времени загрузки. Проверка корректности данных перед сохранением в файл является важным шагом для обеспечения получения не только мусора.

Я решил упростить этот процесс, реализовав три теста и возвращая только результат прохождения или не прохождения валидации. Проверка наличия ста уникальных пользователей в dataframe является критической, так как каждый участник может иметь только одну запись. Проверка формата имени игрока и суммы очков игроков, равной сумме очков команды, являются дополнительными проверками.

Если dataframe не проходит валидацию, весь процесс парсинга начинается заново. Нажатие кнопки "First" вместо перезагрузки страницы позволяет загружать данные каждого участника быстрее во второй раз. Также увеличивается время ожидания на четверть секунды. Процесс продолжается, пока не будет получен dataframe, прошедший валидацию.


Улучшения скорости

Парсинг контеста занимает примерно три минуты. Если валидация не проходит, процесс перезапускается с немного большим уровнем терпения и занимает около трех с половиной минут. Более умным способом для повторного получения данных было бы возвращать индексы проблемных участников во время валидации.

Еще один подход для ускорения процесса заключается в уменьшении уровня терпения до минимума, скажем, до одной десятой секунды. Данные буфера обмена будут преобразовываться в последовательность значимой информации и проверяться "на лету". Если проверка не проходит, все данные копируются в буфер обмена и производится повторная проверка.

Для моих целей сохранение результатов сезона контестов для последующего анализа занимало от трех до шести минут на контест. Я позволил второму ноутбуку пройти через 120 контестов, пока я выполнял свою ежедневную работу. Все они были спарсены, проверены и сохранены к концу дня.


Заключение

Нет ничего плохого в том, чтобы делать вещи вручную, чтобы понять их суть. Однако, когда шаблон уже определен и задача должна быть выполнена многократно, компьютер лучше всего подходит для решения проблемы. Pyautogui имеет функциональность, которая позволяет легко реализовывать любую структурированную и повторяющуюся задачу.