Как я боролся с анти-читом

от автора

Как все началось

Насколько мне известно, в большинстве русскоязычных тестировщиков скорости печати используется метрика CPM, наткнувшись на следующее видео, мне стало интересно посмотреть на свои показатели метрики WPM.

По окончанию тестирования пользователю показывается результат написанный на картинке. И мне она показалась не совсем корректной.

Достаточно обидно
Достаточно обидно

Реализация автонабора для соревновательного режима

На сайте пользователи создают соревнования в которых может участвовать кто угодно, выбор конкретной «комнаты» выглядит следующий образом

Пример соревнований
Пример соревнований

После присоединения пользователя встречает следующее окно:

Слова и поле для ввода
Слова и поле для ввода

Строка со словами меняется динамически по мере введения слов, изначально я думал что это вызовет некоторые проблемы, однако оказалось что словарь слов реализован достаточно простым способом. Код страницы выглядит следующим образом:

Начало словаря
Начало словаря

Из такой структуры можно сделать два вывода: первый — количество слов для набора ограниченно и длина словаря составляет 291 слово, второй — считать все элементы для набора можно следующим образом:

from bs4 import BeautifulSoup soup = BeautifulSoup(page_source, 'html.parser') words_list = list() boundaries = soup.find(attrs={'id': 'row1', 'style': "top: 1px;"}) for span in boundaries.find_all('span'): words_list.append(span.text)

Дальше только проще. Я реализовал небольшой класс содержащий методы для запуска/получения/ввода и применил его на случайном испытании:

from selenium import webdriver from selenium.webdriver.common.keys import Keys import time from bs4 import BeautifulSoup from webdriver_manager.chrome import ChromeDriverManager import json  HTML_SOURCE = "https://10fastfingers.com/competition/611216d69046c"   class MonkeyWorker:     def __init__(self, url, login, password):         self.__URL = url         #   Create actual driver         self.driver = webdriver.Chrome(ChromeDriverManager(version="92.0.4515.107").install())         self.driver.maximize_window()         #   Login         self.driver.get("https://10fastfingers.com/login")         time.sleep(2)         self.driver.find_element_by_xpath('//input[@name = "data[User][email]"]').send_keys(login)         self.driver.find_element_by_xpath('//input[@name = "data[User][password]"]').send_keys(password)         time.sleep(1)         self.driver.find_element_by_xpath('//button[@class = "CybotCookiebotDialogBodyButton"]').click()         self.driver.find_element_by_xpath('//button[@class = "btn btn-info" and @id = "login-form-submit"]').click()         time.sleep(3)         #   Open input Link         self.driver.get(self.__URL)         time.sleep(5)         self.soup = BeautifulSoup(self.driver.page_source, 'html.parser')         self.words_list = list()         self.input_form = self.driver.find_element_by_xpath('//input[@class = "form-control" and @id = "inputfield"]')      def get_words(self):         boundaries = self.soup.find(attrs={'id': 'row1', 'style': "top: 1px;"})         for span in boundaries.find_all('span'):             self.words_list.append(span.text)      def input_words(self):         for word in self.words_list:             for symbol in word:                 self.input_form.send_keys(symbol)             self.input_form.send_keys(Keys.SPACE)      def close(self):         self.driver.quit()   with open('config.txt') as file:     conf = json.load(file)  monke = MonkeyWorker(HTML_SOURCE, conf["login"], conf["password"]) monke.get_words() monke.input_words() char = input() monke.close()

Метод input_words намеренно итерируется по символам в слове с целью имитации ввода пользователем, а так же для возможности добавления случайной задержки перед вводом символа. Применение алгоритма дало следующий результат:

С этого момента и начинается интересная часть поста.

Встреча с анти-читом

После прохождения испытания я заметил что в таблице результатов не произошло изменений, и вот в чем была причина

При переходе по ссылке, пользователя встречает следующая страница. Из нее понятно что у меня есть неограниченное число попыток на «сдачу экзамена», и что триггером послужил аномально большой показатель WPM. Полный код для обхода анти-чит системы будет приложен в конце поста.

При нажатии на кнопку Start Test сразу появляются первые проблемы. Первая из них продемонстрирована ниже:

Возникает необходимость найти способ нажать на кнопку самым простым способом, и как мне показалось он следующий. После нахождения элемента кнопки необходимо вызвать скрипт клика. В Selenium это делается следующим способом:

driver.execute_script("$(arguments[0]).click();", self.driver.find_element_by_xpath('//button[@class = \                                     "btn btn-large btn-info" and @id = "start-btn"]'))

Было вполне ожидаемо, что после запуска теста меня встретит изображение с текстом который мне придется набирать. Мне было необходимо реализовать способ сохранения изображения для того чтобы потом превращать его в список слов. Моя идея заключалась в том, чтобы отыскать ссылку изображения в коде страницы и после загрузить картинку с помощью requests . Выглядеть это должно было примерно следующим образом:

IMAGE_URL = str(self.soup.find('div', attrs={"id": "word-img"}).find_all('img')[0]['src']) image = requests.get(IMAGE_URL).content()

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

def img_getter():     """     Function to get image by opening new tab. Doesn't work because 10fastfingers have smart text genearator     :return:     """     #   Getting image     IMAGE_URL = str(self.soup.find('div', attrs={"id": "word-img"}).find_all('img')[0]['src'])     self.driver.execute_script("window.open('');")     self.driver.switch_to.window(self.driver.window_handles[1])     self.driver.get(IMAGE_URL)     #   Convert selenium screenshot to Image     self.image = Image.open(io.BytesIO(self.driver.get_screenshot_as_png()))    # Creating Image     #   self.driver.close()     self.driver.switch_to.window(self.driver.window_handles[0])      self.image.show()     self.__image_preprocessing()

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

Мне был нужен только белый прямоугольник
Мне был нужен только белый прямоугольник
def __old_image_preprocessing(self):     image = self.image.convert('RGB')     numpy_image = np.array(image)     original = numpy_image.copy()     image = image.filter(ImageFilter.MedianFilter(3))      # Find X,Y coordinates of all white pixels     whiteY, whiteX = np.where(np.all(numpy_image == [255, 255, 255], axis=2))     top, bottom = whiteY[0], whiteY[-1]     left, right = whiteX[0], whiteX[-1]      # Crop white solid     ROI = original[top:bottom, left:right]     final = Image.fromarray(ROI)     final.show()     self.image = final

На первый взгляд все работало замечательно, однако все оказалось не так просто. Изображения были разными!

То что мне нужно было вводить
То что мне нужно было вводить
Изображение которое я получал переходя по ссылке
Изображение которое я получал переходя по ссылке

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

def getting_image(self):     element = self.driver.find_element_by_xpath('//div[@id = "word-img"]')     location = element.location_once_scrolled_into_view     size = element.size     area = (location["x"] * 2, location["y"] * 2, location["x"] * 2 + size["width"] * 2,             location["y"] * 2 + size["height"] * 2)      self.image = self.driver.get_screenshot_as_png()     self.image = Image.open(io.BytesIO(self.image))     self.image = self.image.crop(area)     self.__image_preprocessing()      def __image_preprocessing(self):     """     Apply SHARPEN filter to make words detecting little better     :return:     """     self.image.show()     self.image = self.image.filter(ImageFilter.SHARPEN)     self.image.show()     self.image = self.image.filter(ImageFilter.SHARPEN)     self.image.show()

И это сработало! Теперь осталось воспользоваться OCR Tesseract и дело сделано.

import pytesseract as image_engine from pytesseract import Output  def get_words(self):      words = image_engine.image_to_data(self.image, lang="eng", output_type=Output.DICT)     clean_words = list(filter(lambda x: x != '', words["text"]))     self.words_list = clean_words

При желании можно сделать более тщательную обработку изображения, исправив дисторсию слов, можно лучше настроить Tesseract, можно воспользоваться для определения слов чем-то в духе: https://github.com/courao/ocr.pytorch

Я же хотел просто увидеть результат работы. И он меня не разочаровал:

Так я реализовывал способ обойти анти-чит систему на https://10fastfingers.com/. Понятное дело что сделано это все лишь для веселья и возможности вспомнить основы работы с Selenium. Надеюсь мой пост Вам понравился. Спасибо за внимание!

Анти-чит алгоритм

from selenium import webdriver from selenium.webdriver.common.keys import Keys import time from bs4 import BeautifulSoup from webdriver_manager.chrome import ChromeDriverManager import json import pytesseract as image_engine from pytesseract import Output from PIL import Image, ImageFilter import io import numpy as np  HTML_SOURCE = "https://10fastfingers.com/anticheat"   class MonkeyWorker:     def __init__(self, url, login, password):         self.__URL = url         self.driver = webdriver.Chrome(ChromeDriverManager(version="92.0.4515.107").install())         self.action = webdriver.ActionChains(self.driver)         self.driver.maximize_window()         #   Login         self.driver.get("https://10fastfingers.com/login")         time.sleep(2)         self.driver.find_element_by_xpath('//input[@name = "data[User][email]"]').send_keys(login)         self.driver.find_element_by_xpath('//input[@name = "data[User][password]"]').send_keys(password)         time.sleep(1)         #   Bad way is about using execute java script         self.driver.find_element_by_xpath('//button[@class = \                                                 "CybotCookiebotDialogBodyButton"]').click()    # Apply cookies         self.driver.find_element_by_xpath('//button[@class = "btn btn-info" and @id = "login-form-submit"]').click()         time.sleep(1)   # Little sleep in case after login script redirect to another page          #   Go to anti-cheat page         self.driver.get(self.__URL)         time.sleep(1)         self.driver.find_element_by_xpath('//a[@href= "/anticheat/view/1/1" and @class = "btn btn-info" \                                             and text() = "Start Test "]').click()         time.sleep(5)          #   Button has specific form - so in my opinion the best way to click it - use execute script         self.driver.execute_script("$(arguments[0]).click();", self.driver.find_element_by_xpath('//button[@class = \                                     "btn btn-large btn-info" and @id = "start-btn"]'))         time.sleep(1)         self.soup = BeautifulSoup(self.driver.page_source, 'html.parser')         self.words_list = list()          #   Save element with form where we need to put Keys         self.input_form = self.driver.find_element_by_xpath('//textarea[@id = "word-input"]')         self.getting_image()      def getting_image(self):         element = self.driver.find_element_by_xpath('//div[@id = "word-img"]')         location = element.location_once_scrolled_into_view         size = element.size         area = (location["x"] * 2, location["y"] * 2, location["x"] * 2 + size["width"] * 2,                 location["y"] * 2 + size["height"] * 2)          self.image = self.driver.get_screenshot_as_png()         self.image = Image.open(io.BytesIO(self.image))         self.image = self.image.crop(area)         self.__image_preprocessing()          def not_working():             """             Function to get image by opening new tab. Doesn't work because 10fastfingers have smart text genearator             :return:             """             #   Getting image             IMAGE_URL = str(self.soup.find('div', attrs={"id": "word-img"}).find_all('img')[0]['src'])             self.driver.execute_script("window.open('');")             self.driver.switch_to.window(self.driver.window_handles[1])             self.driver.get(IMAGE_URL)             #   Convert selenium screenshot to Image             self.image = Image.open(io.BytesIO(self.driver.get_screenshot_as_png()))    # Creating Image             #   self.driver.close()             self.driver.switch_to.window(self.driver.window_handles[0])              self.image.show()             self.__image_preprocessing()       def __image_preprocessing(self):         """         Apply SHARPEN filter to make words detecting little better         :return:         """         self.image.show()         self.image = self.image.filter(ImageFilter.SHARPEN)         self.image.show()         self.image = self.image.filter(ImageFilter.SHARPEN)         self.image.show()      def __old_image_preprocessing(self):         image = self.image.convert('RGB')         numpy_image = np.array(image)         original = numpy_image.copy()         image = image.filter(ImageFilter.MedianFilter(3))          # Find X,Y coordinates of all white pixels         whiteY, whiteX = np.where(np.all(numpy_image == [255, 255, 255], axis=2))         top, bottom = whiteY[0], whiteY[-1]         left, right = whiteX[0], whiteX[-1]          # Crop white solid         ROI = original[top:bottom, left:right]         final = Image.fromarray(ROI)         final.show()         self.image = final      def get_words(self):          words = image_engine.image_to_data(self.image, lang="eng", output_type=Output.DICT)         clean_words = list(filter(lambda x: x != '', words["text"]))         self.words_list = clean_words      def input_words(self):         for word in self.words_list:             for symbol in word:                 self.input_form.send_keys(symbol)             self.input_form.send_keys(Keys.SPACE)         self.input_form.send_keys(Keys.TAB, Keys.ENTER)      def close(self):         self.driver.quit()   with open('config.txt') as file:     conf = json.load(file)   monkey = MonkeyWorker(HTML_SOURCE, conf["login"], conf["password"]) monkey.get_words() monkey.input_words() char = input() monkey.close() 


ссылка на оригинал статьи https://habr.com/ru/articles/573072/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *