Spaces:
Build error
Build error
Commit ·
8af87f7
1
Parent(s): f39bc7e
fix( ): add 5 scrapers, fix README links
Browse filesREADME FIX:
- Converted footer links to clickable markdown format
- Added PHASE_ROADMAP.md link
SCRAPERS RESTORED:
- scrapers/loksabha_scraper.py
- scrapers/sebi_scraper.py
- scrapers/electoral_bond_scraper.py
- scrapers/opensanctions_scraper.py
- scrapers/wikidata_scraper.py
- .env.example +17 -12
- README.md +5 -10
- scrapers/electoral_bond_scraper.py +72 -0
- scrapers/loksabha_scraper.py +112 -0
- scrapers/opensanctions_scraper.py +72 -0
- scrapers/sebi_scraper.py +91 -0
- scrapers/wikidata_scraper.py +74 -0
.env.example
CHANGED
|
@@ -1,17 +1,22 @@
|
|
| 1 |
-
#
|
| 2 |
-
#
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
NEO4J_PASSWORD=t3TxyBvDeGDXyuytyjbjjkCD8P6dwAj-XlHgVBBH4SOUxcdNcCrHI
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# OpenSanctions API (free at opensanctions.org)
|
| 15 |
-
OPENSANCTIONS_API_KEY=d86bbf9c96a76465cvclkjhgfd4f048860282
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
|
|
|
|
|
|
|
|
|
| 1 |
+
# BharatGraph Environment Variables
|
| 2 |
+
# Copy this file to .env and add your REAL values there
|
| 3 |
+
# This file uses placeholders only — NEVER put real keys here
|
| 4 |
+
# .env is git-ignored and stays only on your computer
|
| 5 |
|
| 6 |
+
# data.gov.in — register free at https://data.gov.in/user/register
|
| 7 |
+
DATAGOV_API_KEY=your_datagov_api_key_here
|
|
|
|
| 8 |
|
| 9 |
+
# Neo4j AuraDB — free at https://neo4j.com/cloud/platform/aura-graph-database
|
| 10 |
+
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
|
| 11 |
+
NEO4J_USER=neo4j
|
| 12 |
+
NEO4J_PASSWORD=your_generated_password_here
|
| 13 |
|
| 14 |
+
# OpenSanctions — free API key at https://opensanctions.org/api
|
| 15 |
+
OPENSANCTIONS_API_KEY=your_opensanctions_api_key_here
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# Copernicus Satellite — free at https://dataspace.copernicus.eu
|
| 18 |
+
COPERNICUS_USER=your_copernicus_username_here
|
| 19 |
+
COPERNICUS_PASSWORD=your_copernicus_password_here
|
| 20 |
|
| 21 |
+
# News API — optional, free at https://newsapi.org
|
| 22 |
+
NEWSAPI_KEY=your_newsapi_key_here
|
README.md
CHANGED
|
@@ -303,14 +303,9 @@ Pipeline 47 records processed and saved in 15 seconds
|
|
| 303 |
|
| 304 |
---
|
| 305 |
|
| 306 |
-
##
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
See `CONTRIBUTING.md`.
|
| 313 |
-
|
| 314 |
-
## Security
|
| 315 |
-
|
| 316 |
-
See `SECURITY.md`.
|
|
|
|
| 303 |
|
| 304 |
---
|
| 305 |
|
| 306 |
+
## Reference Documents
|
| 307 |
|
| 308 |
+
- [Contributing Guidelines](CONTRIBUTING.md)
|
| 309 |
+
- [Security Policy](SECURITY.md)
|
| 310 |
+
- [Phase Roadmap](PHASE_ROADMAP.md)
|
| 311 |
+
- [MIT License](LICENSE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scrapers/electoral_bond_scraper.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from scrapers.base_scraper import BaseScraper
|
| 5 |
+
from loguru import logger
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ElectoralBondScraper(BaseScraper):
|
| 9 |
+
|
| 10 |
+
ECI_URL = "https://www.eci.gov.in/electoral-bonds"
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
super().__init__(name="electoral_bond", delay=2.0)
|
| 14 |
+
|
| 15 |
+
def fetch_bond_data(self, save: bool = True) -> list:
|
| 16 |
+
logger.info("[ElectoralBond] Fetching bond disclosure data...")
|
| 17 |
+
html = self.get_html(self.ECI_URL)
|
| 18 |
+
bonds = []
|
| 19 |
+
if html:
|
| 20 |
+
bonds = self._parse_bond_page(html)
|
| 21 |
+
if not bonds:
|
| 22 |
+
logger.warning("[ElectoralBond] Live page returned no data — using sample")
|
| 23 |
+
bonds = self._get_sample_bonds()
|
| 24 |
+
if save and bonds:
|
| 25 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 26 |
+
filepath = f"data/samples/electoral_bonds_{timestamp}.json"
|
| 27 |
+
self.save_json(bonds, filepath)
|
| 28 |
+
logger.success(f"[ElectoralBond] Saved {len(bonds)} records")
|
| 29 |
+
return bonds
|
| 30 |
+
|
| 31 |
+
def _parse_bond_page(self, html: str) -> list:
|
| 32 |
+
soup = BeautifulSoup(html, "lxml")
|
| 33 |
+
bonds = []
|
| 34 |
+
for link in soup.find_all("a", href=True):
|
| 35 |
+
title = link.get_text(strip=True)
|
| 36 |
+
href = link.get("href", "")
|
| 37 |
+
if any(kw in title.lower() for kw in ["bond","donor","purchase","redemption"]):
|
| 38 |
+
bonds.append({
|
| 39 |
+
"title": title,
|
| 40 |
+
"url": href if href.startswith("http") else self.ECI_URL,
|
| 41 |
+
"source": "Election Commission of India",
|
| 42 |
+
"source_url": self.ECI_URL,
|
| 43 |
+
"scraped_at": datetime.now().isoformat(),
|
| 44 |
+
"entity_type": "electoral_bond_disclosure",
|
| 45 |
+
})
|
| 46 |
+
return bonds
|
| 47 |
+
|
| 48 |
+
def _get_sample_bonds(self) -> list:
|
| 49 |
+
return [
|
| 50 |
+
{
|
| 51 |
+
"bond_number": "EB001",
|
| 52 |
+
"purchaser_name": "SAMPLE CORPORATION LTD",
|
| 53 |
+
"denomination_crore": 1.0,
|
| 54 |
+
"purchase_date": "2023-04-05",
|
| 55 |
+
"redemption_date": "2023-04-12",
|
| 56 |
+
"redeemed_by": "SAMPLE POLITICAL PARTY",
|
| 57 |
+
"source": "Supreme Court ordered ECI disclosure (sample)",
|
| 58 |
+
"source_url": self.ECI_URL,
|
| 59 |
+
"scraped_at": datetime.now().isoformat(),
|
| 60 |
+
"entity_type": "electoral_bond",
|
| 61 |
+
},
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
print("=" * 55)
|
| 67 |
+
print("BharatGraph - Electoral Bond Scraper")
|
| 68 |
+
print("=" * 55)
|
| 69 |
+
scraper = ElectoralBondScraper()
|
| 70 |
+
bonds = scraper.fetch_bond_data(save=True)
|
| 71 |
+
print(f"\n Total records: {len(bonds)}")
|
| 72 |
+
print("\nDone!")
|
scrapers/loksabha_scraper.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from scrapers.base_scraper import BaseScraper
|
| 5 |
+
from loguru import logger
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LokSabhaScraper(BaseScraper):
|
| 9 |
+
|
| 10 |
+
BASE_URL = "https://loksabha.nic.in"
|
| 11 |
+
QUESTIONS_URL = "https://loksabha.nic.in/Questions/QResult15.aspx"
|
| 12 |
+
|
| 13 |
+
MINISTRIES = [
|
| 14 |
+
"Ministry of Finance",
|
| 15 |
+
"Ministry of Home Affairs",
|
| 16 |
+
"Ministry of Rural Development",
|
| 17 |
+
"Ministry of Health",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
super().__init__(name="loksabha", delay=3.0)
|
| 22 |
+
self.session.headers.update({
|
| 23 |
+
"User-Agent": (
|
| 24 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 25 |
+
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
|
| 26 |
+
),
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
def fetch_questions(self, ministry: str = "Ministry of Finance",
|
| 30 |
+
limit: int = 20) -> list:
|
| 31 |
+
logger.info(f"[LokSabha] Fetching questions for: {ministry}")
|
| 32 |
+
html = self.get_html(self.QUESTIONS_URL, params={"ministry": ministry})
|
| 33 |
+
if not html:
|
| 34 |
+
logger.warning("[LokSabha] Could not fetch page — using sample")
|
| 35 |
+
return self._get_sample_questions(ministry)
|
| 36 |
+
questions = self._parse_questions(html, ministry, limit)
|
| 37 |
+
if not questions:
|
| 38 |
+
return self._get_sample_questions(ministry)
|
| 39 |
+
return questions
|
| 40 |
+
|
| 41 |
+
def _parse_questions(self, html: str, ministry: str, limit: int) -> list:
|
| 42 |
+
soup = BeautifulSoup(html, "lxml")
|
| 43 |
+
questions = []
|
| 44 |
+
for row in soup.find_all("tr")[1:limit + 1]:
|
| 45 |
+
cols = row.find_all("td")
|
| 46 |
+
if len(cols) < 3:
|
| 47 |
+
continue
|
| 48 |
+
question = {
|
| 49 |
+
"question_number": cols[0].get_text(strip=True),
|
| 50 |
+
"subject": cols[1].get_text(strip=True),
|
| 51 |
+
"member_name": cols[2].get_text(strip=True) if len(cols) > 2 else "",
|
| 52 |
+
"ministry": ministry,
|
| 53 |
+
"session": "17th Lok Sabha",
|
| 54 |
+
"source": "Lok Sabha",
|
| 55 |
+
"source_url": self.BASE_URL,
|
| 56 |
+
"scraped_at": datetime.now().isoformat(),
|
| 57 |
+
"entity_type": "parliamentary_question",
|
| 58 |
+
}
|
| 59 |
+
if question["subject"]:
|
| 60 |
+
questions.append(question)
|
| 61 |
+
logger.info(f"[LokSabha] Parsed {len(questions)} questions")
|
| 62 |
+
return questions
|
| 63 |
+
|
| 64 |
+
def _get_sample_questions(self, ministry: str) -> list:
|
| 65 |
+
return [
|
| 66 |
+
{
|
| 67 |
+
"question_number": "Q001",
|
| 68 |
+
"subject": "Status of PM-KISAN scheme beneficiary verification",
|
| 69 |
+
"member_name": "SAMPLE MP",
|
| 70 |
+
"ministry": ministry,
|
| 71 |
+
"session": "17th Lok Sabha",
|
| 72 |
+
"source": "Lok Sabha (sample)",
|
| 73 |
+
"source_url": self.BASE_URL,
|
| 74 |
+
"scraped_at": datetime.now().isoformat(),
|
| 75 |
+
"entity_type": "parliamentary_question",
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"question_number": "Q002",
|
| 79 |
+
"subject": "Irregularities in MGNREGA fund disbursement",
|
| 80 |
+
"member_name": "SAMPLE MP",
|
| 81 |
+
"ministry": ministry,
|
| 82 |
+
"session": "17th Lok Sabha",
|
| 83 |
+
"source": "Lok Sabha (sample)",
|
| 84 |
+
"source_url": self.BASE_URL,
|
| 85 |
+
"scraped_at": datetime.now().isoformat(),
|
| 86 |
+
"entity_type": "parliamentary_question",
|
| 87 |
+
},
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
def fetch_all_ministries(self, save: bool = True) -> list:
|
| 91 |
+
all_questions = []
|
| 92 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 93 |
+
for ministry in self.MINISTRIES:
|
| 94 |
+
questions = self.fetch_questions(ministry, limit=10)
|
| 95 |
+
all_questions.extend(questions)
|
| 96 |
+
if save and all_questions:
|
| 97 |
+
filepath = f"data/samples/loksabha_questions_{timestamp}.json"
|
| 98 |
+
self.save_json(all_questions, filepath)
|
| 99 |
+
logger.success(f"[LokSabha] Saved {len(all_questions)} questions")
|
| 100 |
+
return all_questions
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
print("=" * 55)
|
| 105 |
+
print("BharatGraph - Lok Sabha Scraper")
|
| 106 |
+
print("=" * 55)
|
| 107 |
+
scraper = LokSabhaScraper()
|
| 108 |
+
questions = scraper.fetch_all_ministries(save=True)
|
| 109 |
+
print(f"\n Total: {len(questions)}")
|
| 110 |
+
if questions:
|
| 111 |
+
print(f" Example: {questions[0]['subject'][:60]}")
|
| 112 |
+
print("\nDone!")
|
scrapers/opensanctions_scraper.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from scrapers.base_scraper import BaseScraper
|
| 4 |
+
from loguru import logger
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class OpenSanctionsScraper(BaseScraper):
|
| 8 |
+
|
| 9 |
+
API_URL = "https://api.opensanctions.org/search/default"
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
super().__init__(name="opensanctions", delay=1.5)
|
| 13 |
+
self.api_key = os.getenv("OPENSANCTIONS_API_KEY", "")
|
| 14 |
+
if self.api_key:
|
| 15 |
+
self.session.headers.update({"Authorization": f"ApiKey {self.api_key}"})
|
| 16 |
+
else:
|
| 17 |
+
logger.warning("[OpenSanctions] No API key — add OPENSANCTIONS_API_KEY to .env")
|
| 18 |
+
|
| 19 |
+
def search_entity(self, name: str, schema: str = "Person") -> list:
|
| 20 |
+
logger.info(f"[OpenSanctions] Searching: '{name}'")
|
| 21 |
+
params = {"q": name, "schema": schema, "limit": 10}
|
| 22 |
+
data = self.get_json(self.API_URL, params=params)
|
| 23 |
+
if not data or "results" not in data:
|
| 24 |
+
return []
|
| 25 |
+
results = []
|
| 26 |
+
for r in data["results"]:
|
| 27 |
+
props = r.get("properties", {})
|
| 28 |
+
results.append({
|
| 29 |
+
"entity_id": r.get("id", ""),
|
| 30 |
+
"name": r.get("caption", name),
|
| 31 |
+
"schema": r.get("schema", schema),
|
| 32 |
+
"datasets": r.get("datasets", []),
|
| 33 |
+
"is_pep": "pep" in str(r.get("datasets", [])).lower(),
|
| 34 |
+
"is_sanctioned":any(d in ["us_ofac_sdn","un_sc_sanctions","eu_fsf"]
|
| 35 |
+
for d in r.get("datasets", [])),
|
| 36 |
+
"countries": props.get("country", []),
|
| 37 |
+
"positions": props.get("position", []),
|
| 38 |
+
"source": "OpenSanctions",
|
| 39 |
+
"source_url": "https://opensanctions.org",
|
| 40 |
+
"scraped_at": datetime.now().isoformat(),
|
| 41 |
+
"entity_type": "sanctions_check",
|
| 42 |
+
})
|
| 43 |
+
logger.success(f"[OpenSanctions] Found {len(results)} matches for '{name}'")
|
| 44 |
+
return results
|
| 45 |
+
|
| 46 |
+
def screen_entity_list(self, names: list) -> dict:
|
| 47 |
+
results = {}
|
| 48 |
+
for name in names:
|
| 49 |
+
matches = self.search_entity(name)
|
| 50 |
+
if any(m["is_sanctioned"] or m["is_pep"] for m in matches):
|
| 51 |
+
results[name] = matches
|
| 52 |
+
logger.warning(f"[OpenSanctions] FLAG: '{name}' in sanctions/PEP data")
|
| 53 |
+
else:
|
| 54 |
+
logger.info(f"[OpenSanctions] CLEAR: '{name}'")
|
| 55 |
+
return results
|
| 56 |
+
|
| 57 |
+
def fetch_and_save(self, names: list, save: bool = True) -> dict:
|
| 58 |
+
results = self.screen_entity_list(names)
|
| 59 |
+
if save and results:
|
| 60 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 61 |
+
self.save_json(results, f"data/samples/opensanctions_{timestamp}.json")
|
| 62 |
+
return results
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
print("=" * 55)
|
| 67 |
+
print("BharatGraph - OpenSanctions Scraper")
|
| 68 |
+
print("=" * 55)
|
| 69 |
+
scraper = OpenSanctionsScraper()
|
| 70 |
+
results = scraper.fetch_and_save(["Sample Person", "Test Corporation"], save=True)
|
| 71 |
+
print(f"\n Flagged: {len(results)}")
|
| 72 |
+
print("\nDone!")
|
scrapers/sebi_scraper.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from scrapers.base_scraper import BaseScraper
|
| 5 |
+
from loguru import logger
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class SEBIScraper(BaseScraper):
|
| 9 |
+
|
| 10 |
+
BASE_URL = "https://www.sebi.gov.in"
|
| 11 |
+
ORDERS_URL = "https://www.sebi.gov.in/enforcement/orders"
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
super().__init__(name="sebi", delay=2.0)
|
| 15 |
+
self.session.headers.update({
|
| 16 |
+
"User-Agent": (
|
| 17 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 18 |
+
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
|
| 19 |
+
),
|
| 20 |
+
})
|
| 21 |
+
|
| 22 |
+
def fetch_enforcement_orders(self, limit: int = 20) -> list:
|
| 23 |
+
logger.info("[SEBI] Fetching enforcement orders...")
|
| 24 |
+
html = self.get_html(self.ORDERS_URL)
|
| 25 |
+
if not html:
|
| 26 |
+
logger.warning("[SEBI] Could not fetch page — using sample")
|
| 27 |
+
return self._get_sample_orders()
|
| 28 |
+
orders = self._parse_orders(html, limit)
|
| 29 |
+
if not orders:
|
| 30 |
+
return self._get_sample_orders()
|
| 31 |
+
return orders
|
| 32 |
+
|
| 33 |
+
def _parse_orders(self, html: str, limit: int) -> list:
|
| 34 |
+
soup = BeautifulSoup(html, "lxml")
|
| 35 |
+
orders = []
|
| 36 |
+
for link in soup.find_all("a", href=True):
|
| 37 |
+
href = link.get("href", "")
|
| 38 |
+
title = link.get_text(strip=True)
|
| 39 |
+
if not title or len(title) < 10:
|
| 40 |
+
continue
|
| 41 |
+
if not any(kw in href.lower() for kw in ["order","adjudication","settlement"]):
|
| 42 |
+
continue
|
| 43 |
+
full_url = href if href.startswith("http") else self.BASE_URL + href
|
| 44 |
+
orders.append({
|
| 45 |
+
"title": title,
|
| 46 |
+
"url": full_url,
|
| 47 |
+
"order_type": "Enforcement Order",
|
| 48 |
+
"source": "SEBI",
|
| 49 |
+
"source_url": self.BASE_URL,
|
| 50 |
+
"scraped_at": datetime.now().isoformat(),
|
| 51 |
+
"entity_type": "regulatory_order",
|
| 52 |
+
})
|
| 53 |
+
if len(orders) >= limit:
|
| 54 |
+
break
|
| 55 |
+
logger.info(f"[SEBI] Parsed {len(orders)} orders")
|
| 56 |
+
return orders
|
| 57 |
+
|
| 58 |
+
def _get_sample_orders(self) -> list:
|
| 59 |
+
return [
|
| 60 |
+
{
|
| 61 |
+
"title": "Adjudication Order against Director for insider trading",
|
| 62 |
+
"url": "https://www.sebi.gov.in/enforcement/orders/sample-1",
|
| 63 |
+
"order_type": "Adjudication Order",
|
| 64 |
+
"violation": "Insider trading under SEBI (PIT) Regulations 2015",
|
| 65 |
+
"source": "SEBI (sample)",
|
| 66 |
+
"source_url": self.BASE_URL,
|
| 67 |
+
"scraped_at": datetime.now().isoformat(),
|
| 68 |
+
"entity_type": "regulatory_order",
|
| 69 |
+
},
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
def fetch_and_save(self, save: bool = True) -> list:
|
| 73 |
+
orders = self.fetch_enforcement_orders(limit=30)
|
| 74 |
+
if save and orders:
|
| 75 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 76 |
+
filepath = f"data/samples/sebi_orders_{timestamp}.json"
|
| 77 |
+
self.save_json(orders, filepath)
|
| 78 |
+
logger.success(f"[SEBI] Saved {len(orders)} orders")
|
| 79 |
+
return orders
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
print("=" * 55)
|
| 84 |
+
print("BharatGraph - SEBI Scraper")
|
| 85 |
+
print("=" * 55)
|
| 86 |
+
scraper = SEBIScraper()
|
| 87 |
+
orders = scraper.fetch_and_save(save=True)
|
| 88 |
+
print(f"\n Total orders: {len(orders)}")
|
| 89 |
+
if orders:
|
| 90 |
+
print(f" Example: {orders[0]['title'][:60]}")
|
| 91 |
+
print("\nDone!")
|
scrapers/wikidata_scraper.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from scrapers.base_scraper import BaseScraper
|
| 4 |
+
from loguru import logger
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class WikidataScraper(BaseScraper):
|
| 8 |
+
|
| 9 |
+
SPARQL_URL = "https://query.wikidata.org/sparql"
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
super().__init__(name="wikidata", delay=1.0)
|
| 13 |
+
self.session.headers.update({
|
| 14 |
+
"Accept": "application/sparql-results+json",
|
| 15 |
+
"User-Agent": "BharatGraph/0.1 (civic-transparency-platform)",
|
| 16 |
+
})
|
| 17 |
+
|
| 18 |
+
def enrich_politician(self, name: str) -> dict:
|
| 19 |
+
logger.info(f"[Wikidata] Enriching: '{name}'")
|
| 20 |
+
query = f"""
|
| 21 |
+
SELECT ?person ?birthDate ?educationLabel ?positionLabel ?partyLabel WHERE {{
|
| 22 |
+
?person wdt:P31 wd:Q5 .
|
| 23 |
+
?person rdfs:label "{name}"@en .
|
| 24 |
+
OPTIONAL {{ ?person wdt:P569 ?birthDate }}
|
| 25 |
+
OPTIONAL {{ ?person wdt:P69 ?education }}
|
| 26 |
+
OPTIONAL {{ ?person wdt:P39 ?position }}
|
| 27 |
+
OPTIONAL {{ ?person wdt:P102 ?party }}
|
| 28 |
+
SERVICE wikibase:label {{ bd:serviceParam wikibase:language "en" . }}
|
| 29 |
+
}}
|
| 30 |
+
LIMIT 5
|
| 31 |
+
"""
|
| 32 |
+
data = self.get_json(self.SPARQL_URL, params={"query": query, "format": "json"})
|
| 33 |
+
bindings = data.get("results",{}).get("bindings",[]) if data else []
|
| 34 |
+
if not bindings:
|
| 35 |
+
return self._empty_enrichment(name)
|
| 36 |
+
return {
|
| 37 |
+
"name": name,
|
| 38 |
+
"wikidata_id":bindings[0].get("person",{}).get("value","").split("/")[-1],
|
| 39 |
+
"birth_date": bindings[0].get("birthDate",{}).get("value",""),
|
| 40 |
+
"education": list({b.get("educationLabel",{}).get("value","")
|
| 41 |
+
for b in bindings if b.get("educationLabel")}),
|
| 42 |
+
"positions": list({b.get("positionLabel",{}).get("value","")
|
| 43 |
+
for b in bindings if b.get("positionLabel")}),
|
| 44 |
+
"party": bindings[0].get("partyLabel",{}).get("value",""),
|
| 45 |
+
"source": "Wikidata",
|
| 46 |
+
"source_url": "https://www.wikidata.org",
|
| 47 |
+
"scraped_at": datetime.now().isoformat(),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
def _empty_enrichment(self, name: str) -> dict:
|
| 51 |
+
return {"name": name, "wikidata_id": "", "birth_date": "",
|
| 52 |
+
"education": [], "positions": [], "party": "",
|
| 53 |
+
"source": "Wikidata", "source_url": "https://www.wikidata.org",
|
| 54 |
+
"scraped_at": datetime.now().isoformat()}
|
| 55 |
+
|
| 56 |
+
def enrich_entity_list(self, names: list, save: bool = True) -> list:
|
| 57 |
+
results = [self.enrich_politician(n) for n in names]
|
| 58 |
+
if save and results:
|
| 59 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 60 |
+
self.save_json(results, f"data/samples/wikidata_enrichment_{timestamp}.json")
|
| 61 |
+
logger.success(f"[Wikidata] Saved enrichment for {len(results)} entities")
|
| 62 |
+
return results
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
print("=" * 55)
|
| 67 |
+
print("BharatGraph - Wikidata Scraper")
|
| 68 |
+
print("=" * 55)
|
| 69 |
+
scraper = WikidataScraper()
|
| 70 |
+
results = scraper.enrich_entity_list(["Narendra Modi", "Rahul Gandhi"], save=True)
|
| 71 |
+
for r in results:
|
| 72 |
+
print(f"\n {r['name']}: ID={r['wikidata_id']} party={r['party']}")
|
| 73 |
+
print(f" positions={r['positions'][:2]}")
|
| 74 |
+
print("\nDone!")
|