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

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

Browse files
.//src//data_collection//wikipedia_collector.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wikipedia data collector for Kinyarwanda."""
2
+
3
+ import bz2
4
+ import json
5
+ import logging
6
+ import xml.etree.ElementTree as ET
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import requests
11
+ from tqdm import tqdm
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ WIKI_API = "https://rw.wikipedia.org/w/api.php"
16
+
17
+
18
+ class WikipediaCollector:
19
+ """Collects Kinyarwanda Wikipedia content."""
20
+
21
+ def __init__(self, output_dir: str, config: dict[str, Any]):
22
+ self.output_dir = Path(output_dir)
23
+ self.output_dir.mkdir(parents=True, exist_ok=True)
24
+ self.config = config
25
+ self.session = requests.Session()
26
+ self.session.headers.update({"User-Agent": "BwengeAi/0.1 (Kinyarwanda AI Research)"})
27
+
28
+ def get_site_stats(self) -> dict[str, Any]:
29
+ """Get Wikipedia site statistics."""
30
+ params = {
31
+ "action": "query",
32
+ "meta": "siteinfo",
33
+ "siprop": "statistics",
34
+ "format": "json",
35
+ }
36
+
37
+ response = self.session.get(WIKI_API, params=params)
38
+ response.raise_for_status()
39
+ data = response.json()
40
+
41
+ stats = data.get("query", {}).get("statistics", {})
42
+ logger.info(f"Kinyarwanda Wikipedia stats: {stats}")
43
+ return stats
44
+
45
+ def fetch_article_list(self) -> list[str]:
46
+ """Fetch list of all article titles."""
47
+ articles = []
48
+ params = {
49
+ "action": "query",
50
+ "list": "allpages",
51
+ "apnamespace": 0,
52
+ "aplimit": "max",
53
+ "format": "json",
54
+ }
55
+
56
+ while True:
57
+ response = self.session.get(WIKI_API, params=params)
58
+ response.raise_for_status()
59
+ data = response.json()
60
+
61
+ pages = data.get("query", {}).get("allpages", [])
62
+ articles.extend(page["title"] for page in pages)
63
+
64
+ if "continue" in data:
65
+ params["apcontinue"] = data["continue"]["apcontinue"]
66
+ else:
67
+ break
68
+
69
+ logger.info(f"Found {len(articles)} articles")
70
+ return articles
71
+
72
+ def fetch_article_content(self, title: str) -> dict[str, Any] | None:
73
+ """Fetch content for a single article."""
74
+ params = {
75
+ "action": "parse",
76
+ "page": title,
77
+ "prop": "wikitext|text|categories|links",
78
+ "format": "json",
79
+ }
80
+
81
+ try:
82
+ response = self.session.get(WIKI_API, params=params)
83
+ response.raise_for_status()
84
+ data = response.json()
85
+
86
+ parse = data.get("parse", {})
87
+ text = parse.get("text", {}).get("*", "")
88
+ clean_text = self._clean_html(text)
89
+
90
+ return {
91
+ "title": title,
92
+ "text": clean_text,
93
+ "wikitext": parse.get("wikitext", {}).get("*", ""),
94
+ "categories": [cat["*"] for cat in parse.get("categories", [])],
95
+ "links": [link["*"] for link in parse.get("links", [])],
96
+ "length": len(clean_text),
97
+ }
98
+ except Exception as e:
99
+ logger.warning(f"Failed to fetch article '{title}': {e}")
100
+ return None
101
+
102
+ def _clean_html(self, html: str) -> str:
103
+ """Clean HTML content to plain text."""
104
+ from bs4 import BeautifulSoup
105
+
106
+ soup = BeautifulSoup(html, "lxml")
107
+
108
+ for tag in soup(["script", "style", "sup", "sub"]):
109
+ tag.decompose()
110
+
111
+ text = soup.get_text(separator=" ", strip=True)
112
+
113
+ lines = text.split()
114
+ clean_lines = []
115
+ for line in lines:
116
+ line = line.strip()
117
+ if line and not line.startswith("[") and len(line) > 2:
118
+ clean_lines.append(line)
119
+
120
+ return " ".join(clean_lines)
121
+
122
+ def collect_via_api(self, max_articles: int | None = None) -> list[dict[str, Any]]:
123
+ """Collect articles via the Wikipedia API."""
124
+ articles = self.fetch_article_list()
125
+
126
+ if max_articles:
127
+ articles = articles[:max_articles]
128
+
129
+ logger.info(f"Collecting {len(articles)} articles via API...")
130
+
131
+ results = []
132
+ output_path = self.output_dir / "wikipedia_articles.jsonl"
133
+
134
+ with open(output_path, "w", encoding="utf-8") as f:
135
+ for title in tqdm(articles, desc="Fetching Wikipedia articles"):
136
+ article = self.fetch_article_content(title)
137
+ if article and article["text"]:
138
+ f.write(json.dumps(article, ensure_ascii=False) + "\n")
139
+ results.append(article)
140
+
141
+ summary = {
142
+ "source": "kinyarwanda_wikipedia",
143
+ "total_articles": len(articles),
144
+ "collected": len(results),
145
+ "output_path": str(output_path),
146
+ "total_characters": sum(r["length"] for r in results),
147
+ }
148
+
149
+ summary_path = self.output_dir / "wikipedia_summary.json"
150
+ with open(summary_path, "w", encoding="utf-8") as f:
151
+ json.dump(summary, f, indent=2, ensure_ascii=False)
152
+
153
+ logger.info(f"Collected {len(results)} articles ({summary['total_characters']} characters)")
154
+ return results
155
+
156
+ def download_dump(self) -> Path | None:
157
+ """Download the Wikipedia dump file."""
158
+ dump_config = self.config.get("wikipedia", {})
159
+ dump_url = dump_config.get("dump_url", "https://dumps.wikimedia.org/rwwiki/latest/")
160
+ dump_file = dump_config.get("dump_file", "rwwiki-latest-pages-articles.xml.bz2")
161
+
162
+ url = f"{dump_url}{dump_file}"
163
+ output_path = self.output_dir / dump_file
164
+
165
+ if output_path.exists():
166
+ logger.info(f"Dump file already exists: {output_path}")
167
+ return output_path
168
+
169
+ logger.info(f"Downloading dump from {url}...")
170
+
171
+ try:
172
+ response = requests.get(url, stream=True, timeout=300)
173
+ response.raise_for_status()
174
+
175
+ total_size = int(response.headers.get("content-length", 0))
176
+
177
+ with open(output_path, "wb") as f:
178
+ with tqdm(total=total_size, unit="B", unit_scale=True, desc="Downloading") as pbar:
179
+ for chunk in response.iter_content(chunk_size=8192):
180
+ f.write(chunk)
181
+ pbar.update(len(chunk))
182
+
183
+ logger.info(f"Downloaded dump to {output_path}")
184
+ return output_path
185
+
186
+ except Exception as e:
187
+ logger.error(f"Failed to download dump: {e}")
188
+ return None
189
+
190
+ def parse_dump(self, dump_path: Path, max_articles: int | None = None) -> list[dict[str, Any]]:
191
+ """Parse a Wikipedia XML dump file using iterative XML parsing."""
192
+ logger.info(f"Parsing dump file: {dump_path}")
193
+
194
+ results = []
195
+ count = 0
196
+ title = ""
197
+ in_page = False
198
+ in_text = False
199
+ text_parts: list[str] = []
200
+
201
+ with bz2.open(dump_path, "rb") as f:
202
+ for event, elem in ET.iterparse(f, events=("start", "end")):
203
+ tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
204
+
205
+ if event == "start" and tag == "page":
206
+ in_page = True
207
+ title = ""
208
+ text_parts = []
209
+
210
+ elif event == "start" and tag == "title" and in_page:
211
+ title = (elem.text or "").strip()
212
+
213
+ elif event == "start" and tag == "text" and in_page:
214
+ in_text = True
215
+ if elem.text:
216
+ text_parts.append(elem.text)
217
+
218
+ elif event == "end" and tag == "text" and in_page:
219
+ in_text = False
220
+
221
+ elif event == "end" and tag == "page" and in_page:
222
+ in_page = False
223
+ full_text = " ".join(text_parts)
224
+ clean_text = self._clean_mediawiki(full_text)
225
+
226
+ if clean_text and len(clean_text) > 100:
227
+ results.append({
228
+ "title": title,
229
+ "text": clean_text,
230
+ "length": len(clean_text),
231
+ })
232
+ count += 1
233
+
234
+ if count % 1000 == 0:
235
+ logger.info(f" Parsed {count:,} articles...")
236
+
237
+ if max_articles and count >= max_articles:
238
+ break
239
+
240
+ elem.clear()
241
+
242
+ output_path = self.output_dir / "wikipedia_dump_articles.jsonl"
243
+ with open(output_path, "w", encoding="utf-8") as f:
244
+ for article in results:
245
+ f.write(json.dumps(article, ensure_ascii=False) + "\n")
246
+
247
+ logger.info(f"Parsed {len(results)} articles from dump")
248
+ return results
249
+
250
+ def _clean_mediawiki(self, text: str) -> str:
251
+ """Clean MediaWiki markup to plain text."""
252
+ import re
253
+
254
+ text = re.sub(r"\[\[([^|\]]*\|)?([^\]]*)\]\]", r"\2", text)
255
+ text = re.sub(r"\{\{[^}]*\}\}", "", text)
256
+ text = re.sub(r"'''?", "", text)
257
+ text = re.sub(r"<[^>]+>", "", text)
258
+ text = re.sub(r"==+\s*[^=]*\s*==+", "", text)
259
+ text = re.sub(r"\s+", " ", text)
260
+
261
+ return text.strip()