Coding-With-Bashir commited on
Commit
ade7922
·
verified ·
1 Parent(s): 0a5b9ed

Upload .\src\data_collection\igihe_scraper.py with huggingface_hub

Browse files
.//src//data_collection//igihe_scraper.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Igihe news scraper for Kinyarwanda content."""
2
+
3
+ import json
4
+ import logging
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import requests
10
+ from bs4 import BeautifulSoup
11
+
12
+ from data_collection.web_utils import check_robots_txt
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IgiheScraper:
18
+ """Scrapes Kinyarwanda news content from Igihe.com."""
19
+
20
+ def __init__(self, output_dir: str, config: dict[str, Any]):
21
+ self.output_dir = Path(output_dir)
22
+ self.output_dir.mkdir(parents=True, exist_ok=True)
23
+ self.config = config
24
+ self.session = requests.Session()
25
+ self.session.headers.update({
26
+ "User-Agent": "BwengeAi/0.1 (Kinyarwanda AI Research; contact: info@bwengeai.rw)"
27
+ })
28
+
29
+ igihe_config = config.get("igihe", {})
30
+ self.base_url = igihe_config.get("base_url", "https://igihe.com")
31
+ self.categories = igihe_config.get("categories", ["amakuru", "politiki", "ubuzima"])
32
+ self.rate_limit = igihe_config.get("rate_limit", 2.0)
33
+ self.contact_email = igihe_config.get("contact_email", "info@igihe.com")
34
+
35
+ def fetch_page(self, url: str) -> BeautifulSoup | None:
36
+ """Fetch a page and return parsed HTML."""
37
+ try:
38
+ if not check_robots_txt(url, user_agent="BwengeAi/0.1", delay=self.rate_limit):
39
+ return None
40
+ time.sleep(self.rate_limit)
41
+ response = self.session.get(url, timeout=30)
42
+ response.raise_for_status()
43
+ return BeautifulSoup(response.text, "lxml")
44
+ except Exception as e:
45
+ logger.error(f"Failed to fetch {url}: {e}")
46
+ return None
47
+
48
+ def get_article_links(self, category: str, max_pages: int = 5) -> list[str]:
49
+ """Get article links from a category page."""
50
+ links = []
51
+
52
+ for page in range(1, max_pages + 1):
53
+ if page == 1:
54
+ url = f"{self.base_url}/{category}"
55
+ else:
56
+ url = f"{self.base_url}/{category}?debut_gh_news={((page - 1) * 20)}"
57
+
58
+ soup = self.fetch_page(url)
59
+ if not soup:
60
+ break
61
+
62
+ article_links = soup.find_all("a", href=True)
63
+ for link in article_links:
64
+ href = link["href"]
65
+ if "/article/" in href or "/news/" in href:
66
+ if not href.startswith("http"):
67
+ href = f"{self.base_url}{href}"
68
+ if href not in links:
69
+ links.append(href)
70
+
71
+ logger.info(f" Category '{category}' page {page}: found {len(article_links)} links")
72
+
73
+ return links
74
+
75
+ def scrape_article(self, url: str) -> dict[str, Any] | None:
76
+ """Scrape a single article."""
77
+ soup = self.fetch_page(url)
78
+ if not soup:
79
+ return None
80
+
81
+ article = {"url": url, "source": "igihe"}
82
+
83
+ title_tag = soup.find("h1") or soup.find("title")
84
+ if title_tag:
85
+ article["title"] = title_tag.get_text(strip=True)
86
+
87
+ article["text"] = self._extract_article_text(soup)
88
+
89
+ if not article["text"] or len(article["text"]) < 100:
90
+ return None
91
+
92
+ author_tag = soup.find("span", class_=lambda x: x and "author" in x.lower()) or \
93
+ soup.find("a", class_=lambda x: x and "author" in x.lower())
94
+ if author_tag:
95
+ article["author"] = author_tag.get_text(strip=True)
96
+
97
+ date_tag = soup.find("time") or soup.find("span", class_=lambda x: x and "date" in x.lower())
98
+ if date_tag:
99
+ article["date"] = date_tag.get("datetime", date_tag.get_text(strip=True))
100
+
101
+ article["length"] = len(article["text"])
102
+
103
+ return article
104
+
105
+ def _extract_article_text(self, soup: BeautifulSoup) -> str:
106
+ """Extract article text content."""
107
+ for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
108
+ tag.decompose()
109
+
110
+ article = soup.find("article") or soup.find("div", class_=lambda x: x and "content" in x.lower())
111
+
112
+ if article:
113
+ paragraphs = article.find_all("p")
114
+ else:
115
+ paragraphs = soup.find_all("p")
116
+
117
+ texts = []
118
+ for p in paragraphs:
119
+ text = p.get_text(strip=True)
120
+ if text and len(text) > 20:
121
+ texts.append(text)
122
+
123
+ return "\n\n".join(texts)
124
+
125
+ def collect_category(self, category: str, max_articles: int = 100) -> list[dict[str, Any]]:
126
+ """Collect articles from a category."""
127
+ logger.info(f"Collecting articles from category: {category}")
128
+
129
+ links = self.get_article_links(category, max_pages=10)
130
+ logger.info(f" Found {len(links)} article links")
131
+
132
+ articles = []
133
+ for url in links[:max_articles]:
134
+ article = self.scrape_article(url)
135
+ if article:
136
+ articles.append(article)
137
+ logger.debug(f" Scraped: {article.get('title', 'untitled')[:50]}")
138
+
139
+ return articles
140
+
141
+ def collect_all(self) -> list[dict[str, Any]]:
142
+ """Collect articles from all configured categories."""
143
+ logger.info("Starting Igihe collection...")
144
+ logger.info(f"NOTE: Igihe content is copyrighted. Contact {self.contact_email} for licensing.")
145
+
146
+ all_articles = []
147
+
148
+ for category in self.categories:
149
+ articles = self.collect_category(category, max_articles=50)
150
+ all_articles.extend(articles)
151
+ logger.info(f" Category '{category}': {len(articles)} articles collected")
152
+
153
+ output_path = self.output_dir / "igihe_articles.jsonl"
154
+ with open(output_path, "w", encoding="utf-8") as f:
155
+ for article in all_articles:
156
+ f.write(json.dumps(article, ensure_ascii=False) + "\n")
157
+
158
+ summary = {
159
+ "source": "igihe",
160
+ "categories": self.categories,
161
+ "total_articles": len(all_articles),
162
+ "output_path": str(output_path),
163
+ "total_characters": sum(a.get("length", 0) for a in all_articles),
164
+ "contact_email": self.contact_email,
165
+ "copyright_notice": "All Rights Reserved - IGIHE Ltd",
166
+ }
167
+
168
+ summary_path = self.output_dir / "igihe_summary.json"
169
+ with open(summary_path, "w", encoding="utf-8") as f:
170
+ json.dump(summary, f, indent=2, ensure_ascii=False)
171
+
172
+ logger.info(f"Collected {len(all_articles)} articles from Igihe")
173
+ return all_articles