DEV Community

agenthustler
agenthustler

Posted on

Building a Grant and Funding Opportunity Tracker with Python

Building a Grant and Funding Opportunity Tracker with Python

Billions in grants go unclaimed yearly because organizations miss deadlines. A Python scraper can aggregate opportunities from Grants.gov, state agencies, and foundations into one searchable feed.

Core Grant Scraper

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass
from datetime import datetime
import sqlite3, json, re, time

@dataclass
class Grant:
    source: str
    title: str
    agency: str
    amount_min: float
    amount_max: float
    deadline: str
    url: str
    description: str
    category: str

class GrantTracker:
    def __init__(self, db_path='grants.db', api_key=None):
        self.db = sqlite3.connect(db_path)
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({'User-Agent': 'GrantTracker/1.0'})
        self._init_db()

    def _init_db(self):
        self.db.execute('''CREATE TABLE IF NOT EXISTS grants (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            source TEXT, title TEXT, agency TEXT,
            amount_min REAL, amount_max REAL,
            deadline TEXT, url TEXT UNIQUE,
            description TEXT, category TEXT,
            found_date TEXT, notified BOOLEAN DEFAULT 0
        )''')
        self.db.commit()

    def _fetch(self, url):
        if self.api_key:
            return self.session.get(
                f"http://api.scraperapi.com?api_key={self.api_key}&url={url}")
        return self.session.get(url)

    def scrape_grants_gov(self, keyword, pages=5):
        grants = []
        for p in range(1, pages+1):
            resp = self._fetch(
                f"https://www.grants.gov/search-grants?keywords={keyword}&page={p}")
            soup = BeautifulSoup(resp.text, 'html.parser')
            for item in soup.select('.grant-listing, .search-result-item'):
                title_el = item.select_one('.grant-title, h3 a')
                if not title_el:
                    continue
                text = item.get_text()
                amts = re.findall(r'\$[\d,]+', text)
                amts = [int(a.replace('$','').replace(',','')) for a in amts] or [0]
                grants.append(Grant(
                    source='grants.gov',
                    title=title_el.get_text(strip=True),
                    agency=self._text(item, '.agency-name'),
                    amount_min=min(amts), amount_max=max(amts),
                    deadline=self._text(item, '.deadline'),
                    url='https://www.grants.gov' + title_el.get('href',''),
                    description=self._text(item, '.description'),
                    category=keyword
                ))
            time.sleep(2)
        return grants

    def _text(self, el, sel):
        f = el.select_one(sel)
        return f.get_text(strip=True) if f else ''
Enter fullscreen mode Exit fullscreen mode

Smart Matching

class GrantMatcher:
    def __init__(self, db):
        self.db = db

    def find_matches(self, profile):
        q = "SELECT * FROM grants WHERE notified=0 AND deadline > date('now')"
        params = []
        if profile.get('min_amount'):
            q += ' AND amount_max >= ?'
            params.append(profile['min_amount'])
        if profile.get('max_amount'):
            q += ' AND amount_min <= ?'
            params.append(profile['max_amount'])
        if profile.get('categories'):
            ph = ','.join('?'*len(profile['categories']))
            q += f' AND category IN ({ph})'
            params.extend(profile['categories'])
        cursor = self.db.execute(q, params)
        cols = [d[0] for d in cursor.description]
        return [dict(zip(cols, r)) for r in cursor.fetchall()]
Enter fullscreen mode Exit fullscreen mode

Running the Tracker

def run(profile, keywords):
    tracker = GrantTracker(api_key='YOUR_KEY')
    matcher = GrantMatcher(tracker.db)
    for kw in keywords:
        grants = tracker.scrape_grants_gov(kw)
        print(f"{kw}: {len(grants)} found")
        time.sleep(3)
    matches = matcher.find_matches(profile)
    print(f"\n{len(matches)} grants match your profile")
    for g in matches:
        print(f"  {g['title']} - ${g['amount_min']:,.0f}-${g['amount_max']:,.0f}")
    return matches

profile = {'min_amount': 10000, 'max_amount': 500000,
           'categories': ['technology', 'education']}
run(profile, ['technology education', 'AI innovation'])
Enter fullscreen mode Exit fullscreen mode

Many grant portals use JavaScript and CAPTCHAs. ScraperAPI handles both automatically. ThorData provides proxy infrastructure for monitoring dozens of portals. Track uptime with ScrapeOps.


Follow for more Python automation tutorials.

Top comments (0)