Ежедневный фэнтези-спорт - Парсинг данных о конкурсах
Table Of Content
- Введение
- Автоматизация
- Настройка
- Парсинг
- копировать все
- пройти по странице
- Преобразование данных в информацию
- очистка и разделение строки буфера обмена
- определение начала значимых данных
- (относительно последнего элемента 'WON')
- индексы позиций
- индексы для информации о составе команды, имен игроков и их счетов
- Валидация
- Улучшения скорости
- Заключение
Автоматизация скучных вещей.
Введение
Одной из целей моего путешествия в мир ежедневного фэнтези-спорта было собрать огромное количество данных для анализа вне сезона. Результаты конкурсов являются ключевым элементом этой картины. С помощью этих данных можно сравнить составленные моделью составы с реальными составами, составленными участниками.
Обычные методы, такие как 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 имеет функциональность, которая позволяет легко реализовывать любую структурированную и повторяющуюся задачу.