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

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

Browse files
.//src//data_collection//kigalitoday_scraper.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kigali Today 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 KigaliTodayScraper:
18
+ """Scrapes Kinyarwanda news content from KigaliToday.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)"
27
+ })
28
+
29
+ kt_config = config.get("kigalitoday", {})
30
+ self.base_url = kt_config.get("base_url", "https://www.kigalitoday.com")
31
+ self.categories = kt_config.get("categories", ["amakuru", "ubukungu", "imikino"])
32
+ self.rate_limit = kt_config.get("rate_limit", 2.0)
33
+
34
+ def fetch_page(self, url: str) -> BeautifulSoup | None:
35
+ """Fetch a page and return parsed HTML."""
36
+ try:
37
+ if not check_robots_txt(url, user_agent="BwengeAi/0.1", delay=self.rate_limit):
38
+ return None
39
+ time.sleep(self.rate_limit)
40
+ response = self.session.get(url, timeout=30)
41
+ response.raise_for_status()
42
+ return BeautifulSoup(response.text, "lxml")
43
+ except Exception as e:
44
+ logger.error(f"Failed to fetch {url}: {e}")
45
+ return None
46
+
47
+ def get_article_links(self, category: str, max_pages: int = 5) -> list[str]:
48
+ """Get article links from a category page."""
49
+ links = []
50
+
51
+ for page in range(1, max_pages + 1):
52
+ if page == 1:
53
+ url = f"{self.base_url}/{category}"
54
+ else:
55
+ url = f"{self.base_url}/{category}?page={page}"
56
+
57
+ soup = self.fetch_page(url)
58
+ if not soup:
59
+ break
60
+
61
+ for a_tag in soup.find_all("a", href=True):
62
+ href = a_tag["href"]
63
+ if "/article/" in href or "/news/" in href:
64
+ if not href.startswith("http"):
65
+ href = f"{self.base_url}{href}"
66
+ if href not in links:
67
+ links.append(href)
68
+
69
+ logger.info(f" Category '{category}' page {page}: {len(links)} links total")
70
+
71
+ return links
72
+
73
+ def scrape_article(self, url: str) -> dict[str, Any] | None:
74
+ """Scrape a single article."""
75
+ soup = self.fetch_page(url)
76
+ if not soup:
77
+ return None
78
+
79
+ article = {"url": url, "source": "kigalitoday"}
80
+
81
+ title_tag = soup.find("h1") or soup.find("title")
82
+ if title_tag:
83
+ article["title"] = title_tag.get_text(strip=True)
84
+
85
+ article["text"] = self._extract_article_text(soup)
86
+
87
+ if not article["text"] or len(article["text"]) < 100:
88
+ return None
89
+
90
+ date_tag = soup.find("time") or soup.find("span", class_=lambda x: x and "date" in str(x).lower())
91
+ if date_tag:
92
+ article["date"] = date_tag.get("datetime", date_tag.get_text(strip=True))
93
+
94
+ article["length"] = len(article["text"])
95
+
96
+ return article
97
+
98
+ def _extract_article_text(self, soup: BeautifulSoup) -> str:
99
+ """Extract article text content."""
100
+ for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
101
+ tag.decompose()
102
+
103
+ article = soup.find("article") or soup.find("div", class_=lambda x: x and "content" in str(x).lower())
104
+
105
+ if article:
106
+ paragraphs = article.find_all("p")
107
+ else:
108
+ paragraphs = soup.find_all("p")
109
+
110
+ texts = []
111
+ for p in paragraphs:
112
+ text = p.get_text(strip=True)
113
+ if text and len(text) > 20:
114
+ texts.append(text)
115
+
116
+ return "\n\n".join(texts)
117
+
118
+ def collect_category(self, category: str, max_articles: int = 100) -> list[dict[str, Any]]:
119
+ """Collect articles from a category."""
120
+ logger.info(f"Collecting articles from Kigali Today category: {category}")
121
+
122
+ links = self.get_article_links(category, max_pages=10)
123
+ logger.info(f" Found {len(links)} article links")
124
+
125
+ articles = []
126
+ for url in links[:max_articles]:
127
+ article = self.scrape_article(url)
128
+ if article:
129
+ articles.append(article)
130
+ logger.debug(f" Scraped: {article.get('title', 'untitled')[:50]}")
131
+
132
+ return articles
133
+
134
+ def collect_all(self) -> list[dict[str, Any]]:
135
+ """Collect articles from all configured categories."""
136
+ logger.info("Starting Kigali Today collection...")
137
+
138
+ all_articles = []
139
+
140
+ for category in self.categories:
141
+ articles = self.collect_category(category, max_articles=50)
142
+ all_articles.extend(articles)
143
+ logger.info(f" Category '{category}': {len(articles)} articles collected")
144
+
145
+ output_path = self.output_dir / "kigalitoday_articles.jsonl"
146
+ with open(output_path, "w", encoding="utf-8") as f:
147
+ for article in all_articles:
148
+ f.write(json.dumps(article, ensure_ascii=False) + "\n")
149
+
150
+ summary = {
151
+ "source": "kigalitoday",
152
+ "categories": self.categories,
153
+ "total_articles": len(all_articles),
154
+ "output_path": str(output_path),
155
+ "total_characters": sum(a.get("length", 0) for a in all_articles),
156
+ }
157
+
158
+ summary_path = self.output_dir / "kigalitoday_summary.json"
159
+ with open(summary_path, "w", encoding="utf-8") as f:
160
+ json.dump(summary, f, indent=2, ensure_ascii=False)
161
+
162
+ logger.info(f"Collected {len(all_articles)} articles from Kigali Today")
163
+ return all_articles