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 ''
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()]
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'])
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)