first commit
This commit is contained in:
parent
2afd24cce6
commit
7dfed35b3e
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
./data/logs/*.logs
|
||||
./data/data.db
|
||||
.vscode
|
||||
.gitignore
|
||||
.idea/
|
||||
./__pycache__/
|
||||
./GlobalExambBot/__pycache__/
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
configuration.yml
|
||||
GlobalExambBot/__pycache__/
|
||||
data/profiles/prof_*
|
||||
.idea/
|
||||
__pycache__/
|
||||
data/logs/*.log
|
||||
data/data.db
|
||||
.vscode
|
||||
BIN
ChromeDriver/chromedriver
Executable file
BIN
ChromeDriver/chromedriver
Executable file
Binary file not shown.
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
RUN apt-get update && apt-get install -yq wget
|
||||
|
||||
# download and install the specific version of Chromium
|
||||
RUN wget --no-verbose -O /tmp/chrome.deb http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_104.0.5112.79-1_amd64.deb
|
||||
RUN apt-get install -yf /tmp/chrome.deb
|
||||
|
||||
# set display port to avoid crash
|
||||
ENV DISPLAY=:99
|
||||
|
||||
# copy the script
|
||||
COPY . /app/
|
||||
|
||||
# set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# install selenium
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# run the script
|
||||
ENTRYPOINT ["python", "main.py"]
|
||||
38
GlobalExamBot/Sheets.py
Normal file
38
GlobalExamBot/Sheets.py
Normal file
@ -0,0 +1,38 @@
|
||||
import logging
|
||||
from math import floor
|
||||
|
||||
from GlobalExamBot.helpers import wait_between
|
||||
from GlobalExamBot.database import Database
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
class Sheets:
|
||||
def __init__(self, driver, action, configuration):
|
||||
self.driver = driver
|
||||
self.actions = action
|
||||
self.configuration = configuration
|
||||
self.pagecard_xpath = '//a[@class="mb-4 w-full lg:w-auto lg:mb-0 button-solid-primary-small"]'
|
||||
self.Sheetscard_xpath = '//div[@class="container py-8 lg:pt-12 lg:pb-12"]'
|
||||
self.manageSheets = Database()
|
||||
|
||||
def search(self):
|
||||
WebDriverWait(self.driver, 15).until(ec.visibility_of_element_located((By.XPATH, self.pagecard_xpath)))
|
||||
page_cards = self.driver.find_elements(by=By.XPATH, value=self.pagecard_xpath)
|
||||
card_list = []
|
||||
for card in page_cards :
|
||||
if not self.manageSheets.link_exist(card.get_attribute('href')):
|
||||
card_list.append(card)
|
||||
return card_list
|
||||
|
||||
def watch(self, Sheets_el):
|
||||
self.actions.move_to_element(Sheets_el).click(Sheets_el).perform()
|
||||
WebDriverWait(self.driver, 15).until(ec.visibility_of_element_located((By.XPATH, self.Sheetscard_xpath)))
|
||||
max_height = self.driver.execute_script("return document.body.scrollHeight")
|
||||
for height in range(0, max_height, floor(max_height/10)) :
|
||||
self.driver.execute_script(f"window.scrollTo(0, { height })")
|
||||
logging.info(f'Position : { height } | MaxPosition: { max_height }')
|
||||
wait_between(25,30)
|
||||
logging.info(f'Add new url in database: { self.driver.current_url }')
|
||||
self.manageSheets.add_link(self.driver.current_url)
|
||||
68
GlobalExamBot/bot.py
Normal file
68
GlobalExamBot/bot.py
Normal file
@ -0,0 +1,68 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from GlobalExamBot.helpers import TypeInField, element_exists, wait_between
|
||||
from GlobalExamBot.Sheets import Sheets
|
||||
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
class Bot:
|
||||
def __init__(self, driver, action, configuration):
|
||||
self.driver = driver
|
||||
self.actions = action
|
||||
self.configuration = configuration
|
||||
self.email_xpath = '//input[@name="email"]'
|
||||
self.password_xpath = '//input[@name="password"]'
|
||||
self.index = 0
|
||||
self.scrollcount = 0
|
||||
self.categories = ['https://exam.global-exam.com/library/study-sheets/categories/grammar',
|
||||
'https://exam.global-exam.com/library/study-sheets/categories/language-functions',
|
||||
'https://exam.global-exam.com/library/study-sheets/categories/vocabulary']
|
||||
|
||||
def login(self):
|
||||
email_el = WebDriverWait(self.driver, 10).until(ec.visibility_of_element_located((By.XPATH, self.email_xpath)))
|
||||
self.actions.move_to_element(email_el).click(email_el).perform()
|
||||
TypeInField(self.driver, self.email_xpath, self.configuration.username)
|
||||
password_el = WebDriverWait(self.driver, 10).until(ec.visibility_of_element_located((By.XPATH, self.password_xpath)))
|
||||
self.actions.move_to_element(password_el).click(password_el).perform()
|
||||
TypeInField(self.driver, self.password_xpath, self.configuration.password)
|
||||
password_el.send_keys(Keys.RETURN)
|
||||
|
||||
def run(self):
|
||||
|
||||
profile = f'prof_{self.configuration.username}'
|
||||
|
||||
if not os.path.exists(f'./Profiles/{profile}'):
|
||||
self.driver.get('https://auth.global-exam.com/login')
|
||||
self.login()
|
||||
else :
|
||||
self.driver.get('https://exam.global-exam.com/library/study-sheets/categories/grammar')
|
||||
if element_exists('//input[@name="email"]', self.driver) :
|
||||
self.login()
|
||||
|
||||
logging.info('Logged in')
|
||||
|
||||
Sheets_action = Sheets(self.driver, self.actions, self.configuration)
|
||||
|
||||
while True:
|
||||
self.driver.get('https://exam.global-exam.com/library/study-sheets/categories/grammar')
|
||||
Sheets_list = Sheets_action.search()
|
||||
if Sheets_list :
|
||||
logging.info(f'Sheets n°{ self.index }')
|
||||
Sheets_action.watch(Sheets_list[0])
|
||||
self.index +=1
|
||||
self.scrollcount = 0
|
||||
wait_between(3,10)
|
||||
else:
|
||||
logging.info('All visible Sheets have already been read. Need to scroll down ...')
|
||||
self.driver.execute_script(f"window.scrollTo(0, document.body.scrollHeight)")
|
||||
self.scrollcount += 1
|
||||
wait_between(5,15)
|
||||
if self.scrollcount > 10:
|
||||
logging.info('End of page or network error.')
|
||||
self.scrollcount = 0
|
||||
logging.info(self.driver.get_log('browser'))
|
||||
4
GlobalExamBot/constants.py
Normal file
4
GlobalExamBot/constants.py
Normal file
@ -0,0 +1,4 @@
|
||||
def init():
|
||||
global VERSION, APP_NAME
|
||||
APP_NAME = 'GlobalExamBot'
|
||||
VERSION = '1.0.0'
|
||||
48
GlobalExamBot/database.py
Normal file
48
GlobalExamBot/database.py
Normal file
@ -0,0 +1,48 @@
|
||||
import sqlite3
|
||||
|
||||
class Database:
|
||||
def __init__(self, database_link='./data/data.db'):
|
||||
"""
|
||||
Database constructor
|
||||
"""
|
||||
self.database_link = database_link
|
||||
|
||||
def add_link(self, link):
|
||||
"""
|
||||
Add a link to the database
|
||||
|
||||
:param link String: link of a sheet
|
||||
"""
|
||||
connection = sqlite3.connect(self.database_link)
|
||||
c = connection.cursor()
|
||||
c.execute('''INSERT INTO sheet_links (link) VALUES (:link);''', (link,))
|
||||
c.close()
|
||||
connection.commit()
|
||||
|
||||
def link_exist(self, link):
|
||||
"""
|
||||
Returns true if the link exists in the database.
|
||||
|
||||
:param link String: Link of a sheet
|
||||
"""
|
||||
connection = sqlite3.connect(self.database_link)
|
||||
c = connection.cursor()
|
||||
c.execute('''SELECT * FROM sheet_links WHERE link = ?;''', (link,))
|
||||
data = c.fetchall()
|
||||
# If this link exist or not
|
||||
if len(data) == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def create_table_sheets():
|
||||
"""
|
||||
Create new tables to save the sheets links.
|
||||
|
||||
"""
|
||||
connection = sqlite3.connect('./data/data.db')
|
||||
c = connection.cursor()
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS sheet_links
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, link text);''')
|
||||
c.close()
|
||||
connection.commit()
|
||||
65
GlobalExamBot/driver.py
Normal file
65
GlobalExamBot/driver.py
Normal file
@ -0,0 +1,65 @@
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
class Driver:
|
||||
def __init__(self, profile):
|
||||
self.profile = profile
|
||||
self.chrome_options = None
|
||||
self.driver = None
|
||||
self.action = None
|
||||
|
||||
def setup(self, log_path='./data/logs/', headless=True):
|
||||
self.chrome_options = Options()
|
||||
|
||||
# Anti bot detection
|
||||
self.chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])
|
||||
self.chrome_options.add_experimental_option('useAutomationExtension', False)
|
||||
self.chrome_options.add_argument('--disable-blink-features=AutomationControlled')
|
||||
|
||||
# Language Browser
|
||||
self.chrome_options.add_argument('--lang=fr-FR')
|
||||
|
||||
# Maximize Browser
|
||||
self.chrome_options.add_argument('--start-maximized')
|
||||
|
||||
# Headless Mode
|
||||
if headless:
|
||||
self.chrome_options.add_argument('--headless')
|
||||
self.chrome_options.add_argument("window-size=1400,2100")
|
||||
|
||||
|
||||
# Optimize CPU
|
||||
self.chrome_options.add_argument("--no-sandbox")
|
||||
self.chrome_options.add_argument("--disable-dev-shm-usage")
|
||||
self.chrome_options.add_argument("--disable-renderer-backgrounding")
|
||||
self.chrome_options.add_argument("--disable-background-timer-throttling")
|
||||
self.chrome_options.add_argument("--disable-backgrounding-occluded-windows")
|
||||
self.chrome_options.add_argument("--disable-client-side-phishing-detection")
|
||||
self.chrome_options.add_argument("--disable-crash-reporter")
|
||||
self.chrome_options.add_argument("--disable-oopr-debug-crash-dump")
|
||||
self.chrome_options.add_argument("--no-crash-upload")
|
||||
self.chrome_options.add_argument("--disable-gpu")
|
||||
self.chrome_options.add_argument("--disable-extensions")
|
||||
self.chrome_options.add_argument("--disable-low-res-tiling")
|
||||
self.chrome_options.add_argument("--log-level=3")
|
||||
self.chrome_options.add_argument("--silent")
|
||||
|
||||
|
||||
# Disable save password
|
||||
prefs = {'credentials_enable_service': False,
|
||||
'profile.password_manager_enabled': False}
|
||||
self.chrome_options.add_experimental_option('prefs', prefs)
|
||||
|
||||
# Set profile
|
||||
self.chrome_options.add_argument(f'user-data-dir=./data/profiles/{self.profile}')
|
||||
|
||||
self.driver = webdriver.Chrome(f'./ChromeDriver/chromedriver',options=self.chrome_options, service_args=[f'--log-path={log_path}ChromeDriver.log'])
|
||||
self.action = ActionChains(self.driver)
|
||||
return self.driver, self.action
|
||||
|
||||
def get_driver(self):
|
||||
return self.driver
|
||||
|
||||
def get_action(self):
|
||||
return self.action
|
||||
104
GlobalExamBot/helpers.py
Normal file
104
GlobalExamBot/helpers.py
Normal file
@ -0,0 +1,104 @@
|
||||
# Standard libraries
|
||||
import sys
|
||||
import logging
|
||||
from time import sleep
|
||||
from random import uniform
|
||||
import argparse
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
import GlobalExamBot.constants as const
|
||||
|
||||
|
||||
class Helpers:
|
||||
def __init__(self):
|
||||
"""
|
||||
Helpers class constructor
|
||||
"""
|
||||
|
||||
def ask_to_exit(self):
|
||||
"""
|
||||
The user is asked if he wants to leave
|
||||
|
||||
:return: Boolean
|
||||
"""
|
||||
try:
|
||||
user_input = input('Type "STOP" to stop the application:\n')
|
||||
|
||||
# Continue
|
||||
if user_input.upper() == "STOP":
|
||||
return True
|
||||
# Exit
|
||||
else:
|
||||
print('/!\\ Please type "STOP" to exit the bot execution /!\\')
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def load_configuration(self):
|
||||
"""
|
||||
this method allows you to load arguments.
|
||||
:return: args
|
||||
"""
|
||||
header()
|
||||
# Load all configuration variables
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-p','--password', help='Set GlobalExam password', type=str, required=True)
|
||||
parser.add_argument('-u','--username',help='Set GlobalExam username', type=str, required=True)
|
||||
parser.add_argument('--noheadless',help='Desactivate Chrome headless mode', required=False, action='store_true')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def logging_configuration(self, logging_level=logging.INFO, filename='data/logs/bot_globalexam.log'):
|
||||
logging.basicConfig(filename=filename,
|
||||
level=logging_level,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging_level)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# Utilities methods
|
||||
def header():
|
||||
"""
|
||||
This function display an header when the script start
|
||||
"""
|
||||
const.init()
|
||||
logging.info('==\t=============================================================\t==')
|
||||
logging.info('==\t ' + const.APP_NAME + ' \t==')
|
||||
logging.info('==\t version : ' + const.VERSION + ' \t==')
|
||||
logging.info('==\t=============================================================\t==')
|
||||
|
||||
def wait_between( min, max):
|
||||
"""
|
||||
Wait random time in second beetween min and max seconds, to have an not linear behavior and be more human.
|
||||
"""
|
||||
rand=uniform(min, max)
|
||||
sleep(rand)
|
||||
|
||||
def TypeInField(element, xpath, myValue):
|
||||
"""Type in a field"""
|
||||
val = myValue
|
||||
elem = element.find_element(by=By.XPATH, value=xpath)
|
||||
for i in range(len(val)):
|
||||
elem.send_keys(val[i])
|
||||
wait_between(0.2, 0.4)
|
||||
wait_between(0.4, 0.7)
|
||||
|
||||
def element_exists(xpath, element, by=By.XPATH):
|
||||
"""
|
||||
Check if an element exist
|
||||
|
||||
:return: Boolean
|
||||
"""
|
||||
try:
|
||||
wait_between(2,3)
|
||||
element.find_element(by=by, value=xpath)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
0
data/logs/.gitkeep
Normal file
0
data/logs/.gitkeep
Normal file
0
data/profiles/.gitkeep
Normal file
0
data/profiles/.gitkeep
Normal file
49
main.py
Normal file
49
main.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from GlobalExamBot.helpers import Helpers
|
||||
from GlobalExamBot.database import create_table_sheets
|
||||
|
||||
from GlobalExamBot.driver import Driver
|
||||
from GlobalExamBot.bot import Bot
|
||||
|
||||
def main():
|
||||
try:
|
||||
helpers = Helpers()
|
||||
|
||||
# Configuration of the logging library
|
||||
helpers.logging_configuration()
|
||||
|
||||
# Load all configuration variables
|
||||
config = helpers.load_configuration()
|
||||
|
||||
logging.info('Starting bot ...')
|
||||
|
||||
create_table_sheets()
|
||||
|
||||
profile = f'prof_{config.username}'
|
||||
|
||||
logging.info(f'Username : {config.username}')
|
||||
|
||||
# Initialize driver and actions
|
||||
if config.noheadless :
|
||||
driver, action = Driver(profile).setup(headless=False)
|
||||
else:
|
||||
driver, action = Driver(profile).setup()
|
||||
|
||||
# Start bot actions
|
||||
Bot(driver, action, config).run()
|
||||
|
||||
except KeyboardInterrupt :
|
||||
if not helpers.ask_to_exit() :
|
||||
logging.info('Restart bot ...')
|
||||
driver.quit()
|
||||
main()
|
||||
else :
|
||||
logging.info('Bye bye !')
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
selenium==4.5.0
|
||||
Loading…
x
Reference in New Issue
Block a user