Arknights — мобильная игра в жанре Tower Defense, в которую я играю уже больше года. Поклонники создали несколько инструментов и порталов, чтобы помочь друг другу в прогрессе. Вдохновленный ими, я решил начать работу над персональным проектом Data Science для игры.
Результатом, которым я делюсь сегодня, является скрипт Python для веб-скрейпинга набора данных статистики персонажей. Это будет не учебник, а своего рода демонстрация и рассказ, где я объясню свой мыслительный процесс и прогресс моего кода, чтобы прийти к окончательному решению.
Если вам просто нужен код, посетите мой репозиторий GitHub здесь.

Цели и проблемы, которые необходимо решить
Определившись с целями проекта, мне нужно было найти набор данных. В моем случае я искал характеристики персонажей, т. е. их Атаку, Защиту, Сопротивление и т. д. по мере повышения их уровня. Я не нашел этот набор данных в Интернете, но я знаю место, где эти данные записаны: Вики Arknights на GamePress. Итак, я придумал скрипт на Python, который просматривает страницы отдельных персонажей, считывает их статистику и объединяет все в один CSV-файл.
Хитрость в очистке набора данных заключалась в взаимодействии со страницей. При загрузке страницы персонажа (в качестве примера возьмем Фантома), вы видите, что по умолчанию его статистика показывается для 1-го уровня неэлитного продвижения. Но я хотел получить всю статистику для всех уровней. Другими словами, мне нужно было, чтобы скрипт программно взаимодействовал со стрелками рядом с уровнем персонажа, чтобы повышать его уровень, каждый раз считывал статистику, а затем повторял этот процесс для каждого уровня продвижения (не элитного, элитного 1 и элитного 2). .
Выбор библиотек
Язык программирования уже выбран: Python. Это язык, на котором мне удобнее всего писать код парсинга веб-страниц, а также язык, который я буду использовать для оставшейся части этого проекта. Вопрос заключался в том, какие библиотеки использовать. Раньше я использовал Beautiful Soup, но знал, что он не может взаимодействовать с веб-страницами. Что бы он ни читал сначала со страницы, это все, что вы можете получить. Не поймите меня неправильно, Beautiful Soup невероятно полезен, но отсутствие интерактивности здесь нарушило условия сделки.
Так что я (снова) вернулся к Selenium. Я использовал его в прошлом, хотя и реже, чем Beautiful Soup, но я хорошо понял его функциональные возможности и то, как он сможет достичь того, чего я хотел. В отличие от Beautiful Soup, который читает HTML, CSS и JavaScript страницы, Selenium может открыть новое окно браузера и, по сути, заставить бота выполнять ваши команды, то есть взаимодействовать со страницей. Конечно, он по-прежнему может читать то же содержимое, что и Beautiful Soup, но вы можете указать на текстовое поле и ввести свои учетные данные для аутентификации на веб-сайте или нажимать кнопки, как если бы вы делали это вручную.
Это было именно то, что я искал. Теперь, когда у меня был выбран язык программирования и библиотеки, я мог приступить к написанию кода.
Код: шаг за шагом
Теперь я расскажу вам о каждой задаче, которую мне нужно было решить в сценарии, чтобы завершить сценарий. Я предполагаю, что вы знакомы с основными методами парсинга веб-страниц, такими как использование инспектора браузера для поиска элементов HTML, но если вы понимаете HTML и Python, вы сможете понять, что происходит в коде.
Мы начнем с самой основной части: считываем статистику уровня персонажа по умолчанию (не элитный уровень 1) и строим оттуда, пока не придем к сценарию, который находит все существующие страницы персонажей и извлекает все данные о каждом персонаже. страница.
О, и, пожалуйста, примите во внимание, что Selenium будет работать с размером окна по умолчанию, который составляет примерно половину размера моего экрана.
Закрытие прикрепленного объявления
Хотя это могло бы не быть проблемой, если бы я развернул окно браузера или даже запустил Selenium в автономном режиме (не открывая окно браузера), я думаю, что было бы неплохо поделиться тем, как я решил эту проблему.
Когда Selenium прокрутил страницу вниз, в правом нижнем углу появилось липкое объявление. Проблема заключалась в том, что реклама перекрывалась кнопками повышения уровня. Когда Selenium пытался нажать на эти кнопки, он выдавал ошибку, потому что липкая реклама «получала клик». Думайте об этом как о слоях, реклама была на слое над кнопками повышения уровня, и Selenium нажимал на то, что было сверху…
Вы можете подумать: «Он только что сказал Selenium закрыть прилепленную рекламу», и это абсолютно верно. Магия заключалась в том, чтобы получить код для этого.
Для эффекта мне нужно было выполнить три шага в коде:
- Найдите элемент HTML для прокрутки вниз
- Прокрутите вниз до этого элемента, чтобы появилась прикрепленная реклама.
- Нажмите кнопку закрытия липкой рекламы
Вот фрагмент кода для этого:
Чтение данных для одного уровня
Убрав липкую рекламу, я смог выполнить самую основную задачу процесса: прочитать статистику для уровня персонажа по умолчанию. Это было довольно много из-за моих собственных требований о том, какие данные собирать и экспериментировать с синтаксисом Selenium, но в окончательной версии я очищаю следующее:
- Имя: имя персонажа
- Редкость: редкость персонажа (1–6 звезд)
- Класс: класс персонажа (охранник, снайпер, защитник и т.д.)
- Уровень продвижения: неэлитный, элитный 1 или элитный 2
- Уровень
- HP
- Атака
- Защита
- Сопротивление
- Время передислокации
- Стоимость ДП
- Количество блоков
- Интервал атаки
- CN Release Date: дата выхода персонажа на китайском сервере
- Global Release Date: дата выпуска персонажа на глобальных серверах.
- Is Limited: персонаж доступен только на ограниченных событиях?
(те, что не объяснено, являются внутриигровой статистикой)
Большую часть этих данных было легко прочитать. Помимо отсутствия идентификаторов у HTML-элементов, что означало, что мне приходилось находить элементы по их HTML-классу, в основном это был вопрос «выбрать элемент с классом X и прочитать его текст». Было несколько хитрых вещей, например, редкость, которая включала подсчет количества звезд редкости (элементы изображения), и поле «Ограничено», которое включало поиск слова «ограничено» в текстовом поле… Также нужно было взять несколько характеристик в качестве одну строку, а затем разбить текст, но я перейду к этому во фрагменте кода.
Опять же, если не принимать во внимание интерактивность, это был простой веб-скрейпинг. Было бы проще, если бы больше элементов HTML имели идентификаторы, но, к счастью, человек/люди, которые написали эти страницы, написали имена классов, как если бы они были идентификаторами, потому что они не повторялись слишком часто на каждой странице. Оглядываясь назад, это также было хорошо для практики Selenium.
Вот фрагмент кода для этой части. Обратите внимание, что он не включает имя персонажа, потому что оно читается не со страницы в окончательном решении, а из цикла, который запускает очистку:
Фрагмент кажется длинным, но на самом деле это одно и то же основное действие, повторяющееся несколько раз: найти элемент по имени класса (или id, если возможно) и прочитать его текст.
Но есть некоторые части, которые я хочу назвать. В строках с 11 по 16, чтобы найти оператор редкости, у меня был единственный способ найти все элементы img, которые существуют для редкости (этого изображения), и посчитать их. Редкость персонажа варьируется от 1 до 6, поэтому персонажи будут иметь столько звездочек для редкости.
Доступность символов и даты выпуска были найдены в двух таблицах с одним и тем же классом HTML, строки с 18 по 21 соответственно.

Сначала я прочитал даты выпуска, и они находятся во второй таблице с одной строкой для каждого выпуска (китайский и глобальный серверы соответственно). Код был одинаковым для каждого значения, но данные считывались из другой строки, следовательно, и с другим индексом. Дата китайского выпуска читается в строках 23–27, а глобальная — в строках с 28 по 32. Чтобы узнать, был ли символ ограничен, мне пришлось перейти к первой таблице HTML и прочитать текстовую информацию Headhunting (строки 33–37). Если оно включало слово limited, то Is Limited записывалось как 1, иначе как 0. Это условие записывалось в виде однострочника с тернарным оператором.
Другие характеристики персонажа (HP, Attack, Block Count и т. д.) требовали манипуляций со строками. Эти шесть статистических данных также были разбиты на две таблицы, но данные были глубоко вложены друг в друга.

(пожалуйста, игнорируйте второй набор характеристик, они относятся к способности Фантома, у других персонажей этого второго набора нет)
Мое решение состояло в том, чтобы прочитать текст каждой таблицы сразу, что вернуло следующее для первой таблицы.
HP 769 ATK 215 DEF 144
Каждая строка содержит название статистики, за которым следует ее значение в следующей строке. Я вызвал метод splitlines для этой строки, чтобы автоматически разделить каждое значение на символы новой строки. Таким образом, приведенная выше строка будет генерировать список
["HP", "769", "ATK", "215", "DEF", "144"]
Это было повторено для обеих таблиц статистики (начиная со строки 50 и далее).
Получить статистику для всех уровней одного персонажа
Теперь, когда я мог читать статистику для одного уровня, мне нужно было:
- Читать статистику для всех уровней текущего уровня продвижения
- Повторите процесс для оставшихся уровней продвижения.
Это было на самом деле довольно просто. Идея заключалась в том, чтобы прочитать статистику, нажать кнопку повышения уровня, прочитать статистику, нажать кнопку повышения уровня, …, а затем повторить это для каждого уровня продвижения. Это была идеальная цель для написания пользовательской функции, которая делала бы это.
Для повышения уровня необходимо нажать кнопку со стрелкой вправо, а для изменения уровня продвижения — нажать одну из трех кнопок. Итак, что я сделал:
- Найдите три кнопки уровня продвижения
- Прочитайте статистику для каждого уровня этого уровня продвижения (статистика чтения, повторение повышения уровня)
- Продвигайте персонажа, нажимая кнопку следующего уровня повышения
- Повторите шаг 2.
- Повторите шаг 3. (при необходимости у некоторых персонажей есть три уровня продвижения, у других два, а у третьих только один)
Кроме того, поскольку количество уровней в уровнях повышения зависит от уровня повышения и редкости персонажа, для этой пользовательской функции требовался параметр: количество уровней или количество раз, чтобы прочитать статистику и нажать кнопку повышения уровня. Вот фрагмент кода для этой части.
Большая часть кода уже была показана в предыдущем фрагменте кода. Важной частью здесь является логика чтения статистики, повышения уровня и продвижения персонажей по мере необходимости. Примечание. Я также реализовал некоторую логику, начинающуюся со строки 16 для различных уровней продвижения. Он проверяет, сколько уровней продвижения доступно на странице персонажа.
Переменная rank_buttons представляет собой список кнопок. Каждая кнопка — это уровень продвижения, поэтому я мог получить доступ к элементу этого списка и вызвать метод click. Затем функция get_stats_per_level повторяет цикл чтения уровня статистики X раз, в соответствии с количеством доступных уровней.
Если вы играете в Arknights, вы можете заметить, что я вычищаю слишком много уровней для персонажей с продвижением Elite 2 (строки 31 и далее), потому что это включает в себя 4 звезды или более редкости. Например, 4-звездочный персонаж имеет только 45 уровней для элиты 0/не элиты, а я пытаюсь получить 50 (строка 32). Это приводит к тому, что статистика для уровня 45 записывается для этих дополнительных уровней (дублированные значения).
Я сделал этот выбор, учитывая разнообразие максимальных уровней для разных уровней повышения и редкости, а также недостатки производительности в коде. Используемые ограничения уровня были основаны на 6-звездочных персонажах (тех, у которых максимальный уровень). Затем я удаляю повторяющиеся строки в конце скрипта перед записью в выходной CSV-файл (не включенный в этот фрагмент кода).
Наконец, обратите внимание, что данные вообще не сохраняются. Позже я объясню, как я справился с этим с помощью pandas DataFrames. Подсказка: функция get_stats_per_level нуждается в модификации.
Получить всех доступных персонажей
На этом логика очистки данных завершена. Но есть два недостающих компонента: знать, какие страницы очищаются, то есть список страниц с символами, и хранить данные в работающем кадре данных pandas для вывода в конце.
Эта часть далась мне очень легко, потому что я не в первый раз просматриваю эту вики Arknights. В прошлый раз мне также пришлось собирать список доступных персонажей, но вместо статистики я просматривал URL-адреса изображений персонажей, чтобы создать приложение для создания обоев со Streamlit! Хватит саморекламы, вот код для получения списка доступных символов и URL их страниц в виде словаря.
И смотрите, я действительно использовал Beautiful Soup (и запросы) для этой части. Честно говоря, я должен был переписать код, чтобы вместо этого использовать Selenium, но у меня уже был этот код, и я знал, что он работает, поэтому я использовал его повторно. Никакой интерактивности не требуется, просто загрузите страницу и соскребите информацию из таблицы символов.
В строках 15 и 16 вы видите, что я на самом деле не паршу URL-адреса. Имя персонажей на самом деле является гиперссылкой на их страницы (с использованием относительного пути). Снова используя Phantom в качестве примера,

Это означает, что значение атрибута href добавляется к базовому URL-адресу «https://gamepress.gg». Эта информация хранится в виде словаря, который сопоставляет каждый символ с URL-адресом их страницы.
Строка 21 показывает жестко закодированное значение. Немного углубившись в игру, Амия является главным героем, и недавно она получила альтернативную форму другого класса. Другие альтернативные персонажи — это отдельные персонажи, но два варианта Амии — это один и тот же персонаж в игре. Поэтому я думаю, что именно поэтому вики даже не показывает эту альтернативу Амия (Страж) в таблице персонажей. Однако для Amiya и Amiya (Guard) есть отдельные страницы, поэтому я жестко кодирую страницу для последней. Чтобы избежать проблем с написанием дополнительного кода, я решил, что можно жестко запрограммировать этого, так как еще одна Амия не должна добавляться в игру в течение длительного времени, а альтернативы для других персонажей добавляются как новые персонажи.
Наконец, словарь выводится в виде pickle-файла. Я сделал это в предыдущем проекте и сделал это снова здесь, потому что это работает как хорошая контрольная точка на случай, если что-то сломается при очистке страниц персонажей. Переменная словаря используется в остальной части скрипта, но рекомендуется иметь эту контрольную точку, если впоследствии что-то пойдет не так.
Вот фрагмент кода с рефакторингом кода. Как и прежде, я опускаю код, который не изменился с момента последнего показа.
Запись данных в DataFrane для вывода CSV
Когда все остальное было на своих местах, последней частью головоломки было сохранение данных где-нибудь, когда скрипт просматривает страницы и в конце выводит CSV.
Нам нужно вернуться к функции get_stats_per_level. Теперь, когда мне нужно было сохранить очищенные данные в DataFrame, функции требовался доступ к этому DataFrame. DataFrame изначально создается как пустой DataFrame вне функции. Однако он передается функции в качестве аргумента, поэтому в него записывается новая строка данных при каждой итерации цикла, повышающего уровень символов. DataFrame с последними данными возвращается в конце функции. Взгляните на следующий фрагмент кода, чтобы лучше понять этот новый код.
Я думаю, что комментарии в коде не требуют пояснений, но, по сути, чтобы добавить новую строку в DataFrame, я создаю новый словарь с последней извлеченной статистикой и добавляю его в DataFrame.
Если вы хотите взглянуть на полный скрипт, вы можете найти его на моем GitHub здесь. А если вам нужен результирующий набор данных, вы можете найти его здесь.
Заключительные мысли
Это было началом очень интересного личного проекта. У меня не только была возможность работать с данными из игры, на которую я трачу много времени, но я еще и работал над наукой о данных… ну, на данном этапе над инженерией данных. Было круто вернуться к Selenium после его использования пару лет назад и узнать больше о веб-скрапинге.
Но, учитывая характер работы, это шаткий сценарий. Сегодня это работает, но если GamePress решит изменить эти страницы, они могут легко сломать сценарий. API всегда лучше альтернативы, то есть когда они являются альтернативой… Когда API не вариант, веб-скрапинг все равно может выполнить работу, если ваши требования достаточно гибки и/или у вас есть люди, которые будут поддерживать код в долгосрочная перспектива.
И последнее, но не менее важное: отзывы очень приветствуются не только по коду, но и по самой статье. Обычно я пишу туториалы, но на этот раз я решил больше обсудить, что я сделал и почему я это сделал. Пожалуйста, дайте мне знать, понравилось ли вам читать и что можно улучшить. Я был слишком многословен? Нашел ли я хороший баланс между рассказом о коде и фактическим его показом? Должен ли я был подробнее остановиться на пояснениях кода? В любом случае, спасибо за чтение статьи, и я надеюсь, что этот код может помочь вам в будущем :)
P.S. если вы попытаетесь запустить этот скрипт сегодня или в ближайшие несколько дней, он сломается, потому что вики уже добавляет недавно объявленных персонажей, а их страницы все еще находятся в стадии разработки. Поэтому, если вы попытаетесь запустить скрипт, обязательно игнорируйте URL-адреса для этих символов. Код для этого находится в финальном сценарии (строки 132–135). При необходимости раскомментируйте эти строки.