-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Обновлённая версия скрипта
- Loading branch information
Showing
5 changed files
with
278 additions
and
127 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
class LoginError(Exception): | ||
def __init__(self): | ||
self.message = "Неопознанная ошибка регистрации" | ||
super().__init__(self.message) | ||
|
||
|
||
class EmailPasswordError(Exception): | ||
def __init__(self): | ||
self.message = "Неверный email или пароль" | ||
super().__init__(self.message) | ||
|
||
|
||
class GettingTestError(Exception): | ||
def __init__(self): | ||
self.message = "Ошибка в получении теста" | ||
super().__init__(self.message) | ||
|
||
|
||
class SignError(Exception): | ||
def __init__(self): | ||
self.message = "Ошибка генерации запроса регистрации" | ||
super().__init__(self.message) | ||
|
||
|
||
class TeacherError(Exception): | ||
def __init__(self): | ||
self.message = "Пользователь не является учителем" | ||
super().__init__(self.message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,7 @@ | ||
# Скрипт для поиска ответов тестов Examer | ||
Данный скрипт испльзуется для автоматического поиска ответов тестов Экзамера путём перебора вопросов по этой теме с аккаунта преподавателя. Для использования необходимо сделать насколько подготовительных шагов | ||
|
||
### Регистрация учителя | ||
***На данный момент доступен вход на Экзамер только через вк.*** Поэтому создаём новый аккаунт вк (но можно использовать свой). Далее на сайте Examer'а входим через вк и выбираем пункт "Я учитель" | ||
Данный скрипт используется для автоматического поиска ответов тестов Экзамера путём перебора вопросов по этой теме с аккаунта преподавателя. Для использования необходимо сделать насколько подготовительных шагов | ||
|
||
## Подготовка | ||
### Установка Python | ||
Данный скрипт написан на python, поэтому скачиваем последнюю версию с [сайта](https://www.python.org/). Следуем инструкциям для вашей ОС. | ||
|
||
|
@@ -14,18 +12,63 @@ requirements.txt`. Открываем коммандную строку или | |
$ pip install -r requirements.txt | ||
``` | ||
|
||
### Установка логина и пароля | ||
Открываем файл `script.py`. В начале файла необходимо внести логин и пароль от вк для нашего преподавателя: | ||
### Регистрация учителя | ||
Регистрируем аккаунт на examer и после регистрации выбираем опцию **"Я учитель"**. | ||
|
||
## Простой пример | ||
В файле `script.py` вставляем логин и пароль от только что созданного аккаунта нашего преподавателя: | ||
```python | ||
# ====== Ваши данные здесь ============== | ||
# ======================================= | ||
|
||
EMAIL = 'TYPE YOUR E-MAIL' # логин | ||
PASSWORD = 'TYPE YOUR PASSWORD' # пароль | ||
EMAIL = 'TYPE YOUR E-MAIL' | ||
PASSWORD = 'TYPE YOUR PASSWORD' | ||
|
||
# ======================================= | ||
``` | ||
Далее сохраняет файл и запускаем командой | ||
Далее сохраняем файл и запускаем командой | ||
```bash | ||
$ python script.py | ||
``` | ||
Нас попросят ввести ссылку на тест. Вставляем её и через некоторое время в папке появится файл с ответами `answers.txt`. | ||
Нас попросят ввести ссылку на тест. Вставляем её и через некоторое время в папке появится файл с ответами `answers.txt`. | ||
|
||
## Использование | ||
### Вход в аккаунт | ||
```python | ||
from examer import Examer | ||
|
||
ex = Examer('[email protected]', 'password') | ||
``` | ||
В случае ошибки поднимаются исключения: | ||
* `ExamerException.LoginError` - неопознанная ошибка регистрации. | ||
* `ExamerException.EmailPasswordError` - неверный логин или пароль. | ||
* `ExamerException.SignError` - Ошибка генерации запроса регистрации (неверные куки, подпись запроса и прочее) | ||
* `ExamerException.TeacherError` - Данный пользователь не является учителем | ||
|
||
### Получение теста с ответами | ||
```python | ||
link = "https://t.examer.ru/f9afa" | ||
test = ex.get_test(link) | ||
``` | ||
В случае ошибки поднимаются исключения: | ||
* `ExamerException.GettingTestError` - ошибка получения теста. Осноная причина - неверныая ссылка на тест | ||
|
||
#### Работа с тестом | ||
```python | ||
# Тема теста | ||
test.theme | ||
# ID теста | ||
test.id_test | ||
# Возможное число баллов за тест | ||
test.score | ||
# Примерное время на выполнение теста | ||
test.avg_time | ||
|
||
for task in test.get_tasks(): | ||
# Текст вопроса | ||
task.question | ||
# Правильный ответ | ||
task.answer | ||
# Баллов за вопрос | ||
task.difficult | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,92 +1,179 @@ | ||
import mechanicalsoup | ||
from json import loads | ||
|
||
|
||
def convertDif(grade): | ||
if grade == 'easy': | ||
return 1 | ||
elif grade == 'normal': | ||
return 2 | ||
else: | ||
return 3 | ||
import hashlib | ||
from typing import List, Dict | ||
|
||
from bs4 import BeautifulSoup | ||
import requests | ||
|
||
from ExamerException import * | ||
|
||
|
||
class Task: | ||
""" | ||
Структура, описывающая задание | ||
:field id: str - ID задания | ||
:field question: str - текст задания | ||
:field difficult: int - баллы за задание (1, 2 или 3) | ||
:field avg_time: float - время на выполнение | ||
:field answer: str - правильный ответ | ||
""" | ||
def __init__(self, task_dict: Dict[str, str]): | ||
self.id: str = task_dict['id'] | ||
self.question: str = self.__remove_tags(task_dict['task_text']) | ||
self.difficult: int = self.__convert_dif(task_dict['difficult']) | ||
self.avg_time: float = float(task_dict['avg_time']) | ||
self.answer = None | ||
|
||
@staticmethod | ||
def __remove_tags(text: str) -> str: | ||
soup = BeautifulSoup(text, 'html.parser') | ||
return soup.text | ||
|
||
@staticmethod | ||
def __convert_dif(grade: str) -> int: | ||
if grade == 'easy': | ||
return 1 | ||
elif grade == 'normal': | ||
return 2 | ||
else: | ||
return 3 | ||
|
||
|
||
class ExamerTest: | ||
""" | ||
Структура, описывабщая тест | ||
:field theme: str - тема теста | ||
:field score: str - возможное количество баллов за тест | ||
:field tasks: Dist[str, Task] - словарь заданий вида ("task_id": Task) | ||
:field avg_time: int - примерное время выполнения теста | ||
""" | ||
def __init__(self, test_dict: Dict): | ||
self.theme: str = test_dict['title'] | ||
self.id_test: str = str(test_dict['scenarioId']) | ||
self.score: str = str(test_dict['score']) | ||
self.tasks: Dict[str, Task] = {} | ||
self.avg_time = 0 | ||
for task in test_dict['tasks']: | ||
t = Task(task) | ||
self.tasks[t.id] = t | ||
self.avg_time += t.avg_time | ||
self.unprocessed_tasks_id: List[str] = list(self.tasks.keys()) | ||
|
||
self.avg_time = round(self.avg_time / 30) | ||
|
||
def get_tasks(self) -> List[Task]: | ||
""" | ||
Получить список вопросов | ||
:return: List[Task] | ||
""" | ||
return list(self.tasks.values()) | ||
|
||
|
||
class Examer(object): | ||
def __init__(self, login=None, password=None): | ||
self.list_of_task = [] | ||
if login and password: | ||
self.auth(login, password) | ||
self.score = 'НЕТДАННЫХ' | ||
self.time = 0 | ||
|
||
|
||
def auth(self, login, passw): | ||
self.person = mechanicalsoup.StatefulBrowser() | ||
self.person.open('https://examer.ru/login/vkontakte') | ||
self.person.select_form() # Выбор формы с регистрацией {id="login_submit"} | ||
self.person['email'] = login | ||
self.person['pass'] = passw # ввод данных | ||
self.person.submit_selected() # Submit'им форму | ||
|
||
|
||
def set_link(self, link): | ||
self.link = link.split('/')[-1] | ||
|
||
|
||
def start(self, *arg, num_of_iter=100): | ||
list_of_pull = [] | ||
dict_of_task = {} | ||
print('Get tasks') | ||
tasks = self.person.get('https://teacher.examer.ru/api/v2/teacher/test/student/' + self.link) | ||
tasks = loads(tasks.text) | ||
def __init__(self, email: str, password: str): | ||
""" | ||
Основной класс для взаимодействия с API examer. | ||
:param email: str - email для входа в аккаунт | ||
:param password: str - пароль для входа в аккаунт | ||
:raises EmailPasswordError: неверный логин или пароль | ||
:raises LoginError: неопознанная ошибка входа | ||
:raises SignError: ошибка отправки запроса решистрации | ||
:raises TeacherError: пользователь не является учителем | ||
""" | ||
self.BASE_URL = "https://examer.ru/" | ||
self.SIGN_POSTFIX = 'Ic8_31' | ||
self.session = requests.session() | ||
if email and password: | ||
self.auth(email, password) | ||
|
||
def auth(self, email: str, password: str): | ||
""" | ||
Метод входа в аккаунт | ||
:param email: str | ||
:param password: str | ||
:return: None | ||
:raises EmailPasswordError: неверный логин или пароль | ||
:raises LoginError: неопознанная ошибка входа | ||
:raises SignError: ошибка отправки запроса решистрации | ||
:raises TeacherError: пользователь не является учителем | ||
""" | ||
response = self.session.get(self.BASE_URL) | ||
soup = BeautifulSoup(response.content, 'html.parser') | ||
token = soup.find(id='login-form').find('input', attrs={"name": "_token"})["value"] | ||
params = self.prepare_auth_request_params(email, password, token) | ||
|
||
headers = {"Referer": self.BASE_URL, "Cookie": response.headers['Set-Cookie']} | ||
response = self.session.post(self.BASE_URL + 'api/v2/login', headers=headers, data=params) | ||
if not response.json()['success']: | ||
if response.json()['error'] == 3: | ||
raise EmailPasswordError() | ||
elif response.json()['error'] == 101: | ||
raise SignError() | ||
else: | ||
raise LoginError() | ||
response = self.session.get(self.BASE_URL + 'api/v2/user').json() | ||
if not response['profile']['is_teacher']: | ||
raise TeacherError() | ||
|
||
def get_test(self, link: str) -> ExamerTest: | ||
""" | ||
Метод получения ответов на тест по ссылке. | ||
:param link: str - ссылка на тест | ||
:return: ExamerTest | ||
""" | ||
test_id = link.split('/')[-1] | ||
test = self.get_questions(test_id) | ||
|
||
while len(test.unprocessed_tasks_id): | ||
data = self.generate_test(test.id_test, test.theme) | ||
for task in data['tasks']: | ||
if task['id'] in test.unprocessed_tasks_id: | ||
test.tasks[task['id']].answer = task['answer'] | ||
test.unprocessed_tasks_id.remove(task['id']) | ||
return test | ||
|
||
def prepare_auth_request_params(self, email: str, password: str, token: str) -> Dict[str, str]: | ||
""" | ||
Подготовка параметров для регистрации и добавление подписи. | ||
:param email: str - email для входа | ||
:param password: str - пароль для входа | ||
:param token: str - токен с формы регистрации | ||
:return: Dict[str, str] | ||
""" | ||
data = { | ||
'_mail': email, | ||
'_pass': password, | ||
"_token": token, | ||
"source_reg": 'login_popup' | ||
} | ||
string = ''.join(sorted(data.values())) + self.SIGN_POSTFIX | ||
data['s'] = hashlib.md5(string.encode('utf-8')).hexdigest() | ||
return data | ||
|
||
def get_questions(self, link: str) -> ExamerTest: | ||
""" | ||
Получение вопросов теста. | ||
:param link: str - идентификатор теста из ссылки | ||
:return: ExamerText | ||
""" | ||
tasks = self.session.get(self.BASE_URL + 'api/v2/teacher/test/student/' + link).json() | ||
if 'error' in tasks: | ||
raise ArithmeticError | ||
self.theme = tasks['test']['title'] # Тема теста | ||
self.id_test = str(tasks['test']['scenarioId']) # ID теста | ||
self.score = str(tasks['test']['score']) | ||
|
||
|
||
|
||
for z in tasks['test']['tasks']: # Перебор в заданиях | ||
dict_of_task[z['id']] = {'question': '🌚'*convertDif(z['difficult']) + '\n' + z['task_text'], 'answer': None} | ||
self.time += float(z['avg_time']) | ||
list_of_pull.append(z['id']) # Добавление ID в список необработанных | ||
|
||
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= | ||
print('Get answers') | ||
payload = {'sid': '3', 'scenario': '1', 'id': self.id_test, 'title': self.theme, 'easy': '6', 'normal': '7', 'hard': '7'} | ||
iterations = 0 | ||
while len(list_of_pull) and iterations <= num_of_iter: # Защита от невозможности найти вопрос | ||
iterations += 1 | ||
res = self.person.post(url='https://teacher.examer.ru/api/v2/teacher/test', data=payload) # Получаем данные с ответами | ||
data = loads(res.text) # Конвертируем | ||
|
||
for task in data['tasks']: # В каждом вопросе ищем | ||
if task['id'] in list_of_pull: # Если ID задания в пуле | ||
dict_of_task[task['id']]['answer'] = task['answer'] # то отмечаем ответ | ||
list_of_pull.remove(task['id']) # и удаляем из пула | ||
|
||
for ids in dict_of_task: | ||
self.list_of_task.append(dict_of_task[ids]) | ||
|
||
|
||
def format_text(self): | ||
print('Format text') | ||
for task in self.list_of_task: | ||
s = task['question'] | ||
i = 0 | ||
while s.find('<') != -1 or s.find('>') != -1: | ||
pattern = s[s.find('<') : s.find('>')+1] | ||
|
||
if pattern == '<li>': | ||
i += 1 | ||
count = 1 | ||
rep = str(i) + ') ' | ||
elif pattern == '': | ||
break | ||
else: | ||
rep = '' | ||
count = 999 | ||
|
||
s = s.replace(pattern, rep, count) | ||
task['question'] = s | ||
raise GettingTestError() | ||
return ExamerTest(tasks['test']) | ||
|
||
def generate_test(self, test_id: str, test_theme: str) -> Dict: | ||
""" | ||
Генерация теста из случайных вопросов по выбранной теме | ||
:param test_id: str - ID предмета | ||
:param test_theme: str - название темы теста | ||
:return: Dict | ||
""" | ||
payload = {'sid': '3', 'scenario': '1', 'id': test_id, 'title': test_theme, 'easy': '6', 'normal': '7', | ||
'hard': '7'} | ||
return self.session.post(url='https://teacher.examer.ru/api/v2/teacher/test', | ||
data=payload).json() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,6 @@ certifi | |
chardet | ||
idna | ||
lxml | ||
MechanicalSoup | ||
requests | ||
six | ||
soupsieve | ||
|
Oops, something went wrong.