Как все началось
Насколько мне известно, в большинстве русскоязычных тестировщиков скорости печати используется метрика 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/
Добавить комментарий