Ich habe ein Python-Programm entwickelt, das den Bewerbungsprozess vollständig automatisiert, um meine Fähigkeiten im Bereich der Automatisierung zu demonstrieren. Als jemand, der alles liebt, was man automatisieren kann, war dieses Projekt eine perfekte Gelegenheit, meine technischen Kenntnisse und mein Interesse an Automatisierung zu zeigen. Das Programm selbst löst kein spezifisches Problem, sondern übernimmt den Bewerbungsprozess nach vorgegebenen Kriterien vollständig selbstständig. Es demonstriert meine Fähigkeit, komplexe Abläufe zu automatisieren und dabei verschiedene Technologien zu integrieren.

Das Programm durchsucht die Webseite der Bundesagentur für Arbeit nach Ausbildungsplätzen, indem es mit Python und Selenium den Browser steuert. Es stellt die Suche auf Ausbildungen ein und gibt die gewünschte Berufsrichtung sowie den Ort ein. Anschließend extrahiert das Programm alle Links der Suchergebnisse und durchläuft diese systematisch. Dabei sammelt es alle relevanten Daten zu den Stellenanzeigen und filtert sie nach bestimmten Kriterien. Zur Identifizierung von E-Mail-Adressen und Ansprechpartnern in den Anzeigen verwendet es Regex und greift bei Bedarf auf die OpenAI API zurück. Wenn alle Kriterien erfüllt sind, versendet das Programm automatisch eine Bewerbung per E-Mail über SMTP an die in der Stellenanzeige angegebene Adresse. Zu den verwendeten Technologien gehören Python, die OpenAI API, SMTP und Selenium.



< Ordnerstruktur />

Python_Bewerbung

│      bewerbung.pdf
│      blacklist.py
│      firmenspeicher.py
│      geckodriver.exe
│      gpt_creds.py
│      mail_creds.py
│      main.py



< main.py />

"""
Dateiname: main.py
Autor: Manuel Kilzer
Datum: 28. Juni 2024

Beschreibung:
	Dieses Skript automatisiert die Suche und Bewerbung für Stellenanzeigen für Fachinformatiker in Anwendungsentwicklung auf der Webseite der Arbeitsagentur. 
	Es durchsucht die Website gezielt nach Stellenangeboten, die den spezifischen Vorgaben und Kriterien entsprechen. Dabei extrahiert es relevante Informationen 
	wie Stellenbeschreibungen, Arbeitsort und Kontaktdaten von potenziellen Arbeitgebern. Nach der Extraktion filtert das Skript die gefundenen Stellenanzeigen 
	nach vordefinierten Kriterien und sendet dann automatisch Bewerbungen per E-Mail an die entsprechenden Arbeitgeber.

Abhängigkeiten:
	- geckodriver.exe (für Selenium mit Firefox) https://github.com/mozilla/geckodriver/releases
    - selenium
    - smtplib
    - openai
    - email
	- time
    - re
    - os

Copyright (c) 2024 Manuel Kilzer
"""

# Standardbibliotheken
import smtplib
import time
import re
import os

# E-Mail-Verarbeitung
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

# Drittanbieter-Bibliotheken
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.common.keys import Keys
from selenium import webdriver
import openai

# Benutzerdefinierte Module
import firmenspeicher as fs
import gpt_creds as gptc
import mail_creds as mc
import blacklist as bl

# Klasse für den Chat mit OpenAI
class ChatGPT:
    def __init__(self, api_key, rolle):
        openai.api_key = api_key
        self.dialog = [{"role" : "system", "content" : rolle}]

    def fragen(self, frage):
        self.dialog.append({"role" : "user", "content" : frage})
        response = openai.chat.completions.create(model = "gpt-3.5-turbo", messages = self.dialog)
        antwort = response.choices[0].message.content
        self.dialog.append({"role" : "assistant", "content" : antwort})
        return antwort

# Klasse für Stellenanzeigen
class Stellenanzeige():
	def __init__(self, linkAfA):
		self.stellenTyp = None
		self.stellenName = None
		self.stellenBeschreibung = None
		self.arbeitgeberName = None 
		self.arbeitgeberBeschreibung = None
		self.emailAdresse = None
		self.ansprechpartner = None
		self.ansprechpartnerAnsprache = None
		self.linkWebseite = None
		self.linkStellenAbgebot = linkAfA
		self.ort = None
		self.arbeitsOrt = None
		self.mindAbschluss = None
		self.afaReferenzNr = None

		print("=====================================================================================")
		print("Stellenangebot Link:", self.linkStellenAbgebot)

	def set_stellenTyp(self):
		try:
			self.stellenTyp = driver.find_element("id", "detail-kopfbereich-angebotsart").text
			print("Stellen-Typ:", self.stellenTyp)

		except Exception as error:
			print("Stellen-Typ: Keine Daten.", error)

	def set_stellenName(self):
		try:
			self.stellenName = driver.find_element("id", "detail-kopfbereich-titel").text
			print("Stellen-Name:", self.stellenName)

		except Exception as error:
			print("Stellen-Name: Keine Daten.", error)

	def set_stellenBeschreibung(self):
		try:
			self.stellenBeschreibung = driver.find_element("id", "detail-beschreibung-beschreibung").text
			print("Stellen-Beschreibung:", self.stellenBeschreibung)
			print()

		except Exception as error:
			print("Stellen-Beschreibung: Keine Daten.", error)
		
	def set_arbeitgeberName(self):
		try:
			self.arbeitgeberName = driver.find_element("id", "detail-kopfbereich-firma").text
			print("Arbeitgeber-Name:", self.arbeitgeberName)

		except Exception as error:
			print("Arbeitgeber-Name: Keine Daten.", error)

	def set_arbeitgeberBeschreibung(self):
		try:
			self.arbeitgeberBeschreibung = driver.find_element("id", "detail-agdarstellung-beschreibung").text
			print("Arbeitgeber-Beschreibung:", self.arbeitgeberBeschreibung)
			print()

		except Exception as error:
			print("Arbeitgeber-Beschreibung: Keine Daten.", error)

	def set_linkWebseite(self):
		try:
			self.linkWebseite = driver.find_element("id", "detail-agdarstellung-link-0").get_attribute("href")
			print("Weblink:", self.linkWebseite)

		except Exception as error:
			print("Weblink: Keine Daten.", error)

	# Auslesen des angegebenen Arbeitsorts aus dem Stellenangebot und zuweisung zur ort Variable.
	def set_ort(self):
		try:
			self.ort = driver.find_element("id", "detail-kopfbereich-arbeitsort").text
			print("Arbeits-Ort:", self.ort)

		except Exception as error:
			print("Arbeits-Ort: Keine Daten.", error)

	def set_arbeitsOrt(self):
		try:
			self.arbeitsOrt = driver.find_element("id", "detail-arbeitsorte-arbeitsort-0").text
			print("Arbeits-Adresse:", self.arbeitsOrt)

		except Exception as error:
			print("Arbeits-Adresse: Keine Daten.", error)

	def set_mindAbschluss(self):
		try:
			abschluss = driver.find_element("id", "detail-beschreibung-bildungsabschluss").text.replace("Benötigter Schulabschluss: ", "")
			if "hauptschul" in abschluss.lower():
				self.mindAbschluss = 1

			elif "mittlere" in abschluss.lower():
				self.mindAbschluss = 2

			elif "fachhochschul" in abschluss.lower():
				self.mindAbschluss = 3

			print("Geforderter Abschluss:", lesbareAbschluesse[self.mindAbschluss])

		except Exception as error:
			print("Geforderter Abschluss: Keine Daten.", error)

	def set_afaReferenzNr(self):
		try:
			self.afaReferenzNr = driver.find_element("id", "detail-footer-referenznummer").text
			print("Referenz-Nr.:", self.afaReferenzNr)

		except Exception as error:
			print("Referenz-Nr.: Keine Daten.", error)

	def search_emailAdresse(self):
		try:
			emailPattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
			email_matches = re.findall(emailPattern, self.stellenBeschreibung)
			if len(email_matches) >= 1:
				self.emailAdresse = email_matches[0]
			else:
				self.emailAdresse = None

			print("Email:", self.emailAdresse)

		except Exception as error:
			print("Email: Keine Daten.", error)

	def search_ansprechpartner(self):
		try:
			# Dieses RegEx-Muster selektiert Anreden ("Frau" oder "Herr/Herrn") und kann bis zu drei Namen erfassen.
			# Es wird verwendet, um mögliche Ansprechpartner im Text zu identifizieren. In seltenen Fällen sind andere
			# Namen im Text zu finden, was zu einer möglichen falschen Ansprache der Bezugsperson führen kann, an die die 
			# Bewerbung gehen soll. Sollte mit RegEx kein Name gefunden werden, wird versucht, mit GPT einen Namen heraus-
			# zu filtern.
			ansprechPattern = r"(Frau|Herrn?)\s(([A-Z][a-z]+)(\s[A-Z][a-z]+)?(\s[A-Z][a-z]+)?)"

			if self.stellenBeschreibung:
				if self.emailAdresse:
					re_ergebnis = re.search(ansprechPattern, self.stellenBeschreibung)

					if re_ergebnis:
						if "fr" in re_ergebnis.group(1).lower():
							self.ansprechpartnerAnsprache = "Frau"

						elif "he" in re_ergebnis.group(1).lower():
							self.ansprechpartnerAnsprache = "Herr"

						self.ansprechpartner = re_ergebnis.group(2)

					else:
						ansprechpartner_Antwort = gpt_Ansprechpartner.fragen(self.stellenBeschreibung.replace("\n", " ")).replace(".", "").replace(",", "")
						print("\tAnsprechpartner GPT:", ansprechpartner_Antwort)

						if ansprechpartner_Antwort != "None":
							self.ansprechpartner = ansprechpartner_Antwort
							geschlecht_Antwort = gpt_Geschlecht.fragen(self.ansprechpartner).replace(".", "").replace(",", "")
							print("\tGeschlecht GPT:", geschlecht_Antwort)

							if geschlecht_Antwort != "None":
								self.ansprechpartnerAnsprache = geschlecht_Antwort

					self.ansprechpartner = self.ansprechpartner.replace("\n", "")
					self.ansprechpartnerAnsprache = self.ansprechpartnerAnsprache.replace("\n", "")

			print("Ansprechpartner:", self.ansprechpartner)
			print("Ansprechpartner Geschlecht:", self.ansprechpartnerAnsprache)

		except Exception as error:
			print("Ansprechpartner: Keine Daten.", error)

	def set_AlleDaten(self):
		self.set_stellenTyp()
		self.set_stellenName()
		self.set_stellenBeschreibung()
		self.set_arbeitgeberName()
		self.set_arbeitgeberBeschreibung()
		self.set_linkWebseite()
		self.set_ort()
		self.set_arbeitsOrt()
		self.set_mindAbschluss()
		self.set_afaReferenzNr()
		self.search_emailAdresse()
		self.search_ansprechpartner()


# Funktion zum Entfernen des Cookie-Fensters der Webseite.
def entferneCookieFenster():
	actions = ActionChains(driver)
	actions.send_keys(Keys.ENTER)
	actions.perform()

# Funktion zum Wechseln zur Ausbildungssuche in der Suchmaske.
def wechsleZurAusbildungsSuche():
	dropdownAuswahl = driver.find_element("id", "Angebotsart-dropdown-button")
	dropdownAuswahl.click()
	time.sleep(1)
	auswahlAusbildungen = driver.find_element("id", "Angebotsart-dropdown-item-1")
	auswahlAusbildungen.click()

# Funktion gibt in die Suchmaske ein WAS gesucht wird.
def wasEingabe(text):
	was = driver.find_element("id", "was-input")
	was.send_keys(text)

# Funktion gibt in die Suchmaske ein WO gesucht wird.
def woEingabe(text):
	wo = driver.find_element("id", "wo-input")
	wo.send_keys(text)

# Funktion klickt in der Suchmaske auf den "Stellen finden" Button und löst somit die Suche aus.
def stellenFindenClick():
	findenButton = driver.find_element("id", "btn-stellen-finden")
	findenButton.click()

# Funktion definiert den Ablauf der Eingabe der Daten für WAS und WO.
def sucheNachAusbildungsstellen(was, wo):
	wasEingabe(was)
	time.sleep(1)
	woEingabe(wo)
	time.sleep(1)
	stellenFindenClick()

# Funktion wechselt die Ansicht der angezeigten Suchergebnisse zur Listenansicht.
def wechsleZurListenAnsicht():
	listenAnsichtButton = driver.find_element("id", "listen-layout-button")
	listenAnsichtButton.click()

# Funktion sortiert die Suchergebnisse nach Entfernung.
def wechsleSortierungNachEntfernung():
	dropDownSortierung = driver.find_element("id", "sortierung-dropdown-button")
	dropDownSortierung.click()

# Die Funktion scrollt 4 Mal nach unten, um an den unteren Bildrand zu kommen, wo der Link zum Laden weiterer Ergebnisse
# ist. Danach werden alle Links der Stellenangebote ausgelesen und in einer Liste (links = []) gespeichert.
# Sie gibt in der Konsole aus, wie viele Links sie insgesamt gefunden hat. Zurückgegeben wird die liste (links).
def fetchResultLinks():
	for x in range(4):
		try:
			driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # Scrolle runter.
			time.sleep(1)
			weitereErgebnisseButton = driver.find_element("id", "ergebnisliste-ladeweitere-button")

			if weitereErgebnisseButton:
				weitereErgebnisseButton.click()

			time.sleep(1)

		except Exception as error:
			# Bei dieser Exception soll nichts weiter passieren wenn sie fällt. 
			# Ist einfach nur wenn bereits ganz nach unten gescrollt wurde.
			print("Scroll down.", error)
			
	quellText = driver.page_source
	word_count = quellText.lower().count("ergebnisliste-item".lower()) # Zählt die Links.

	links = []
	count = 0
	for x in range(word_count):
		try:
			href = driver.find_element("id", "ergebnisliste-item-" + str(count)).get_attribute("href") # Lese Link aus.
			links.append(href) # Füge Link der liste links hinzu.
			count += 1

		except Exception as error:
			# Sollte der driver das Element nicht finden können.
			print("fetchResultLinks find_element Fehler.", error)
			break

	print()
	print("Gesamt:", len(links))
	return links

# Email-Versand mit Anhang
def send_email(empfaenger, betreff, email_text, pdf_filename):
	try:
		# E-Mail-Konfiguration
		sender_email = mc.mail
		sender_passwort = mc.pw
		receiver_email = empfaenger
		subject = betreff
		body = email_text

		# E-Mail erstellen
		message = MIMEMultipart()
		message["From"] = sender_email
		message["To"] = receiver_email
		message["Subject"] = subject
		message.attach(MIMEText(body, "plain"))

		# PDF-Datei als Anhang hinzufügen
		attachment = open(pdf_filename, "rb")

		part = MIMEBase("application", "octet-stream")
		part.set_payload(attachment.read())
		encoders.encode_base64(part)
		part.add_header("Content-Disposition", f"attachment; filename= {pdf_filename}")

		message.attach(part)

		# Verbindung zum SMTP-Server herstellen
		with smtplib.SMTP_SSL(mc.server, mc.port) as server:
			server.login(sender_email, sender_passwort)

			# E-Mail senden
			server.sendmail(sender_email, receiver_email, message.as_string())

		print("E-Mail erfolgreich versendet!")
		return True

	except Exception as error:
		print("send_email()", error)
		return False

liste_Firmen = fs.firmen # Liste von Firmen und E-Mailadressen an die bereits eine Bewerbung raus ist.
lesbareAbschluesse = {1 : "Hauptschulabschluss", 2 : "Mittlere Reife", 3 : "Fachhochschulreife"}

if __name__ == "__main__":
	os.system("cls") # Leeren der Konsolenausgabe
	
	try:
		gpt_Ansprechpartner = ChatGPT(gptc.key, gptc.rolle_Ansprechpartner) # Speziell auf ermittlung von Ansprechpartnern getrimmte GPT-Prompts.
		gpt_Geschlecht = ChatGPT(gptc.key, gptc.rolle_Geschlecht) # Speziell auf Ermittlung der Anspsprache (Geschlecht) getrimmte GPT-Prompts.

		optionen = Options()
		optionen.headless = False # False = Browser wird angezeigt
		driver = webdriver.Firefox(options = optionen) # Starte Firefox
		driver.set_window_position(10, 10) # Position des Browserfensters im Hauptbildschirm von oben links
		driver.set_window_size(1500, 1100) # bxh

		# Alle Links der Stellenangebote crawlen
		driver.get("https://www.arbeitsagentur.de/jobsuche/suche")
		time.sleep(4)
		entferneCookieFenster()
		time.sleep(2)
		wechsleZurAusbildungsSuche()
		time.sleep(1)
		sucheNachAusbildungsstellen("Fachinformatiker Anwendungsentwicklung", "70565")
		time.sleep(1)
		wechsleZurListenAnsicht()
		time.sleep(0.5)
		links = fetchResultLinks()

		# Alle Stellenanzeigen crawlen
		stellenangebote = []
		for link in links:
			driver.get(link)
			time.sleep(2)

			stellenangebot = Stellenanzeige(link)
			stellenangebot.set_AlleDaten()
			if stellenangebot.emailAdresse != None: # Füge Stellenangebot nur hinzu wenn eine Emailadresse enthalten ist.
				stellenangebote.append(stellenangebot)
			
			print()
			print()

		print(len(stellenangebote))
		print(stellenangebote)
		print()
		print()

		# Hier werden die Stellenangebote darauf überprüft ob bereits schon Bewerbungen an diese raus sind.
		# Ebenfalls wird überprüft ob die Firmen in einer Liste sind, an die keine Bewerbungen raus sollen.
		for stelle in stellenangebote:
			if stelle.emailAdresse not in liste_Firmen and stelle.arbeitgeberName not in liste_Firmen: # Überprüft ob Firma oder E-Mail bereits angeschrieben wurde.
				block  = False
				for word in bl.words:
					if word in stelle.stellenBeschreibung:
						block = True
						print("## BLOCK ##", word)

				for firma in bl.firmen:
					if firma in stelle.arbeitgeberName or firma in stelle.stellenBeschreibung: # Prüfung ob Firma unerwünscht ist.
						block = True
						print("## BLOCK ##", firma)

				if block != True:
					print("============================================")
					print("Firmenname:", stelle.arbeitgeberName)
					print("E-Mailadresse:", stelle.emailAdresse)

					# Überprüfen ob ein Ansprechpartner gefunden wurde und eine passende Ansprache dazu.
					if stelle.ansprechpartner != None or stelle.ansprechpartnerAnsprache != None:
						# Konstruieren des Ansprachenblocks (Beispiel: Herr Max Mustermann)
						gesamtAnsprechpartner = stelle.ansprechpartnerAnsprache + " " + stelle.ansprechpartner

						# Konstruieren des Ansprachensatzes
						if "Herr" in stelle.ansprechpartnerAnsprache:
							gesamtAnsprache = "Sehr geehrter " + gesamtAnsprechpartner + ","
						
						elif "Frau" in stelle.ansprechpartnerAnsprache:
							gesamtAnsprache = "Sehr geehrte " + gesamtAnsprechpartner + ","
						
						else:
							gesamtAnsprache = "Sehr geehrte Damen und Herren,"

						print("Ansprache:", gesamtAnsprache)

					else:
						gesamtAnsprache = "Sehr geehrte Damen und Herren,"
						print("Ansprache:", gesamtAnsprache)

					# Konstruieren der E-Mail
					emailText = ""
					grussSignatur = "Beste Grüße\nManuel Kilzer\n\n\nHaeckerstraße 2\n70565 Stuttgart\n0174/4335483\nmanuel@kilzer.dev\nhttps://www.kilzer.dev"

					emailAdresse = stelle.emailAdresse
					emailBetreff = "Bewerbung als Praktikant im Rahmen einer Umschulung zum Fachinformatiker Anwendungsentwicklung."
					emailGesamtText = gesamtAnsprache + "\n\n" + emailText + "\n\n" + grussSignatur
					emailAnhang = "bewerbung.pdf"
					
					# Versende E-Mail und füge Firma der Liste (liste_Firmen) hinzu um doppelte E-Mails zu vermeiden.
					send_email(emailAdresse, emailBetreff, emailGesamtText, emailAnhang)
					liste_Firmen.extend([stelle.arbeitgeberName, stelle.emailAdresse])

			print()
			print()

	except Exception as error:
		print("Hauptprogramm Fehler!", error)

	finally:
		# Schliessen des Browsers
		if driver:
			driver.quit()

		# Schreibe eine aktualisierte data.py
		if liste_Firmen:
			lines = ["firmen = [\n"]
			for firma in liste_Firmen:
				lines.append('"' + firma + '"' + ",\n")
			lines.append("]")
			with open("data.py", "w") as py_file:
				py_file.writelines(lines)

		exit()
^^

< mail_creds.py />

# Hier die Daten für SMTP von deinem Mailserver eingeben.
server = "web311.dogado.net"
port = 465
email = "###__- max@mustermailer.com -__###"
pw = "###__- Dein Passwort für SMTP -__###"
^^

< gpt_creds.py />

# Folgende drei Angaben sind manuell hier einzutragen.
key = "###__- Hier der API-Schlüssel von OpenAI -__###"
rolle_Ansprechpartner = """Hier Steht ein Prompt für eine Rolle zum Filtern von Ansprechpartnern aus Texten."""
rolle_Geschlecht = """Hier steht ein Prompt für eine Rolle der Geschlechter zu Namen ermitteln soll. """
^^

< blacklist.py />

# Die Liste "firmen" wird manuell mit Firmennamen befüllt, bei denen man sich nicht bewerben möchte. Alles lowercase!
firmen = ["musterfirma ag", "alter_arbeitgeber gmbh", "schlechter_arbeitgeber gbr", "usw ..."]

# Die Liste "words" wird manuell mit Stichwörtern befüllt, die man im Zusammenhang mit der Stellenanzeige nicht gerne lesen möchte.
# Sie wird verwendet, um Wörter abzugleichen und Stellenangebote herauszufiltern. Alles lowercase!
words = ["beispiele", "internet explorer", "worst practise", "obstkorb", "deadcode", "usw ..."]
^^

< firmenspeicher.py />

# Die Liste "firmen" wird vom Hauptprogramm befüllt und die Datei "firmenspeicher.py" immer überschrieben.
# Sie enthält später Firmennamen und E-Mail-Adressen von Firmen, bei denen sich bereits beworben wurde, um doppelte Bewerbungen zu vermeiden.
firmen = []
^^





















© 2024 Manuel Kilzer. Alle Rechte vorbehalten.

Impressum