Как стать автором
Обновить

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

Время на прочтение8 мин
Количество просмотров8.4K

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

Насколько мне известно, в большинстве русскоязычных тестировщиков скорости печати используется метрика 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()
Теги:
Хабы:
Всего голосов 12: ↑10 и ↓2+15
Комментарии5

Публикации

Истории

Работа

Data Scientist
84 вакансии
Python разработчик
132 вакансии

Ближайшие события