sharanyaswarup commited on
Commit
3d271a2
·
1 Parent(s): 7e19788

NAAC updated

Browse files
Files changed (5) hide show
  1. app.py +37 -19
  2. hf_store.py +11 -0
  3. requirements.txt +2 -0
  4. scraper/master_merger.py +315 -0
  5. scraper/naac_downloader.py +85 -19
app.py CHANGED
@@ -309,8 +309,14 @@ def _run_inc_download_and_build(selected_states: List[str]):
309
  def _run_naac_download():
310
  """Background thread logic for NAAC download and merge"""
311
  output_lines = []
 
 
 
 
 
 
312
  output_lines.append("🚀 Starting NAAC Download...")
313
- yield "\n".join(output_lines)
314
 
315
  base_dir = Path.cwd() / "output"
316
  downloads_dir = base_dir / "downloads"
@@ -322,19 +328,20 @@ def _run_naac_download():
322
  output_lines.append(msg)
323
 
324
  output_lines.append("\n⏳ Downloading NAAC Accredited Institutions...")
325
- yield "\n".join(output_lines)
326
 
327
- raw_naac_file = download_naac(output_dir=downloads_dir, headless=True, log_fn=log_naac)
 
328
 
329
  except Exception as e:
330
  output_lines.append(f"❌ NAAC Download error: {e}")
331
  output_lines.append("\n❌ Process stopped or download failed.")
332
- yield "\n".join(output_lines)
333
  return
334
 
335
  # 2. Merge and Filter
336
  output_lines.append("\n⏳ Filtering and formatting NAAC data...")
337
- yield "\n".join(output_lines)
338
 
339
  try:
340
  selected_states = _get_selected_states()
@@ -342,20 +349,17 @@ def _run_naac_download():
342
  final_filename = f"naac_colleges_{timestamp}.xlsx"
343
  final_output_path = base_dir / final_filename
344
 
345
- filter_naac(
346
- raw_file_path=raw_naac_file,
347
- output_file_path=final_output_path,
348
- selected_states=selected_states
349
- )
350
 
351
  output_lines.append(f"✅ Successfully filtered NAAC data based on selected states!")
352
  output_lines.append(f"💾 Saved locally as: {final_filename}")
353
- yield "\n".join(output_lines)
354
 
355
  # 3. Upload to HuggingFace
356
  if is_configured():
357
  output_lines.append("\n⏳ Uploading NAAC file to Hugging Face dataset...")
358
- yield "\n".join(output_lines)
359
 
360
  try:
361
  upload_combined_excel(final_output_path)
@@ -367,11 +371,11 @@ def _run_naac_download():
367
  output_lines.append("\n⚠️ Hugging Face upload skipped (HF_TOKEN not configured).")
368
  output_lines.append("🎉 All NAAC steps completed successfully!")
369
 
370
- yield "\n".join(output_lines)
371
 
372
  except Exception as e:
373
  output_lines.append(f"❌ Error filtering NAAC data: {e}")
374
- yield "\n".join(output_lines)
375
  return
376
 
377
  def _run_aicte_download():
@@ -643,7 +647,9 @@ Click the **Download** button below. The app will automatically open the NAAC we
643
  **Currently keeping data from {state_count} states** *(You can change this in the State Filter tab)*.
644
  """.replace("{state_count}", str(len(_get_selected_states()))))
645
 
646
- btn_download_naac = gr.Button("▶ Download NAAC Data Now", variant="primary")
 
 
647
 
648
  with gr.Column(scale=1):
649
  naac_output = gr.Textbox(
@@ -655,7 +661,12 @@ Click the **Download** button below. The app will automatically open the NAAC we
655
 
656
  btn_download_naac.click(
657
  fn=_run_naac_download,
658
- outputs=naac_output
 
 
 
 
 
659
  )
660
 
661
  # ── Tab 4: AICTE ──────────────────────────────────────────────
@@ -755,7 +766,8 @@ All your previous scrapes are saved automatically. You can view and download the
755
  refresh_hf_btn = gr.Button("🔄 Refresh List")
756
 
757
  with gr.Row():
758
- fetch_hf_btn = gr.DownloadButton("📥 Fetch & Download Selected", variant="primary")
 
759
 
760
  hf_status = gr.Textbox(label="Status", interactive=False, lines=1)
761
 
@@ -815,10 +827,16 @@ All your previous scrapes are saved automatically. You can view and download the
815
  outputs=[hf_files_dropdown],
816
  )
817
 
 
 
 
 
 
 
818
  fetch_hf_btn.click(
819
- fn=download_file_from_hf,
820
  inputs=[hf_files_dropdown],
821
- outputs=[fetch_hf_btn],
822
  )
823
 
824
  # Load dataset files on startup
 
309
  def _run_naac_download():
310
  """Background thread logic for NAAC download and merge"""
311
  output_lines = []
312
+
313
+ def get_ui_update(done=False):
314
+ if done:
315
+ return "\n".join(output_lines), gr.update(interactive=True), gr.update(visible=False)
316
+ return "\n".join(output_lines), gr.update(interactive=False), gr.update(visible=True, interactive=True)
317
+
318
  output_lines.append("🚀 Starting NAAC Download...")
319
+ yield get_ui_update()
320
 
321
  base_dir = Path.cwd() / "output"
322
  downloads_dir = base_dir / "downloads"
 
328
  output_lines.append(msg)
329
 
330
  output_lines.append("\n⏳ Downloading NAAC Accredited Institutions...")
331
+ yield get_ui_update()
332
 
333
+ selected_states = _get_selected_states()
334
+ raw_naac_file = download_naac(output_dir=downloads_dir, selected_states=selected_states, headless=True, log_fn=log_naac)
335
 
336
  except Exception as e:
337
  output_lines.append(f"❌ NAAC Download error: {e}")
338
  output_lines.append("\n❌ Process stopped or download failed.")
339
+ yield get_ui_update(done=True)
340
  return
341
 
342
  # 2. Merge and Filter
343
  output_lines.append("\n⏳ Filtering and formatting NAAC data...")
344
+ yield get_ui_update()
345
 
346
  try:
347
  selected_states = _get_selected_states()
 
349
  final_filename = f"naac_colleges_{timestamp}.xlsx"
350
  final_output_path = base_dir / final_filename
351
 
352
+ import shutil
353
+ shutil.copy2(raw_naac_file, final_output_path)
 
 
 
354
 
355
  output_lines.append(f"✅ Successfully filtered NAAC data based on selected states!")
356
  output_lines.append(f"💾 Saved locally as: {final_filename}")
357
+ yield get_ui_update()
358
 
359
  # 3. Upload to HuggingFace
360
  if is_configured():
361
  output_lines.append("\n⏳ Uploading NAAC file to Hugging Face dataset...")
362
+ yield get_ui_update()
363
 
364
  try:
365
  upload_combined_excel(final_output_path)
 
371
  output_lines.append("\n⚠️ Hugging Face upload skipped (HF_TOKEN not configured).")
372
  output_lines.append("🎉 All NAAC steps completed successfully!")
373
 
374
+ yield get_ui_update(done=True)
375
 
376
  except Exception as e:
377
  output_lines.append(f"❌ Error filtering NAAC data: {e}")
378
+ yield get_ui_update(done=True)
379
  return
380
 
381
  def _run_aicte_download():
 
647
  **Currently keeping data from {state_count} states** *(You can change this in the State Filter tab)*.
648
  """.replace("{state_count}", str(len(_get_selected_states()))))
649
 
650
+ with gr.Row():
651
+ btn_download_naac = gr.Button("▶ Download NAAC Data Now", variant="primary")
652
+ stop_naac_btn = gr.Button("🛑 Stop Scrape", variant="stop", visible=False)
653
 
654
  with gr.Column(scale=1):
655
  naac_output = gr.Textbox(
 
661
 
662
  btn_download_naac.click(
663
  fn=_run_naac_download,
664
+ outputs=[naac_output, btn_download_naac, stop_naac_btn]
665
+ )
666
+ stop_naac_btn.click(
667
+ fn=stop_scrape_process,
668
+ inputs=[],
669
+ outputs=[naac_output, stop_naac_btn, btn_download_naac]
670
  )
671
 
672
  # ── Tab 4: AICTE ──────────────────────────────────────────────
 
766
  refresh_hf_btn = gr.Button("🔄 Refresh List")
767
 
768
  with gr.Row():
769
+ fetch_hf_btn = gr.Button("☁️ Fetch Selected from HF", variant="primary")
770
+ download_hf_btn = gr.File(label="Ready to Download", visible=False)
771
 
772
  hf_status = gr.Textbox(label="Status", interactive=False, lines=1)
773
 
 
827
  outputs=[hf_files_dropdown],
828
  )
829
 
830
+ def handle_hf_fetch(filename):
831
+ path = download_file_from_hf(filename)
832
+ if path:
833
+ return gr.update(value="Fetched successfully!"), gr.update(value=path, visible=True)
834
+ return gr.update(value="Failed to fetch."), gr.update(visible=False)
835
+
836
  fetch_hf_btn.click(
837
+ fn=handle_hf_fetch,
838
  inputs=[hf_files_dropdown],
839
+ outputs=[hf_status, download_hf_btn],
840
  )
841
 
842
  # Load dataset files on startup
hf_store.py CHANGED
@@ -134,6 +134,17 @@ def download_combined_file(remote_filename: str, local_dir: Path) -> Optional[Pa
134
  from huggingface_hub import hf_hub_download
135
  try:
136
  local_dir.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
137
  local_path = hf_hub_download(
138
  repo_id=repo_id,
139
  filename=remote_filename,
 
134
  from huggingface_hub import hf_hub_download
135
  try:
136
  local_dir.mkdir(parents=True, exist_ok=True)
137
+
138
+ # Fast path: Check if we already have it locally to bypass network lag
139
+ expected_local = local_dir / remote_filename
140
+ if expected_local.exists():
141
+ return expected_local
142
+
143
+ # Check without 'combined/' prefix just in case
144
+ fallback_local = local_dir / remote_filename.split('/')[-1]
145
+ if fallback_local.exists():
146
+ return fallback_local
147
+
148
  local_path = hf_hub_download(
149
  repo_id=repo_id,
150
  filename=remote_filename,
requirements.txt CHANGED
@@ -8,3 +8,5 @@ huggingface_hub>=0.20.0
8
  pytest>=7.0.0
9
  pytest-playwright>=0.4.0
10
  spaces
 
 
 
8
  pytest>=7.0.0
9
  pytest-playwright>=0.4.0
10
  spaces
11
+ thefuzz>=0.22.0
12
+ python-Levenshtein>=0.27.0
scraper/master_merger.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from pathlib import Path
3
+ import re
4
+ from thefuzz import process, fuzz
5
+ import time
6
+ from typing import Optional, Tuple, Dict, Any, List
7
+
8
+ def get_latest_file(directory: Path, prefix: str) -> Optional[Path]:
9
+ files = list(directory.glob(f"{prefix}*.xlsx"))
10
+ if not files:
11
+ return None
12
+ return max(files, key=lambda p: p.stat().st_mtime)
13
+
14
+ def clean_text(text: Any) -> str:
15
+ if pd.isna(text):
16
+ return ""
17
+ text = str(text).lower()
18
+ text = re.sub(r'[^a-z0-9\s]', '', text)
19
+ text = re.sub(r'\s+', ' ', text).strip()
20
+ return text
21
+
22
+ def fuzzy_match_college(target_name: str, candidates: list, threshold: int = 85) -> Optional[str]:
23
+ if not candidates or not target_name:
24
+ return None
25
+ best_match = process.extractOne(target_name, candidates, scorer=fuzz.token_sort_ratio)
26
+ if best_match and best_match[1] >= threshold:
27
+ return best_match[0]
28
+ return None
29
+
30
+ def merge_master_database(output_dir: Path):
31
+ yield "Starting Relational Master Database Merger..."
32
+
33
+ files = {
34
+ "aishe": get_latest_file(output_dir, "aishe_colleges"),
35
+ "naac": get_latest_file(output_dir, "naac_colleges"),
36
+ "ugc": get_latest_file(output_dir, "ugc_colleges"),
37
+ "aicte": get_latest_file(output_dir, "aicte_colleges"),
38
+ "inc": get_latest_file(output_dir, "inc_colleges")
39
+ }
40
+
41
+ for k, v in files.items():
42
+ if v:
43
+ yield f"Found {k.upper()} file: {v.name}"
44
+ else:
45
+ yield f"WARNING: No file found for {k.upper()}"
46
+
47
+ if not files["aishe"]:
48
+ yield "ERROR: AISHE master file is required. Aborting."
49
+ return
50
+
51
+ yield "\nLoading AISHE..."
52
+ aishe_sheets = pd.read_excel(files["aishe"], sheet_name=None, header=2)
53
+ aishe_dfs = []
54
+ for sheet_name, df in aishe_sheets.items():
55
+ df['Institution_Category'] = sheet_name
56
+
57
+ # Try to find Management column
58
+ m_col = None
59
+ for col in ['Manegement', 'Management', 'Management Type']:
60
+ if col in df.columns:
61
+ m_col = col
62
+ break
63
+
64
+ if m_col:
65
+ df = df.rename(columns={m_col: "AISHE_Management"})
66
+ else:
67
+ df["AISHE_Management"] = None
68
+
69
+ aishe_dfs.append(df)
70
+
71
+ colleges_df = pd.concat(aishe_dfs, ignore_index=True)
72
+ colleges_df = colleges_df.rename(columns={
73
+ "Aishe Code": "AISHE_ID",
74
+ "Name": "Institution_Name",
75
+ "State": "State",
76
+ "District": "District",
77
+ "Website": "Website",
78
+ "Year Of Establishment": "Year_Of_Establishment"
79
+ })
80
+
81
+ colleges_df['Clean_Name'] = colleges_df['Institution_Name'].apply(clean_text)
82
+ colleges_df['Clean_State'] = colleges_df['State'].apply(clean_text)
83
+ colleges_df['Clean_District'] = colleges_df['District'].apply(clean_text)
84
+
85
+ yield "Removing basic duplicates from AISHE..."
86
+ colleges_df = colleges_df.drop_duplicates(subset=['Clean_Name', 'Clean_State', 'Clean_District'], keep='first')
87
+
88
+ # Initialize APF_ID for base colleges
89
+ colleges_df.insert(0, "APF_ID", [f"APF{i:06d}" for i in range(1, len(colleges_df) + 1)])
90
+ next_apf_id = len(colleges_df) + 1
91
+
92
+ courses_list = []
93
+
94
+ # --- NAAC Overlay ---
95
+ if files["naac"]:
96
+ yield "Overlaying NAAC Data..."
97
+ naac_df = pd.read_excel(files["naac"])
98
+ if "Aishe-Id" in naac_df.columns:
99
+ naac_subset = naac_df[["Aishe-Id", "Current Grade", "Current CGPA"]].copy()
100
+ naac_subset = naac_subset.rename(columns={
101
+ "Aishe-Id": "AISHE_ID",
102
+ "Current Grade": "NAAC_Grade",
103
+ "Current CGPA": "NAAC_CGPA"
104
+ })
105
+ colleges_df = colleges_df.merge(naac_subset, on="AISHE_ID", how="left")
106
+
107
+ def process_overlay(overlay_df, name_col, state_col, district_col, source_name, is_course_source=False, course_cols_map=None, college_cols_map=None):
108
+ nonlocal colleges_df, next_apf_id, courses_list
109
+ print(f"Overlaying {source_name} Data via Fuzzy Matching...")
110
+
111
+ overlay_df['Clean_Name'] = overlay_df[name_col].apply(clean_text)
112
+ overlay_df['Clean_State'] = overlay_df[state_col].apply(clean_text)
113
+ if district_col and district_col in overlay_df.columns:
114
+ overlay_df['Clean_District'] = overlay_df[district_col].apply(clean_text)
115
+
116
+ if college_cols_map:
117
+ for v in college_cols_map.values():
118
+ if v not in colleges_df.columns:
119
+ colleges_df[v] = None
120
+
121
+ # Group by college to prevent duplicating the college for every course
122
+ grouped = overlay_df.groupby(['Clean_Name', 'Clean_State', 'Clean_District'] if district_col else ['Clean_Name', 'Clean_State'])
123
+
124
+ match_count = 0
125
+ unmatched_count = 0
126
+ new_colleges = []
127
+
128
+ for group_keys, group_df in grouped:
129
+ # group_keys is a tuple of (Name, State, [District])
130
+ nm = group_keys[0]
131
+ st = group_keys[1]
132
+ dist = group_keys[2] if len(group_keys) > 2 else None
133
+
134
+ candidates = colleges_df[colleges_df['Clean_State'] == st]
135
+ if dist:
136
+ dist_candidates = candidates[candidates['Clean_District'] == dist]
137
+ if not dist_candidates.empty:
138
+ candidates = dist_candidates
139
+
140
+ candidate_names = candidates['Clean_Name'].tolist()
141
+ best_match = fuzzy_match_college(nm, candidate_names)
142
+
143
+ target_apf_id = None
144
+ if best_match:
145
+ idx = candidates[candidates['Clean_Name'] == best_match].index[0]
146
+ target_apf_id = colleges_df.at[idx, 'APF_ID']
147
+ # Update college-level metadata
148
+ if college_cols_map:
149
+ first_row = group_df.iloc[0]
150
+ for old_col, new_col in college_cols_map.items():
151
+ if old_col in first_row and pd.notna(first_row[old_col]):
152
+ colleges_df.at[idx, new_col] = first_row[old_col]
153
+ match_count += 1
154
+ else:
155
+ # Create a new college entry
156
+ target_apf_id = f"APF{next_apf_id:06d}"
157
+ next_apf_id += 1
158
+ first_row = group_df.iloc[0]
159
+
160
+ new_coll = {
161
+ 'APF_ID': target_apf_id,
162
+ 'Institution_Name': first_row[name_col],
163
+ 'State': first_row[state_col],
164
+ 'Clean_Name': nm,
165
+ 'Clean_State': st,
166
+ 'Missing_In_AISHE': True
167
+ }
168
+ if district_col and district_col in first_row:
169
+ new_coll['District'] = first_row[district_col]
170
+ new_coll['Clean_District'] = dist
171
+
172
+ if college_cols_map:
173
+ for old_col, new_col in college_cols_map.items():
174
+ if old_col in first_row:
175
+ new_coll[new_col] = first_row[old_col]
176
+
177
+ new_colleges.append(new_coll)
178
+ unmatched_count += 1
179
+
180
+ # Process courses if this source provides them
181
+ if is_course_source and course_cols_map:
182
+ for _, row in group_df.iterrows():
183
+ course_entry = {
184
+ 'APF_ID': target_apf_id,
185
+ 'Source': source_name
186
+ }
187
+ for old_col, new_col in course_cols_map.items():
188
+ if old_col in row:
189
+ course_entry[new_col] = row[old_col]
190
+ courses_list.append(course_entry)
191
+
192
+ print(f" [SUCCESS] {source_name}: Mapped {match_count} colleges. {unmatched_count} new colleges appended.")
193
+ if new_colleges:
194
+ colleges_df = pd.concat([colleges_df, pd.DataFrame(new_colleges)], ignore_index=True)
195
+
196
+ # --- UGC Overlay ---
197
+ if files["ugc"]:
198
+ ugc_df = pd.read_excel(files["ugc"])
199
+ process_overlay(
200
+ ugc_df,
201
+ name_col="Name of the college",
202
+ state_col="State",
203
+ district_col="District",
204
+ source_name="UGC",
205
+ is_course_source=False,
206
+ college_cols_map={
207
+ "Status": "UGC_Status",
208
+ "Affiliated To University": "UGC_Affiliated_University",
209
+ "Govt or Non Govt": "Funding_Type"
210
+ }
211
+ )
212
+
213
+ # --- AICTE Overlay ---
214
+ if files["aicte"]:
215
+ aicte_df = pd.read_excel(files["aicte"]).dropna(subset=['Institution Name'])
216
+ process_overlay(
217
+ aicte_df,
218
+ name_col="Institution Name",
219
+ state_col="State",
220
+ district_col="District",
221
+ source_name="AICTE",
222
+ is_course_source=True,
223
+ college_cols_map={"AICTE ID": "AICTE_ID"},
224
+ course_cols_map={
225
+ "AICTE ID": "Source_ID",
226
+ "Program": "Program_Name",
227
+ "Total Students": "Annual_Intake"
228
+ }
229
+ )
230
+
231
+ # --- INC Overlay ---
232
+ if files["inc"]:
233
+ inc_df = pd.read_excel(files["inc"])
234
+ process_overlay(
235
+ inc_df,
236
+ name_col="Institution Name & Address",
237
+ state_col="State",
238
+ district_col="District",
239
+ source_name="INC",
240
+ is_course_source=True,
241
+ college_cols_map={},
242
+ course_cols_map={
243
+ "Programme": "Program_Name",
244
+ "Annual Intake": "Annual_Intake"
245
+ }
246
+ )
247
+
248
+ yield "\nCleaning up columns..."
249
+
250
+ # Unify Management Type (AISHE vs UGC)
251
+ def resolve_mgmt(row):
252
+ f_type = str(row.get('Funding_Type', '')).lower()
253
+ a_mgmt = str(row.get('AISHE_Management', '')).lower()
254
+
255
+ if 'govt' in f_type or 'government' in f_type or 'govt' in a_mgmt or 'government' in a_mgmt:
256
+ return 'Government'
257
+ elif 'private' in f_type or 'private' in a_mgmt or 'unaided' in f_type:
258
+ return 'Private / Non-Government'
259
+ elif 'aided' in f_type or 'aided' in a_mgmt:
260
+ return 'Aided'
261
+ return 'Unknown'
262
+
263
+ colleges_df['Management_Type'] = colleges_df.apply(resolve_mgmt, axis=1)
264
+
265
+ # Final Columns Selection
266
+ compulsory_columns = [
267
+ "APF_ID",
268
+ "AISHE_ID",
269
+ "AICTE_ID",
270
+ "Institution_Name",
271
+ "State",
272
+ "District",
273
+ "Management_Type",
274
+ "Institution_Category",
275
+ "Year_Of_Establishment",
276
+ "Website",
277
+ "UGC_Status",
278
+ "UGC_Affiliated_University",
279
+ "NAAC_Grade",
280
+ "NAAC_CGPA"
281
+ ]
282
+
283
+ for col in compulsory_columns:
284
+ if col not in colleges_df.columns:
285
+ colleges_df[col] = None
286
+
287
+ colleges_df = colleges_df[compulsory_columns]
288
+
289
+ courses_df = pd.DataFrame(courses_list)
290
+ course_columns = ["APF_ID", "Source", "Source_ID", "Program_Name", "Annual_Intake"]
291
+ for col in course_columns:
292
+ if col not in courses_df.columns:
293
+ courses_df[col] = None
294
+ courses_df = courses_df[course_columns]
295
+
296
+ # Sort courses by APF_ID so all courses for a college are listed together
297
+ courses_df.sort_values(by="APF_ID", inplace=True)
298
+
299
+ # Save to Excel
300
+ timestamp = time.strftime("%b_%Y_%H%M%S").lower()
301
+ master_path = output_dir / f"master_database_relational_{timestamp}.xlsx"
302
+
303
+ yield "Writing to Excel file with multiple sheets (this might take a minute)..."
304
+ with pd.ExcelWriter(master_path) as writer:
305
+ colleges_df.to_excel(writer, sheet_name="Colleges", index=False)
306
+ courses_df.to_excel(writer, sheet_name="Courses", index=False)
307
+
308
+ yield f"\n[SUCCESS] Relational Master Database successfully generated!"
309
+ yield f"[SAVED] Saved to: {master_path}"
310
+ yield master_path
311
+
312
+ if __name__ == "__main__":
313
+ out_dir = Path("c:/Users/Sharanya/Downloads/colleges_scraping/output")
314
+ for msg in merge_master_database(out_dir):
315
+ print(msg)
scraper/naac_downloader.py CHANGED
@@ -1,9 +1,12 @@
1
  import argparse
2
- import sys
 
3
  from pathlib import Path
4
  from playwright.sync_api import sync_playwright
 
 
5
 
6
- def download_naac(output_dir: Path, headless: bool = True, log_fn=None) -> Path:
7
  def log(msg: str):
8
  if log_fn:
9
  log_fn(msg)
@@ -17,39 +20,102 @@ def download_naac(output_dir: Path, headless: bool = True, log_fn=None) -> Path:
17
  headless=headless,
18
  args=["--no-sandbox", "--disable-dev-shm-usage"]
19
  )
20
- context = browser.new_context(accept_downloads=True)
21
- page = context.new_page()
22
 
23
- log("Navigate to NAAC Accreditation Status page...")
24
  try:
25
- page.goto("https://www.naac.gov.in/index.php/en/2-uncategorised/32-accreditation-status", wait_until="domcontentloaded", timeout=60_000)
26
 
27
- log("Waiting for NAAC Excel download link...")
28
- # Click the exact link from the user's codegen
29
- with page.expect_download(timeout=120_000) as download_info:
30
- page.get_by_role("link", name="Institutions with valid").click(timeout=30_000)
 
 
 
 
 
 
 
 
 
31
 
32
- download = download_info.value
33
 
34
- dest_path = output_dir / "naac_colleges_raw.xlsx"
35
- log(f"Downloading NAAC file to {dest_path.name}...")
36
- download.save_as(dest_path)
37
 
38
- log("NAAC download completed successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  return dest_path
40
 
41
  except Exception as e:
42
  log(f"Error during NAAC download: {e}")
43
  raise
44
  finally:
45
- context.close()
46
  browser.close()
47
 
48
  if __name__ == "__main__":
49
- parser = argparse.ArgumentParser(description="Download NAAC Accredited Institutions Data")
50
- parser.add_argument("--output-dir", type=str, default="output/downloads", help="Directory to save downloaded files")
51
  parser.add_argument("--headed", action="store_true", help="Run browser in headed mode (visible)")
 
52
  args = parser.parse_args()
53
 
54
  out_dir = Path(args.output_dir)
55
- download_naac(out_dir, headless=not args.headed)
 
1
  import argparse
2
+ import time
3
+ import pandas as pd
4
  from pathlib import Path
5
  from playwright.sync_api import sync_playwright
6
+ from thefuzz import process
7
+ from typing import List
8
 
9
+ def download_naac(output_dir: Path, selected_states: List[str] = None, headless: bool = True, log_fn=None) -> Path:
10
  def log(msg: str):
11
  if log_fn:
12
  log_fn(msg)
 
20
  headless=headless,
21
  args=["--no-sandbox", "--disable-dev-shm-usage"]
22
  )
23
+ page = browser.new_page()
 
24
 
25
+ log("Navigate to NAAC Dashboard...")
26
  try:
27
+ page.goto("https://assessmentonline.naac.gov.in/public/index.php/hei_dashboard", wait_until="networkidle", timeout=60_000)
28
 
29
+ log("Executing stealth API call to fetch all records instantly...")
30
+ js_code = """
31
+ async () => {
32
+ let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
33
+ let url = 'https://assessmentonline.naac.gov.in/public/index.php/hei_dashboard?_token=' + token + '&inst_type=0&state=0&cycle=0&iiqa_status=5&date_range=&inst_name=&draw=1&start=0&length=15000';
34
+ let res = await fetch(url, {
35
+ headers: {
36
+ 'X-Requested-With': 'XMLHttpRequest'
37
+ }
38
+ });
39
+ return await res.json();
40
+ }
41
+ """
42
 
43
+ data = page.evaluate(js_code)
44
 
45
+ records = data.get("data", [])
46
+ total = data.get("recordsTotal", 0)
47
+ log(f"Successfully retrieved {len(records)} records (out of {total} total reported).")
48
 
49
+ if not records:
50
+ raise Exception("API returned empty data.")
51
+
52
+ log("Converting to Excel...")
53
+ df = pd.DataFrame(records)
54
+
55
+ # Rename columns to match what master_merger.py expects
56
+ if "aishe_id" in df.columns:
57
+ df = df.rename(columns={"aishe_id": "Aishe-Id"})
58
+ if "grade" in df.columns:
59
+ df = df.rename(columns={"grade": "Current Grade"})
60
+
61
+ if selected_states and not df.empty and "state_name" in df.columns:
62
+ log(f"Filtering NAAC data for {len(selected_states)} selected states with fuzzy matching...")
63
+
64
+ target_states_lower = [s.strip().lower() for s in selected_states]
65
+
66
+ def is_selected_state(naac_state):
67
+ if pd.isna(naac_state):
68
+ return False
69
+ naac_state = str(naac_state).strip().lower()
70
+
71
+ # Fast exact/substring match
72
+ for t in target_states_lower:
73
+ if naac_state == t or naac_state in t or t in naac_state:
74
+ return True
75
+
76
+ # Known aliases (Orissa/Odisha, Uttarakhand/Uttaranchal)
77
+ aliases = {
78
+ "orissa": "odisha",
79
+ "odisha": "orissa",
80
+ "uttarakhand": "uttaranchal",
81
+ "uttaranchal": "uttarakhand",
82
+ "pondicherry": "puducherry",
83
+ "puducherry": "pondicherry",
84
+ }
85
+ if naac_state in aliases and aliases[naac_state] in target_states_lower:
86
+ return True
87
+
88
+ # Fuzzy match fallback
89
+ best_match, score = process.extractOne(naac_state, target_states_lower)
90
+ return score >= 85
91
+
92
+ df = df[df["state_name"].apply(is_selected_state)]
93
+ log(f"Filtered down to {len(df)} records matching selected states.")
94
+
95
+ # Group states together (sort alphabetically by state, then name)
96
+ if "state_name" in df.columns:
97
+ df = df.sort_values(by=["state_name", "hei_name"], ascending=[True, True])
98
+
99
+
100
+ timestamp = time.strftime("%b_%Y_%H%M%S").lower()
101
+ dest_path = output_dir / f"naac_colleges_{timestamp}.xlsx"
102
+
103
+ df.to_excel(dest_path, index=False)
104
+ log(f"NAAC download completed successfully! Saved to {dest_path.name}")
105
  return dest_path
106
 
107
  except Exception as e:
108
  log(f"Error during NAAC download: {e}")
109
  raise
110
  finally:
 
111
  browser.close()
112
 
113
  if __name__ == "__main__":
114
+ parser = argparse.ArgumentParser(description="Download NAAC Accredited Institutions Data (API Fast Path)")
115
+ parser.add_argument("--output-dir", type=str, default="output", help="Directory to save downloaded files")
116
  parser.add_argument("--headed", action="store_true", help="Run browser in headed mode (visible)")
117
+ parser.add_argument("--states", nargs="+", help="List of states to filter by", default=[])
118
  args = parser.parse_args()
119
 
120
  out_dir = Path(args.output_dir)
121
+ download_naac(out_dir, selected_states=args.states if args.states else None, headless=not args.headed)