diff --git a/.gitignore b/.gitignore index f4a6d8d..bf0f43a 100644 --- a/.gitignore +++ b/.gitignore @@ -140,6 +140,7 @@ dmypy.json # User files output.csv +output*.csv venv8/ quickstart.py states.json \ No newline at end of file diff --git a/README.md b/README.md index b4af5b0..e5e0206 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,22 @@ username: # Insert your username here password: # Insert your password here positions: -- # Position you want to search for +- # positions you want to search for - # Another position you want to search for - # A third position you want to search for locations: -- # Location you want to search in +- # Location you want to search for - # A second location you want to search in + +cover_letter_loctn: +- # '/home/PATH_TO_FILE' + +output_filename: +- # PATH TO OUTPUT FILE (default output.csv) + +blacklist: +- # Company names you want to ignore ``` __NOTE: AFTER EDITING SAVE FILE, DO NOT COMMIT FILE__ diff --git a/config.yaml b/config.yaml index c549cd7..655292f 100644 --- a/config.yaml +++ b/config.yaml @@ -1,11 +1,20 @@ -username: # Insert your username here -password: # Insert your password here +username: +password: positions: -- # Position you want to search for +- # positions you want to search for - # Another position you want to search for - # A third position you want to search for locations: -- # Location you want to search in -- # A second location you want to search in \ No newline at end of file +- # Location you want to search for +- # A second location you want to search in + +cover_letter_loctn: +- # '/home/PATH_TO_FILE' + +output_filename: +- # PATH TO OUTPUT FILE (default output.csv) + +blacklist: +- # Company names you want to ignore \ No newline at end of file diff --git a/easyapplybot.py b/easyapplybot.py index d85e135..d1a35ac 100644 --- a/easyapplybot.py +++ b/easyapplybot.py @@ -1,4 +1,4 @@ -import time, random, os, csv, datetime, platform +import time, random, os, csv, platform from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.common.exceptions import TimeoutException @@ -15,6 +15,7 @@ from webdriver_manager.chrome import ChromeDriverManager import re import yaml +from datetime import datetime, timedelta driver = webdriver.Chrome(ChromeDriverManager().install()) @@ -23,31 +24,44 @@ class EasyApplyBot: MAX_SEARCH_TIME = 10*60 - blacklist = ["Staffigo"] - def __init__(self, username, password, language, resumeloctn=None, filename='output.csv'): + + def __init__(self, + username, + password, + cover_letter_loctn=None, + filename='output.csv', + blacklist=[]): print("\nWelcome to Easy Apply Bot\n") dirpath = os.getcwd() print("current directory is : " + dirpath) - self.resumeloctn = resumeloctn - self.language = language - self.appliedJobIDs = self.get_appliedIDs(filename) + self.cover_letter_loctn = cover_letter_loctn + self.appliedJobIDs = self.get_appliedIDs(filename) if self.get_appliedIDs(filename) != None else [] self.filename = filename self.options = self.browser_options() self.browser = driver self.wait = WebDriverWait(self.browser, 30) - self.start_linkedin(username,password) + self.blacklist = blacklist + self.start_linkedin(username, password) def get_appliedIDs(self, filename): + print(filename) try: df = pd.read_csv(filename, header=None, - names=['timestamp', 'jobID', 'job', 'company', 'attempted', 'result']) - return list(df.jobID) + names=['timestamp', 'jobID', 'job', 'company', 'attempted', 'result'], + lineterminator='\n', + encoding = 'utf-8') + + df['timestamp'] = pd.to_datetime(df['timestamp'], format="%Y-%m-%d %H:%M:%S.%f") + df = df[df['timestamp'] > (datetime.now() - timedelta(days=2))] + jobIDs = list(df.jobID) + print(f"{len(jobIDs)} jobIDs found") + return jobIDs except Exception as e: print(str(e) + " jobIDs could not be loaded from CSV {}".format(filename)) return None @@ -77,24 +91,12 @@ def start_linkedin(self,username,password): except TimeoutException: print("TimeoutException! Username/password field or login button not found") - def wait_for_login(self): - - - time.sleep(1) - - while True: - if self.browser.title != title: - print("\nStarting LinkedIn bot\n") - break - else: - time.sleep(1) - print("\nPlease Login to your LinkedIn account\n") def fill_data(self): self.browser.set_window_size(0, 0) self.browser.set_window_position(2000, 2000) - print(self.resumeloctn) + print(self.cover_letter_loctn) def start_apply(self, positions, locations): start = time.time() @@ -110,6 +112,8 @@ def start_apply(self, positions, locations): print(f"Applying to {position}: {location}") location = "&location=" + location self.applications_loop(position, location) + if len(combos) > 20: + break self.finish_apply() def applications_loop(self, position, location): @@ -156,16 +160,18 @@ def applications_loop(self, position, location): IDs = set(IDs) # remove already applied jobs + before = len(IDs) jobIDs = [x for x in IDs if x not in self.appliedJobIDs] + after = len(jobIDs) - if len(jobIDs) == 0: + + if len(jobIDs) == 0 and len(IDs) > 24: jobs_per_page = jobs_per_page + 25 count_job = 0 self.avoid_lock() self.browser, jobs_per_page = self.next_jobs_page(position, location, jobs_per_page) - # loop over IDs to apply for i, jobID in enumerate(jobIDs): count_job += 1 @@ -194,7 +200,7 @@ def applications_loop(self, position, location): print(f'\n\n********count_application: {count_application}************\n\n') print(f"Time for a nap - see you in:{int(sleepTime/60)} min") print('\n\n****************************************\n\n') - time.sleep (sleepTime) + time.sleep(sleepTime) # go to new page if all jobs are done if count_job == len(jobIDs): @@ -210,6 +216,7 @@ def applications_loop(self, position, location): if len(jobIDs) == 0 or i == (len(jobIDs) - 1): break + def write_to_file(self, button, jobID, browserTitle, result): def re_extract(text, pattern): target = re.search(pattern, text) @@ -217,7 +224,7 @@ def re_extract(text, pattern): target = target.group(1) return target - timestamp = datetime.datetime.now() + timestamp = datetime.now() attempted = False if button == False else True job = re_extract(browserTitle.split(' | ')[0], r"\(?\d?\)?\s?(\w.*)") company = re_extract(browserTitle.split(' | ')[1], r"(\w.*)" ) @@ -227,14 +234,6 @@ def re_extract(text, pattern): writer = csv.writer(f) writer.writerow(toWrite) - def get_job_links(self, page): - links = [] - for link in page.find_all('a'): - url = link.get('href') - if url: - if '/jobs/view' in url: - links.append(url) - return set(links) def get_job_page(self, jobID): #root = 'www.linkedin.com' @@ -244,18 +243,6 @@ def get_job_page(self, jobID): self.job_page = self.load_page(sleep=0.5) return self.job_page - def got_easy_apply(self, page): - #button = page.find("button", class_="jobs-apply-button artdeco-button jobs-apply-button--top-card artdeco-button--3 ember-view") - - button = self.browser.find_elements_by_xpath( - '//button[contains(@class, "jobs-apply")]/span[1]' - ) - EasyApplyButton = button [0] - if EasyApplyButton.text in "Easy Apply" : - return EasyApplyButton - else : - return False - def get_easy_apply_button(self): try : @@ -269,20 +256,6 @@ def get_easy_apply_button(self): return EasyApplyButton - def easy_apply_xpath(self): - button = self.get_easy_apply_button() - button_inner_html = str(button) - list_of_words = button_inner_html.split() - next_word = [word for word in list_of_words if "ember" in word and "id" in word] - ember = next_word[0][:-1] - xpath = '//*[@'+ember+']/button' - return xpath - - def click_button(self, xpath): - triggerDropDown = self.browser.find_element_by_xpath(xpath) - time.sleep(0.5) - triggerDropDown.click() - time.sleep(1) def send_resume(self): def is_present(button_locator): @@ -290,31 +263,44 @@ def is_present(button_locator): button_locator[1])) > 0 try: - time.sleep(3) + time.sleep(random.uniform(1.5, 2.5)) #print(f"Navigating... ") - next_locater = (By.CSS_SELECTOR, "button[aria-label='Continue to next step']") - review_locater = (By.CSS_SELECTOR, "button[aria-label='Review your application']") - submit_locater = (By.CSS_SELECTOR, "button[aria-label='Submit application']") - submit_application_locator = (By.CSS_SELECTOR, "button[aria-label='Submit application']") - error_locator = (By.CSS_SELECTOR, "p[data-test-form-element-error-message='true']") - + next_locater = (By.CSS_SELECTOR, + "button[aria-label='Continue to next step']") + review_locater = (By.CSS_SELECTOR, + "button[aria-label='Review your application']") + submit_locater = (By.CSS_SELECTOR, + "button[aria-label='Submit application']") + submit_application_locator = (By.CSS_SELECTOR, + "button[aria-label='Submit application']") + error_locator = (By.CSS_SELECTOR, + "p[data-test-form-element-error-message='true']") + cover_letter = (By.CSS_SELECTOR, "input[name='file']") + submitted = False while True: + + # Upload Cover Letter if possible + if is_present(cover_letter): + input_button = self.browser.find_elements(cover_letter[0], + cover_letter[1]) + + input_button[0].send_keys(self.cover_letter_loctn) + time.sleep(random.uniform(4.5, 6.5)) + + # Click Next or submitt button if possible button = None - #self.browser.find_element_by_xpath('//*[@id="file-browse-input"]').send_keys(self.resumeloctn) - for i, button_locator in enumerate([next_locater, review_locater, submit_locater, submit_application_locator]): - #print(i) + buttons = [next_locater, review_locater, + submit_locater, submit_application_locator] + for i, button_locator in enumerate(buttons): if is_present(button_locator): - #print("button found") button = self.wait.until(EC.element_to_be_clickable(button_locator)) - + if is_present(error_locator): for element in self.browser.find_elements(error_locator[0], error_locator[1]): text = element.text if "Please enter a valid answer" in text: - #print("Error Found") - #print(element.get_attribute('class')) button = None break if button: @@ -341,7 +327,7 @@ def is_present(button_locator): except Exception as e: print(e) print("cannot apply to this job") - #raise(e) + raise(e) return submitted @@ -394,9 +380,19 @@ def finish_apply(self): assert parameters['username'] is not None assert parameters['password'] is not None + + print(parameters) + cover_letter_loctn = parameters.get('cover_letter_loctn', [None])[0] + output_filename = parameters.get('output_filename', ['output.csv'])[0] + blacklist = parameters.get('blacklist', []) + bot = EasyApplyBot(parameters['username'], parameters['password'], - parameters['locations'] + cover_letter_loctn=cover_letter_loctn, + filename=output_filename, + blacklist=blacklist ) - bot.start_apply(parameters['positions'], parameters['locations']) \ No newline at end of file + locations = [l for l in parameters['locations'] if l != None] + positions = [p for p in parameters['positions'] if p != None] + bot.start_apply(positions, locations) \ No newline at end of file