import asyncio import aiohttp import pandas as pd import json import os import time import math import sys from datetime import datetime, timedelta from pathlib import Path import gradio as gr API_URL = "https://natiga.edudk.net/P20262026/public/api_result.php" OUTPUT_DIR = Path("output") OUTPUT_DIR.mkdir(exist_ok=True) CONCURRENCY = 10 CHECKPOINT_EVERY = 2000 MAX_RETRIES = 3 state = { 'running': False, 'stage': 'idle', 'found': [], 'not_found_seats': [], 'total_scraped': 0, 'total_found': 0, 'total_not_found': 0, 'total_errors': 0, 'current_seat': 0, 'start_time': None, 'elapsed': '00:00:00', 'remaining': '--:--:--', 'rate': 0, 'progress_pct': 0, 'logs': [], } def add_log(msg): ts = datetime.now().strftime("%H:%M:%S") state['logs'].append(f"[{ts}] {msg}") if len(state['logs']) > 500: state['logs'] = state['logs'][-500:] def save_checkpoint(): if state['found']: df = pd.DataFrame(state['found']) path = OUTPUT_DIR / "students_full.csv" df.to_csv(path, index=False, encoding='utf-8-sig') add_log(f"حفظ {len(state['found'])} طالب → students_full.csv") if state['not_found_seats']: pd.DataFrame({'seat_no': state['not_found_seats']}).to_csv( OUTPUT_DIR / "not_found.csv", index=False, encoding='utf-8-sig') async def fetch_one(session, sem, seat): for attempt in range(MAX_RETRIES + 2): try: async with sem: async with session.get(f"{API_URL}?seat={seat}", timeout=aiohttp.ClientTimeout(total=12)) as resp: if resp.status == 429: wait = 5 * (attempt + 1) add_log(f"⏳ Rate-limit على المقعد {seat}، انتظار {wait}ث") await asyncio.sleep(wait) continue body = await resp.text() data = json.loads(body) if data.get('ok') and data.get('data'): return 'found', data['data'] return 'not_found', seat except (asyncio.TimeoutError, aiohttp.ClientError) as e: if attempt < MAX_RETRIES + 1: await asyncio.sleep(1.5 ** attempt) continue return 'error', f"{seat}: {str(e)[:60]}" except json.JSONDecodeError: return 'error', f"{seat}: bad json" return 'not_found', seat async def scrape_range(start, end, retry_mode=False): sem = asyncio.Semaphore(CONCURRENCY) state['start_time'] = time.time() total = end - start + 1 done = 0 connector = aiohttp.TCPConnector(limit=CONCURRENCY + 5, force_close=True) async with aiohttp.ClientSession(connector=connector) as session: batch = [] for seat in range(start, end + 1): if not state['running']: add_log("❌ تم إيقاف السكريب") break batch.append(fetch_one(session, sem, seat)) if len(batch) >= CONCURRENCY * 3: yield await process_batch(batch, total, retry_mode) done += len(batch) batch = [] if batch: yield await process_batch(batch, total, retry_mode) if not retry_mode and state['running']: add_log("🏁 انتهى الشوط الأول — بدء إعادة فحص الغائبين...") save_checkpoint() state['stage'] = 'retry' nf = state['not_found_seats'][:] if nf: async for upd in scrape_range(min(nf), max(nf), retry_mode=True): yield upd async def process_batch(batch, grand_total, retry_mode): results = await asyncio.gather(*batch) newly_found = [] for status, data in results: state['total_scraped'] += 1 if status == 'found': state['total_found'] += 1 state['found'].append(data) elif status == 'not_found': state['total_not_found'] += 1 if not retry_mode: state['not_found_seats'].append(data) else: state['total_errors'] += 1 add_log(f"⚠️ {data}") if not retry_mode and len(state['found']) // CHECKPOINT_EVERY > (len(state['found']) - len(newly_found)) // CHECKPOINT_EVERY: save_checkpoint() elapsed = time.time() - state['start_time'] state['elapsed'] = str(timedelta(seconds=int(elapsed))) state['progress_pct'] = min(100, state['total_scraped'] / grand_total * 100) state['rate'] = state['total_scraped'] / elapsed if elapsed > 0 else 0 if state['rate'] > 0: rem = (grand_total - state['total_scraped']) / state['rate'] state['remaining'] = str(timedelta(seconds=int(rem))) else: state['remaining'] = '--:--:--' return { 'progress': state['progress_pct'], 'scraped': state['total_scraped'], 'found': state['total_found'], 'not_found': state['total_not_found'], 'errors': state['total_errors'], 'elapsed': state['elapsed'], 'remaining': state['remaining'], 'rate': f"{state['rate']:.1f}/ثانية", 'stage': '🔄 تمرير أول' if not retry_mode else '🔁 إعادة غائبين', } def run_scrape(start, end, progress=gr.Progress()): state['running'] = True state['stage'] = 'running' state['total_scraped'] = 0 state['total_found'] = 0 state['total_not_found'] = 0 state['total_errors'] = 0 state['current_seat'] = start async def runner(): async for update in scrape_range(start, end): yield update loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) gen = runner() try: while True: try: upd = loop.run_until_complete(gen.__anext__()) progress(upd['progress'] / 100, desc=f"{upd['stage']} — {upd['scraped']:,}") yield upd except StopAsyncIteration: break finally: loop.close() state['running'] = False save_checkpoint() add_log("✅ السكريب اكتمل بالكامل") yield { 'progress': 100, 'scraped': state['total_scraped'], 'found': state['total_found'], 'not_found': state['total_not_found'], 'errors': state['total_errors'], 'elapsed': state['elapsed'], 'remaining': '00:00:00', 'rate': '0', 'stage': '✅ مكتمل' } def stop_scrape(): state['running'] = False add_log("⏹️ جاري إيقاف السكريب...") return "تم الإيقاف" def reset_state(): state['found'] = [] state['not_found_seats'] = [] state['total_scraped'] = 0 state['total_found'] = 0 state['total_not_found'] = 0 state['total_errors'] = 0 state['progress_pct'] = 0 state['elapsed'] = '00:00:00' state['remaining'] = '--:--:--' state['rate'] = 0 state['logs'] = [] state['stage'] = 'idle' add_log("🔄 تم تصفير الحالة") return "تم التصفير" ESTIMATED = """ ⏱️ **تقدير الوقت المستغرق:** | عدد الطلاب | زمن تقريبي | |---|---| | 10,000 | ~ 25 دقيقة | | 50,000 | ~ 2 ساعة | | 100,000 | ~ 4 ساعات | | 500,000 | ~ 20 ساعة | | 1,000,000 | ~ 40 ساعة (~1.7 يوم) | بافتراض 10 اتصالات متزامنة، متوسط ~3 ثواني لكل طالب في أسوأ الأحوال. """ with gr.Blocks(title="سكريب نتائج الصف التاسع - الدقهلية", theme=gr.themes.Soft(primary_hue="indigo")) as demo: gr.Markdown("# 📊 سكريب نتائج الصف التاسع الأساسي - الدقهلية") gr.Markdown("استخراج بيانات الطلاب من 1 إلى 1,000,000 من موقع النتيجة") with gr.Row(): with gr.Column(scale=2): start_box = gr.Number(label="بداية من رقم الجلوس", value=1, minimum=1, maximum=1_000_000, step=1) end_box = gr.Number(label="نهاية عند رقم الجلوس", value=1_000_000, minimum=1, maximum=1_000_000, step=1) with gr.Column(scale=1): gr.Markdown(ESTIMATED) with gr.Row(): start_btn = gr.Button("▶️ بدء السكريب", variant="primary", size="lg") stop_btn = gr.Button("⏹️ إيقاف", variant="stop", size="lg") reset_btn = gr.Button("🔄 تصفير", size="lg") with gr.Row(): with gr.Column(scale=2): progress_bar = gr.HTML(label="التقدم") stage_display = gr.Textbox(label="المرحلة", interactive=False) with gr.Column(scale=1): stats = gr.JSON(label="الإحصائيات", value={ 'تم السكريب': 0, 'موجود': 0, 'غير موجود': 0, 'أخطاء': 0, 'الوقت المنقضي': '00:00:00', 'المتبقي': '--:--:--', 'المعدل': 0 }) log_display = gr.Textbox(label="سجل الأحداث", lines=15, max_lines=30, interactive=False) output_files = gr.File(label="ملفات الإخراج", visible=True) def format_progress(pct): filled = int(pct / 5) bar = "█" * filled + "░" * (20 - filled) return f"""