Last active
September 23, 2024 13:42
-
-
Save sketchybear/ac07b0bd3b9501e4dfd83364db7b2956 to your computer and use it in GitHub Desktop.
This fairly simple script uses Selenium to check a website for stocks of ADHD meds within a postcode of the UK.
This file contains hidden or 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
from selenium import webdriver | |
from selenium.webdriver.common.by import By | |
from selenium.webdriver.support.ui import Select, WebDriverWait | |
from selenium.webdriver.support import expected_conditions as EC | |
from selenium.webdriver.chrome.service import Service | |
from bs4 import BeautifulSoup | |
import time | |
import re | |
from twilio.rest import Client # Import Twilio client | |
import sys # Import sys to update the progress bar | |
from datetime import datetime | |
import pytz # To handle UK time | |
# Import Twilio credentials and phone numbers from the partial | |
from twilio_config import TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER, TARGET_PHONE_NUMBERS | |
# Set the correct path to ChromeDriver | |
chrome_driver_path = '/usr/bin/chromedriver' | |
# Set up Chromium options to run in headless mode (no GUI) | |
chrome_options = webdriver.ChromeOptions() | |
chrome_options.add_argument('--headless') # Run headless | |
chrome_options.add_argument('--no-sandbox') | |
chrome_options.add_argument('--disable-dev-shm-usage') | |
# Set up the ChromeDriver service with the correct path | |
service = Service(chrome_driver_path) | |
# Initialize Twilio client with credentials from twilio_config.py | |
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) | |
# Specify the parameters for the search | |
target_location = "TOWN" # Replace with your desired location | |
target_postcode = "POSTCODE" # Replace with your desired postcode | |
target_medicine = "Concerta XL" # Replace with your desired medicine | |
target_dosage = "18mg" # Replace with your desired dosage | |
# ANSI escape codes for formatting (for terminal use) | |
RED = "\033[91m" | |
GREEN = "\033[92m" | |
YELLOW = "\033[93m" | |
RESET = "\033[0m" | |
# Progress bar function with countdown timer | |
def progress_bar(duration): | |
total_steps = 50 # Number of steps in the progress bar | |
for i in range(duration): | |
time_remaining = duration - i | |
minutes, seconds = divmod(time_remaining, 60) | |
progress = int((i / duration) * total_steps) | |
bar = "[" + "=" * progress + " " * (total_steps - progress) + "]" | |
sys.stdout.write(f"\r{bar} {minutes:02}:{seconds:02} remaining until next run...") | |
sys.stdout.flush() | |
time.sleep(1) | |
sys.stdout.write("\r" + " " * 80 + "\r") # Clear the progress bar line after it's done | |
# Check if the current time is between 8 AM and 6 PM UK time, and it's not Sunday | |
def is_within_operating_hours(): | |
uk_timezone = pytz.timezone('Europe/London') | |
current_time_uk = datetime.now(uk_timezone) | |
start_time = current_time_uk.replace(hour=8, minute=0, second=0, microsecond=0) | |
end_time = current_time_uk.replace(hour=18, minute=0, second=0, microsecond=0) | |
# Check if it's Sunday (6 is Sunday in Python's weekday() where Monday = 0) | |
if current_time_uk.weekday() == 6: | |
return False | |
# Check if current time is between 8 AM and 6 PM | |
return start_time <= current_time_uk <= end_time | |
# Log errors in errors.txt | |
def log_error(error_message): | |
with open("errors.txt", "a") as error_file: | |
error_file.write(f"{datetime.now()}: {error_message}\n") | |
# Function to perform the check | |
def check_results(): | |
try: | |
print(f"Starting stock search for {target_location} with {target_medicine} ({target_dosage})...") | |
# Initialize the WebDriver for Chromium | |
driver = webdriver.Chrome(service=service, options=chrome_options) | |
# Set up explicit wait | |
wait = WebDriverWait(driver, 10) # Wait for up to 10 seconds | |
# Open the website | |
driver.get("https://www.adhd.directory/stock-checker") | |
print("Website has loaded ...") | |
# Step 1: Wait for the accordion element to be clickable and click it to reveal the fields | |
accordion = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".flex.justify-between.items-center"))) | |
accordion.click() | |
print("Form unfolded, ready for action ...") | |
# Step 2: Wait for the medication dropdown to be present and select the target medicine | |
medication_dropdown = wait.until(EC.presence_of_element_located((By.ID, "medication"))) | |
select_medication = Select(medication_dropdown) | |
select_medication.select_by_visible_text(target_medicine) | |
print(f"Medication '{target_medicine}' selected ...") | |
# Step 3: Wait for the target dosage button to be clickable and click it | |
dosage_button = wait.until(EC.element_to_be_clickable((By.XPATH, f"//button[text()='{target_dosage}']"))) | |
dosage_button.click() | |
print(f"'{target_dosage}' dosage selected ...") | |
# Step 4: Wait for the postcode field to be visible, then enter the target postcode | |
postal_code_input = wait.until(EC.presence_of_element_located((By.ID, "store-search"))) | |
postal_code_input.send_keys(target_postcode) | |
print(f"Postcode '{target_postcode}' has been entered ...") | |
# Step 5: Wait for the submit button using its class and click it | |
submit_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".w-fit.rounded-md.border.border-gray-200.py-1.px-2.transition-colors.bg-indigo-500.text-white.text-xs"))) | |
submit_button.click() | |
print("Submit button clicked, herding llamas ...") | |
# Step 6: Wait for results and fetch all divs with class containing "shadow-md" | |
result_divs = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div[class*='shadow-md']"))) | |
print("Results and llamas gathered ...") | |
# Step 7: Filter results that contain the target location and remove images or non-text elements | |
location_results = [] | |
for div in result_divs: | |
if target_location.lower() in div.text.lower(): # Match target location (case insensitive) | |
# Use BeautifulSoup to parse the div content and remove images and other non-text elements | |
soup = BeautifulSoup(div.get_attribute('innerHTML'), 'html.parser') | |
# Remove all <img>, <svg>, and other non-text elements, especially GIFs | |
for tag in soup(['img', 'svg', 'video', 'picture']): | |
tag.decompose() | |
# Cleaned text | |
cleaned_text = soup.get_text().strip() | |
# Remove non-ASCII characters (like emojis) using regex | |
cleaned_text = re.sub(r'[^\x00-\x7F]+', '', cleaned_text) | |
# Highlight 'out of stock' in red and 'in stock' in green | |
cleaned_text = cleaned_text.replace("out of stock", RED + "out of stock" + RESET) | |
cleaned_text = cleaned_text.replace("in stock", GREEN + "in stock" + RESET) | |
# Append cleaned and formatted text | |
location_results.append(cleaned_text) | |
# Print results in the terminal | |
print(f"\n--- Search Results for {target_location} ---") | |
for result in location_results: | |
print(result) | |
print(f"--- End of Results for {target_location} ---\n") | |
# Only send an SMS if there are in-stock items | |
in_stock_items = [result for result in location_results if "out of stock" not in result.lower() and "in stock" in result.lower()] | |
if in_stock_items: | |
message_body = f"In stock at the following locations in {target_location}:\n" + "\n".join(in_stock_items) | |
# Send SMS to each phone number in the list | |
for phone_number in TARGET_PHONE_NUMBERS: | |
client.messages.create( | |
body=message_body, | |
from_=TWILIO_PHONE_NUMBER, | |
to=phone_number | |
) | |
print(f"Twilio message sent to {phone_number} for in-stock items.") # Log to terminal | |
# Close the driver after you're done | |
driver.quit() | |
except Exception as e: | |
# Log any errors to errors.txt and continue running | |
log_error(str(e)) | |
print(f"An error occurred: {e}. Logged to errors.txt") | |
# Run the check every hour between 8 AM and 6 PM UK time, excluding Sundays | |
while True: | |
if is_within_operating_hours(): | |
print(f"Within operating hours (8 AM - 6 PM, Monday to Saturday). Running the check for {target_location} ...") | |
check_results() | |
else: | |
# Yellow message for outside operating hours or Sunday | |
print(f"{YELLOW}Outside operating hours or it's Sunday. Waiting for the next valid time slot.{RESET}") | |
print("Waiting for 30 mins before checking again ...") | |
progress_bar(1800) # 30 minutes in seconds |
This file contains hidden or 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
# twilio_config.py | |
TWILIO_ACCOUNT_SID = '###########' # Replace with your Account SID | |
TWILIO_AUTH_TOKEN = '#############' # Replace with your Auth Token | |
TWILIO_PHONE_NUMBER = '############' # Replace with your Twilio number | |
TARGET_PHONE_NUMBERS = ["############"] # UK numbers, you can comma separate multiple numbers |
Added some error handling and moved the town, postcode and meds to parameters to make it easier to manage.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now updated with: