🕷️ Scraping NAFDAC Greenbook from A to Z: How I Extracted 8,600+ Official Records into CSV

Author: Idris Abdulhamood  |  Category: Web Scraping, Data Engineering & Automation

The NAFDAC Greenbook (greenbook.nafdac.gov.ng) is the official public portal for all approved drugs, medical devices, herbals, and biologics registered by the Nigerian government.

For data analysts, researchers, or software engineers, having this database in a clean, structured .csv format opens up endless possibilities—from fraud detection systems to market research.

However, getting this data off the website and into an Excel sheet wasn't as simple as running pandas.read_html(). Here is the complete journey of how I scraped the entire portal from A to Z—including the wall I hit, why fast scripts failed, and the solution that successfully captured all 8,608 official entries.


🛑 Phase 1: The Trap – Why Standard Scraping Failed

My first approach was the standard Python scraping toolkit: requests and BeautifulSoup.

The strategy seemed straightforward: iterate through pages 1 to 901, download the HTML table rows, parse <td> elements, and output a DataFrame:

import requests
from bs4 import BeautifulSoup

# The naive approach
res = requests.get("https://greenbook.nafdac.gov.ng/?page=1")
soup = BeautifulSoup(res.text, 'html.parser')
table = soup.find('table')
print(table)  # Result: Found table header, but ZERO table rows!

The Output: ✅ SUCCESS! Downloaded and saved 0 official NAFDAC entries to 'nafdac_registered_drugs.csv'

💡 The Root Cause

The NAFDAC Greenbook uses client-side JavaScript (DataTables). When a web scraper sends a standard HTTP request, the server returns an empty HTML shell. The browser normally runs JavaScript after load to pull data via AJAX requests and render the rows dynamically. Since standard Python requests cannot execute JavaScript, it only saw an empty table frame!

⚡ Phase 2: The Need for Speed (And Why Multithreading Still Failed)

I tried boosting speed using concurrent.futures.ThreadPoolExecutor with 20 parallel workers across 901 pages.

 Progress: Downloaded 50 / 901 pages | Captured 0 entries...
 Progress: Downloaded 100 / 901 pages | Captured 0 entries...
 ...
 Progress: Downloaded 901 / 901 pages | Captured 0 entries...

While the script ran all 901 requests in 30 seconds, it still captured 0 entries. Speed doesn't matter if your HTTP client lacks a JavaScript execution engine!

🛠️ Phase 3: The Breakthrough – Headless Browser Automation

To get the actual rendered data, I switched to a Headless Chromium Browser using Selenium. A headless browser spins up a real Chrome instance in memory, executes all embedded JavaScript, populates the DataTables DOM, and allows full table inspection.

💡 Optimization Trick: Dropdown Batching

Instead of clicking through 901 separate pages at 10 rows per page, I configured Selenium to find the DataTables length dropdown (<select name="entries">) and change the display value to 100 entries per page. This reduced total page interactions from 901 down to ~90!

💻 The Working Production Code

Here is the complete Python script used to scrape the entire database:

# =====================================================================
# NAFDAC GREENBOOK HEADLESS SCRAPER ENGINE
# =====================================================================
import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import Select

# 1. Initialize Headless Chrome Environment
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')

driver = webdriver.Chrome(options=chrome_options)

url = "https://greenbook.nafdac.gov.ng/"
driver.get(url)
time.sleep(5)  # Allow DOM & DataTables JS to load completely

# 2. Expand Table Pagination to 100 Rows Per Page
try:
    select_elem = driver.find_element(By.NAME, "entries")
    select = Select(select_elem)
    select.select_by_value("100")
    time.sleep(3)
except Exception:
    pass

all_records = []
page = 1

# 3. Extract Rendered DOM Rows Page-by-Page
while True:
    rows = driver.find_elements(By.XPATH, "//table/tbody/tr")
    
    for row in rows:
        cols = [col.text.strip() for col in row.find_elements(By.TAG_NAME, "td")]
        if len(cols) >= 4 and cols[0] != "No matching records found":
            all_records.append(cols)

    print(f"Page {page} processed | Captured {len(all_records)} total entries...", flush=True)

    # Click 'Next' button until pagination ends
    try:
        next_btn = driver.find_element(By.XPATH, "//a[contains(text(), 'Next') or contains(@class, 'next')]")
        if "disabled" in next_btn.get_attribute("class"):
            break
        next_btn.click()
        time.sleep(1.2)
        page += 1
    except Exception:
        break

driver.quit()

# 4. Standardize Output Schema
standard_keys = [
    'Product Name', 'Active Ingredients', 'Category', 'NRN', 
    'Form', 'ROA', 'Strengths', 'Applicant Name', 'Approval Date', 'Status'
]

cleaned_rows = []
for item in all_records:
    row_dict = {standard_keys[i]: item[i] for i in range(min(len(item), len(standard_keys)))}
    cleaned_rows.append(row_dict)

# 5. Clean Data & Export CSV
df_full = pd.DataFrame(cleaned_rows)
df_full = df_full.dropna(subset=['NRN']).drop_duplicates(subset=['NRN'])
df_full.to_csv("nafdac_registered_drugs.csv", index=False)

print(f"\n✅ SUCCESS! Rendered and saved {len(df_full)} live entries to 'nafdac_registered_drugs.csv'")

📊 Phase 4: Final CSV Audit & Data Structure

The output file nafdac_registered_drugs.csv contained 8,608 unique records, fully cleaned with 0 duplicate Registration Numbers (NRNs).

Product Category Total Records Captured
Herbals and Nutraceuticals 1,739
Veterinary Products 1,729
Medical Devices 1,727
Drugs & Pharmaceuticals 1,718
Vaccines and Biologics 1,695
TOTAL DATASET SIZE 8,608

💡 Key Takeaways for Web Scraping Dynamic Websites

  • Check for JavaScript Rendering First: Always inspect the page source vs rendered DOM elements before picking your Python scraping stack.
  • HTTP Request Libraries Have Limits: When DataTables or JS frameworks populate tables dynamically, tools like Selenium or Playwright are mandatory.
  • Optimize Display Limits: Check if the target portal allows displaying 50 or 100 entries per page. This significantly reduces total network calls and pagination clicks.
  • Deduplicate at the Source: Dropping duplicates based on unique primary keys (like NRNs) ensures high data hygiene right out of the box.

Comments

Popular posts from this blog