Skip to content

Commit

Permalink
Release v2.0
Browse files Browse the repository at this point in the history
Обновлённая версия скрипта
  • Loading branch information
Kiruha01 authored Dec 19, 2021
2 parents b750440 + 4c6dca4 commit 5e5c631
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 127 deletions.
28 changes: 28 additions & 0 deletions ExamerException.py
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)
65 changes: 54 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Скрипт для поиска ответов тестов Examer
Данный скрипт испльзуется для автоматического поиска ответов тестов Экзамера путём перебора вопросов по этой теме с аккаунта преподавателя. Для использования необходимо сделать насколько подготовительных шагов

### Регистрация учителя
***На данный момент доступен вход на Экзамер только через вк.*** Поэтому создаём новый аккаунт вк (но можно использовать свой). Далее на сайте Examer'а входим через вк и выбираем пункт "Я учитель"
Данный скрипт используется для автоматического поиска ответов тестов Экзамера путём перебора вопросов по этой теме с аккаунта преподавателя. Для использования необходимо сделать насколько подготовительных шагов

## Подготовка
### Установка Python
Данный скрипт написан на python, поэтому скачиваем последнюю версию с [сайта](https://www.python.org/). Следуем инструкциям для вашей ОС.

Expand All @@ -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

```
263 changes: 175 additions & 88 deletions examer.py
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()
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ certifi
chardet
idna
lxml
MechanicalSoup
requests
six
soupsieve
Expand Down
Loading

0 comments on commit 5e5c631

Please sign in to comment.