Skip to content

Instantly share code, notes, and snippets.

@rohithreddy
Created June 20, 2026 08:04
Show Gist options
  • Select an option

  • Save rohithreddy/325cdab8161d0aed705c52dac891e43d to your computer and use it in GitHub Desktop.

Select an option

Save rohithreddy/325cdab8161d0aed705c52dac891e43d to your computer and use it in GitHub Desktop.
Nettlio sample work
#!/usr/bin/env python3
import json
import os
import re
def clean_drug_name(name):
if not name:
return ""
# Strip common professional and consumer page title suffixes
name = re.sub(r':\s*(Package Insert|Prescribing Info|Prescribing Information|FDA prescribing|Professional|Consumer|Side Effects|Uses|Dosage|Warnings).*$', '', name, flags=re.IGNORECASE)
# Strip parenthetical route/form suffixes
name = re.sub(r'\s*\((Oral|Topical|Otic|Ophthalmic|Inhalation|Injection|Rectal|Sublingual|Vaginal|Nasal|Systemic|Intravenous|Intramuscular)\)$', '', name, flags=re.IGNORECASE)
# General fallback for any remaining colon with metadata keywords
if ":" in name:
parts = name.split(":", 1)
after = parts[1].lower()
if any(w in after for w in ["info", "insert", "prescribing", "professional", "monograph", "uses", "guidelines", "side effect", "dosage"]):
name = parts[0].strip()
return name.strip()
def main():
input_file = "drugs_data.json"
if not os.path.exists(input_file):
print(f"Error: {input_file} not found.")
return
with open(input_file, "r", encoding="utf-8") as f:
try:
data = json.load(f)
except Exception as e:
print(f"Error reading JSON: {e}")
return
print(f"Loaded {len(data)} records from {input_file}.")
normalized_data = {}
for record in data:
url = record.get("url")
if not url:
continue
# Normalize the drug name
orig_name = record.get("drug_name", "")
new_name = clean_drug_name(orig_name)
record["drug_name"] = new_name
# Dedup: if URL is seen multiple times, keep the latest one or merge/override
normalized_data[url] = record
# Sort alphabetically by drug_name (case-insensitive) and then by url
sorted_records = sorted(
normalized_data.values(),
key=lambda x: (x.get("drug_name", "").lower(), x.get("url", ""))
)
# Save to a temporary file first
temp_file = input_file + ".tmp"
with open(temp_file, "w", encoding="utf-8") as f:
json.dump(sorted_records, f, indent=2, ensure_ascii=False)
os.replace(temp_file, input_file)
print(f"Normalized and deduplicated. Saved {len(sorted_records)} records to {input_file}.")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Drugs.com Scraper Script
Scrapes drug information from drugs.com for drugs starting with letters K to U.
Saves data in drugs_data.json.
"""
import os
import sys
import re
import json
import time
import random
import logging
import argparse
from urllib.parse import urljoin, urlparse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
# Configure Logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger("drugs_scraper")
def init_driver():
"""Initializes and returns a headless Selenium Chrome WebDriver."""
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1920,1080")
# Set a realistic user agent
chrome_options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
# Exclude automation switches
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=chrome_options)
# Execute CDP script to prevent webdriver detection
driver.execute_cdp_cmd(
"Page.addScriptToEvaluateOnNewDocument",
{
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
}
)
return driver
def load_existing_data(output_file):
"""Loads existing drugs from the output JSON file if it exists."""
if os.path.exists(output_file):
try:
with open(output_file, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
logger.info(f"Loaded {len(data)} existing records from {output_file}.")
return data
except Exception as e:
logger.warning(f"Could not load existing data from {output_file}: {e}. Starting fresh.")
return []
def save_data(data, output_file):
"""Saves the crawled drug records to the JSON output file in sorted order."""
try:
# Sort data alphabetically by drug_name, then by url
sorted_data = sorted(data, key=lambda x: (x.get("drug_name", "").lower(), x.get("url", "")))
# Save to a temporary file first to avoid corrupting data on interrupt
temp_file = output_file + ".tmp"
with open(temp_file, "w", encoding="utf-8") as f:
json.dump(sorted_data, f, indent=2, ensure_ascii=False)
os.replace(temp_file, output_file)
except Exception as e:
logger.error(f"Failed to save data to {output_file}: {e}")
def extract_field_text(b_tag):
"""Extracts text following a bold label tag in a paragraph until <br> or <b>."""
if not b_tag:
return ""
text_parts = []
curr = b_tag.next_sibling
while curr:
if curr.name in ["br", "b", "strong"]:
break
part = curr.get_text() if curr.name else str(curr)
text_parts.append(part)
curr = curr.next_sibling
return "".join(text_parts).strip()
def clean_generic_name(val):
"""Cleans phonetic pronunciations and format metadata from generic name."""
# Remove text inside [...] (phonetic pronunciation)
val = re.sub(r'\[.*?\]', '', val)
# Remove text inside (...) (qualifiers like oral/injection)
val = re.sub(r'\(.*?\)', '', val)
# Clean up multiple spaces
val = re.sub(r'\s+', ' ', val)
return val.strip()
def clean_brand_name(brand):
"""Cleans brand name prefix and dynamic toggle texts."""
# Remove "of <generic> include(s):" prefix if it exists
brand = re.sub(r'^of\s+.*?\s+include[s]?:\s*', '', brand, flags=re.IGNORECASE)
# Remove "... show all X brands" text
brand = re.sub(r'\.{0,3}\s*show all \d+\s*brands', '', brand, flags=re.IGNORECASE)
# Clean leading/trailing punctuation and spaces
brand = brand.strip(" ,\n\r\t.")
return brand
def clean_drug_name(name):
"""Normalizes the drug_name to the drug's name only."""
if not name:
return ""
# Strip common professional and consumer page title suffixes
name = re.sub(r':\s*(Package Insert|Prescribing Info|Prescribing Information|FDA prescribing|Professional|Consumer|Side Effects|Uses|Dosage|Warnings).*$', '', name, flags=re.IGNORECASE)
# Strip parenthetical route/form suffixes
name = re.sub(r'\s*\((Oral|Topical|Otic|Ophthalmic|Inhalation|Injection|Rectal|Sublingual|Vaginal|Nasal|Systemic|Intravenous|Intramuscular)\)$', '', name, flags=re.IGNORECASE)
# General fallback for any remaining colon with metadata keywords
if ":" in name:
parts = name.split(":", 1)
after = parts[1].lower()
if any(w in after for w in ["info", "insert", "prescribing", "professional", "monograph", "uses", "guidelines", "side effect", "dosage"]):
name = parts[0].strip()
return name.strip()
def parse_drug_html(html, url):
"""Parses drug page HTML and extracts relevant structured fields."""
soup = BeautifulSoup(html, "html.parser")
# 1. Drug Name (H1)
h1 = soup.find("h1")
raw_name = h1.get_text(strip=True) if h1 else ""
drug_name = clean_drug_name(raw_name)
# Locate subtitles elements inside the metadata paragraph
b_generic = soup.find(lambda tag: tag.name in ["b", "strong"] and "generic name" in tag.text.lower())
b_brands = soup.find(lambda tag: tag.name in ["b", "strong"] and any(term in tag.text.lower() for term in ["brand name", "brand names"]))
b_class = soup.find(lambda tag: tag.name in ["b", "strong"] and "drug class" in tag.text.lower())
# 2. Generic Name
generic_raw = extract_field_text(b_generic)
generic_name = clean_generic_name(generic_raw) if generic_raw else ""
# 3. Brand Names (array)
brands_raw = extract_field_text(b_brands)
brand_names = []
if brands_raw:
for b in brands_raw.split(","):
cleaned = clean_brand_name(b)
if cleaned:
brand_names.append(cleaned)
# 4. Drug Class (comma-separated string)
drug_class = extract_field_text(b_class)
# Clean multiple spaces and formatting
drug_class = re.sub(r'\s+', ' ', drug_class).strip()
# 5. Related Conditions (array)
related_conditions = []
# Method A: Search under "Related treatment guides" section
h_guides = soup.find(
lambda tag: tag.name in ["h2", "h3", "h4"]
and any(term in tag.text.lower() for term in ["related treatment guides", "related conditions", "related treatment"])
)
if h_guides:
curr = h_guides.find_next_sibling()
# Scan next few siblings to find the <ul> list
for _ in range(4):
if not curr:
break
if curr.name == "ul":
for li in curr.find_all("li"):
cond_text = li.get_text(strip=True)
if cond_text and cond_text not in related_conditions:
related_conditions.append(cond_text)
break
curr = curr.find_next_sibling()
# Method B: Fallback to searching data-type="drug-condition"
if not related_conditions:
for a in soup.find_all("a", attrs={"data-type": "drug-condition"}):
cond_text = a.get_text(strip=True)
if cond_text and cond_text not in related_conditions:
related_conditions.append(cond_text)
# Method C: Fallback to searching lists of conditions in specific sections (strictly matching 'conditions')
if not related_conditions:
# Search inside ul class containing 'conditions'
cond_ul = soup.find("ul", class_=re.compile(r"conditions"))
if cond_ul:
for li in cond_ul.find_all("li"):
cond_text = li.get_text(strip=True)
if cond_text and cond_text not in related_conditions:
related_conditions.append(cond_text)
return {
"drug_name": drug_name,
"url": url,
"drug_class": drug_class,
"generic_name": generic_name,
"brand_names": brand_names,
"related_conditions": related_conditions
}
def crawl_alphabet_page(driver, url):
"""Crawls an alphabet letter index page to extract drug links and subpage pagination links."""
logger.info(f"Crawling alphabet index page: {url}")
driver.get(url)
# Give it a moment to render
time.sleep(1)
html = driver.page_source
soup = BeautifulSoup(html, "html.parser")
# Locate main content container to avoid layout leakage
content_div = soup.find("div", class_="ddc-main-content")
if not content_div:
content_div = soup
# 1. Find drug links
drug_links = set()
ul_lists = content_div.find_all("ul", class_=re.compile("ddc-list-column|ddc-list-unstyled"))
for ul in ul_lists:
for li in ul.find_all("li"):
a = li.find("a")
if a and a.get("href"):
href = a.get("href").strip()
abs_url = urljoin(url, href)
parsed = urlparse(abs_url)
if parsed.netloc == "www.drugs.com":
path = parsed.path.lower()
if path.endswith(".html"):
exclude_patterns = [
"/alpha/", "/support/", "/answers/", "/medical-answers/",
"/newsletters/", "/podcasts/", "sitemap.html", "privacy.html",
"terms.html", "about.html", "contact.html", "feedback.html"
]
if not any(pat in path for pat in exclude_patterns):
drug_links.add(abs_url)
# 2. Find subpage links (pagination)
subpage_links = set()
paging_anchors = soup.find_all("a", class_="ddc-paging-item")
for anchor in paging_anchors:
href = anchor.get("href")
if href:
abs_url = urljoin(url, href)
subpage_links.add(abs_url)
return list(drug_links), list(subpage_links)
def main():
parser = argparse.ArgumentParser(description="Drugs.com Selenium Scraper (Letters K-U)")
parser.add_argument("--start-letter", default="k", help="Letter to start scraping from (e.g. k)")
parser.add_argument("--end-letter", default="u", help="Letter to end scraping at (e.g. u)")
parser.add_argument("--output", default="drugs_data.json", help="Path to output JSON file")
parser.add_argument("--delay", type=float, default=1.5, help="Average delay between page requests in seconds")
parser.add_argument("--test", action="store_true", help="Run in test mode (scrapes very few drugs to verify correctness)")
args = parser.parse_args()
start_char = args.start_letter.lower()
end_char = args.end_letter.lower()
output_file = args.output
base_delay = args.delay
is_test = args.test
# Define alphabet letters to crawl
all_letters = [chr(i) for i in range(ord('a'), ord('z') + 1)]
try:
start_idx = all_letters.index(start_char)
end_idx = all_letters.index(end_char)
target_letters = all_letters[start_idx : end_idx + 1]
except ValueError:
logger.error(f"Invalid start or end letter. Using defaults (k to u).")
target_letters = [chr(i) for i in range(ord('k'), ord('u') + 1)]
logger.info(f"Target letters for crawl: {target_letters}")
if is_test:
logger.info("TEST MODE ENABLED. Crawl will be restricted to a small subset.")
target_letters = [target_letters[0]] # Only first letter
# Load existing data (checkpoint)
crawled_data = load_existing_data(output_file)
crawled_urls = {item["url"] for item in crawled_data}
# Initialize driver
driver = init_driver()
try:
# Phase 1: Gather or load all drug links
discovered_file = "discovered_links.json"
all_drug_urls = set()
# In test mode, we bypass caching to allow dynamic subset runs
if not is_test and os.path.exists(discovered_file):
try:
with open(discovered_file, "r", encoding="utf-8") as f:
links_list = json.load(f)
if isinstance(links_list, list):
all_drug_urls = set(links_list)
logger.info(f"Loaded {len(all_drug_urls)} discovered drug links from {discovered_file}. Skipping index crawling.")
except Exception as e:
logger.warning(f"Could not load discovered links from {discovered_file}: {e}. Crawling index.")
if not all_drug_urls:
logger.info("--- PHASE 1: GATHERING DRUG LINKS ---")
for letter in target_letters:
letter_url = f"https://www.drugs.com/alpha/{letter}.html"
try:
drug_links, subpage_links = crawl_alphabet_page(driver, letter_url)
logger.info(f"Letter '{letter}' main page: Found {len(drug_links)} drug links and {len(subpage_links)} subpages.")
# Add drug links from main page
all_drug_urls.update(drug_links)
# If test mode, limit subpages
if is_test:
subpage_links = subpage_links[:1]
logger.info(f"Test mode: Limited to {len(subpage_links)} subpages.")
# Crawl each subpage
for subpage_url in subpage_links:
logger.info(f"Crawling subpage: {subpage_url}")
sub_drug_links, _ = crawl_alphabet_page(driver, subpage_url)
logger.info(f" Found {len(sub_drug_links)} drug links on subpage.")
all_drug_urls.update(sub_drug_links)
# Polite delay between subpage requests
time.sleep(base_delay + random.uniform(0.5, 1.5))
if is_test:
break
except Exception as e:
logger.error(f"Error crawling letter index for '{letter}': {e}")
# Save discovered links cache (if not in test mode)
if not is_test and all_drug_urls:
try:
with open(discovered_file, "w", encoding="utf-8") as f:
json.dump(list(all_drug_urls), f, indent=2, ensure_ascii=False)
logger.info(f"Saved {len(all_drug_urls)} discovered drug links to {discovered_file}.")
except Exception as e:
logger.error(f"Failed to save discovered links to {discovered_file}: {e}")
# Filter out already crawled URLs
urls_to_crawl = [url for url in all_drug_urls if url not in crawled_urls]
logger.info(f"Total unique drug links discovered: {len(all_drug_urls)}")
logger.info(f"Already crawled: {len(crawled_urls)}")
logger.info(f"Remaining to crawl: {len(urls_to_crawl)}")
if is_test:
urls_to_crawl = urls_to_crawl[:3]
logger.info(f"Test mode: Restricting remaining links to crawl to {len(urls_to_crawl)} items.")
# Phase 2: Crawl each drug page and extract details
logger.info("--- PHASE 2: CRAWLING DRUG DETAILS ---")
success_count = 0
failed_urls = []
# Helper function to crawl a list of URLs
def crawl_urls_list(urls, attempt=1):
nonlocal success_count
next_failed = []
for idx, drug_url in enumerate(urls, 1):
logger.info(f"[Attempt {attempt} - {idx}/{len(urls)}] Scraping drug: {drug_url}")
try:
driver.get(drug_url)
# Polite delay for dynamic elements to load
time.sleep(base_delay + random.uniform(0.5, 1.5))
html = driver.page_source
record = parse_drug_html(html, drug_url)
if record["drug_name"]:
# Ensure we update crawled_data in-place
crawled_data_dict = {item["url"]: item for item in crawled_data}
crawled_data_dict[drug_url] = record
crawled_data[:] = list(crawled_data_dict.values())
crawled_urls.add(drug_url)
success_count += 1
logger.info(f" Successfully parsed: {record['drug_name']}")
# Save progress increment (every 5 records, or always in test mode)
if success_count % 5 == 0 or is_test:
save_data(crawled_data, output_file)
logger.info(" Saved checkpoint data to disk.")
else:
logger.warning(f" Could not extract drug name from {drug_url}. Adding to failed list.")
next_failed.append(drug_url)
except Exception as e:
logger.error(f" Failed to scrape {drug_url}: {e}")
next_failed.append(drug_url)
# Random sleep delay
time.sleep(base_delay + random.uniform(0.5, 1.5))
return next_failed
# First pass
failed_urls = crawl_urls_list(urls_to_crawl, attempt=1)
# Retry passes
max_attempts = 2
for attempt in range(2, max_attempts + 2):
if not failed_urls or is_test:
break
logger.info(f"--- RETRY PASS {attempt-1} FOR {len(failed_urls)} FAILED URLS ---")
# Extra wait before retry pass
time.sleep(5)
failed_urls = crawl_urls_list(failed_urls, attempt=attempt)
# Final save
save_data(crawled_data, output_file)
# Verification Summary
logger.info("--- SCRAPING VERIFICATION SUMMARY ---")
final_crawled_urls = {item["url"] for item in crawled_data}
# Discovered URLs could include crawled ones already, so let's combine sets
missing_urls = all_drug_urls - final_crawled_urls
logger.info(f"Total Unique Drugs Discovered in Index: {len(all_drug_urls)}")
logger.info(f"Total Drugs Successfully Scraped & Saved: {len(crawled_data)}")
logger.info(f"Total Drugs Missing/Failed: {len(missing_urls)}")
if len(missing_urls) == 0:
logger.info("VERIFICATION SUCCESS: All discovered drugs have been successfully scraped and saved!")
else:
logger.warning(f"VERIFICATION PARTIAL: Scraped {len(crawled_data)} of {len(all_drug_urls)} drugs. {len(missing_urls)} failed permanently.")
logger.warning("Failed URLs:")
for url in sorted(missing_urls):
logger.warning(f" - {url}")
finally:
driver.quit()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment