AmmarBasha2011 commited on
Commit
65de7cb
·
verified ·
1 Parent(s): 80f02d7

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +14 -0
  2. app.py +314 -0
  3. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.14-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+
10
+ RUN mkdir -p /app/output
11
+
12
+ EXPOSE 7860
13
+
14
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import aiohttp
3
+ import pandas as pd
4
+ import json
5
+ import os
6
+ import time
7
+ import math
8
+ import sys
9
+ from datetime import datetime, timedelta
10
+ from pathlib import Path
11
+ import gradio as gr
12
+
13
+ API_URL = "https://natiga.edudk.net/P20262026/public/api_result.php"
14
+ OUTPUT_DIR = Path("output")
15
+ OUTPUT_DIR.mkdir(exist_ok=True)
16
+ CONCURRENCY = 10
17
+ CHECKPOINT_EVERY = 2000
18
+ MAX_RETRIES = 3
19
+
20
+ state = {
21
+ 'running': False,
22
+ 'stage': 'idle',
23
+ 'found': [],
24
+ 'not_found_seats': [],
25
+ 'total_scraped': 0,
26
+ 'total_found': 0,
27
+ 'total_not_found': 0,
28
+ 'total_errors': 0,
29
+ 'current_seat': 0,
30
+ 'start_time': None,
31
+ 'elapsed': '00:00:00',
32
+ 'remaining': '--:--:--',
33
+ 'rate': 0,
34
+ 'progress_pct': 0,
35
+ 'logs': [],
36
+ }
37
+
38
+ def add_log(msg):
39
+ ts = datetime.now().strftime("%H:%M:%S")
40
+ state['logs'].append(f"[{ts}] {msg}")
41
+ if len(state['logs']) > 500:
42
+ state['logs'] = state['logs'][-500:]
43
+
44
+ def save_checkpoint():
45
+ if state['found']:
46
+ df = pd.DataFrame(state['found'])
47
+ path = OUTPUT_DIR / "students_full.csv"
48
+ df.to_csv(path, index=False, encoding='utf-8-sig')
49
+ add_log(f"حفظ {len(state['found'])} طالب → students_full.csv")
50
+ if state['not_found_seats']:
51
+ pd.DataFrame({'seat_no': state['not_found_seats']}).to_csv(
52
+ OUTPUT_DIR / "not_found.csv", index=False, encoding='utf-8-sig')
53
+
54
+ async def fetch_one(session, sem, seat):
55
+ for attempt in range(MAX_RETRIES + 2):
56
+ try:
57
+ async with sem:
58
+ async with session.get(f"{API_URL}?seat={seat}", timeout=aiohttp.ClientTimeout(total=12)) as resp:
59
+ if resp.status == 429:
60
+ wait = 5 * (attempt + 1)
61
+ add_log(f"⏳ Rate-limit على المقعد {seat}، انتظار {wait}ث")
62
+ await asyncio.sleep(wait)
63
+ continue
64
+ body = await resp.text()
65
+ data = json.loads(body)
66
+ if data.get('ok') and data.get('data'):
67
+ return 'found', data['data']
68
+ return 'not_found', seat
69
+ except (asyncio.TimeoutError, aiohttp.ClientError) as e:
70
+ if attempt < MAX_RETRIES + 1:
71
+ await asyncio.sleep(1.5 ** attempt)
72
+ continue
73
+ return 'error', f"{seat}: {str(e)[:60]}"
74
+ except json.JSONDecodeError:
75
+ return 'error', f"{seat}: bad json"
76
+ return 'not_found', seat
77
+
78
+ async def scrape_range(start, end, retry_mode=False):
79
+ sem = asyncio.Semaphore(CONCURRENCY)
80
+ state['start_time'] = time.time()
81
+ total = end - start + 1
82
+ done = 0
83
+ connector = aiohttp.TCPConnector(limit=CONCURRENCY + 5, force_close=True)
84
+
85
+ async with aiohttp.ClientSession(connector=connector) as session:
86
+ batch = []
87
+ for seat in range(start, end + 1):
88
+ if not state['running']:
89
+ add_log("❌ تم إيقاف السكريب")
90
+ break
91
+ batch.append(fetch_one(session, sem, seat))
92
+ if len(batch) >= CONCURRENCY * 3:
93
+ yield await process_batch(batch, total, retry_mode)
94
+ done += len(batch)
95
+ batch = []
96
+ if batch:
97
+ yield await process_batch(batch, total, retry_mode)
98
+
99
+ if not retry_mode and state['running']:
100
+ add_log("🏁 انتهى الشوط الأول — بدء إعادة فحص الغائبين...")
101
+ save_checkpoint()
102
+ state['stage'] = 'retry'
103
+ nf = state['not_found_seats'][:]
104
+ if nf:
105
+ async for upd in scrape_range(min(nf), max(nf), retry_mode=True):
106
+ yield upd
107
+
108
+ async def process_batch(batch, grand_total, retry_mode):
109
+ results = await asyncio.gather(*batch)
110
+ newly_found = []
111
+ for status, data in results:
112
+ state['total_scraped'] += 1
113
+ if status == 'found':
114
+ state['total_found'] += 1
115
+ state['found'].append(data)
116
+ elif status == 'not_found':
117
+ state['total_not_found'] += 1
118
+ if not retry_mode:
119
+ state['not_found_seats'].append(data)
120
+ else:
121
+ state['total_errors'] += 1
122
+ add_log(f"⚠️ {data}")
123
+
124
+ if not retry_mode and len(state['found']) // CHECKPOINT_EVERY > (len(state['found']) - len(newly_found)) // CHECKPOINT_EVERY:
125
+ save_checkpoint()
126
+
127
+ elapsed = time.time() - state['start_time']
128
+ state['elapsed'] = str(timedelta(seconds=int(elapsed)))
129
+ state['progress_pct'] = min(100, state['total_scraped'] / grand_total * 100)
130
+ state['rate'] = state['total_scraped'] / elapsed if elapsed > 0 else 0
131
+ if state['rate'] > 0:
132
+ rem = (grand_total - state['total_scraped']) / state['rate']
133
+ state['remaining'] = str(timedelta(seconds=int(rem)))
134
+ else:
135
+ state['remaining'] = '--:--:--'
136
+
137
+ return {
138
+ 'progress': state['progress_pct'],
139
+ 'scraped': state['total_scraped'],
140
+ 'found': state['total_found'],
141
+ 'not_found': state['total_not_found'],
142
+ 'errors': state['total_errors'],
143
+ 'elapsed': state['elapsed'],
144
+ 'remaining': state['remaining'],
145
+ 'rate': f"{state['rate']:.1f}/ثانية",
146
+ 'stage': '🔄 تمرير أول' if not retry_mode else '🔁 إعادة غائبين',
147
+ }
148
+
149
+ def run_scrape(start, end, progress=gr.Progress()):
150
+ state['running'] = True
151
+ state['stage'] = 'running'
152
+ state['total_scraped'] = 0
153
+ state['total_found'] = 0
154
+ state['total_not_found'] = 0
155
+ state['total_errors'] = 0
156
+ state['current_seat'] = start
157
+
158
+ async def runner():
159
+ async for update in scrape_range(start, end):
160
+ yield update
161
+
162
+ loop = asyncio.new_event_loop()
163
+ asyncio.set_event_loop(loop)
164
+ gen = runner()
165
+ try:
166
+ while True:
167
+ try:
168
+ upd = loop.run_until_complete(gen.__anext__())
169
+ progress(upd['progress'] / 100, desc=f"{upd['stage']} — {upd['scraped']:,}")
170
+ yield upd
171
+ except StopAsyncIteration:
172
+ break
173
+ finally:
174
+ loop.close()
175
+ state['running'] = False
176
+ save_checkpoint()
177
+ add_log("✅ السكريب اكتمل بالكامل")
178
+ yield {
179
+ 'progress': 100, 'scraped': state['total_scraped'],
180
+ 'found': state['total_found'], 'not_found': state['total_not_found'],
181
+ 'errors': state['total_errors'], 'elapsed': state['elapsed'],
182
+ 'remaining': '00:00:00', 'rate': '0', 'stage': '✅ مكتمل'
183
+ }
184
+
185
+ def stop_scrape():
186
+ state['running'] = False
187
+ add_log("⏹️ جاري إيقاف السكريب...")
188
+ return "تم الإيقاف"
189
+
190
+ def reset_state():
191
+ state['found'] = []
192
+ state['not_found_seats'] = []
193
+ state['total_scraped'] = 0
194
+ state['total_found'] = 0
195
+ state['total_not_found'] = 0
196
+ state['total_errors'] = 0
197
+ state['progress_pct'] = 0
198
+ state['elapsed'] = '00:00:00'
199
+ state['remaining'] = '--:--:--'
200
+ state['rate'] = 0
201
+ state['logs'] = []
202
+ state['stage'] = 'idle'
203
+ add_log("🔄 تم تصفير الحالة")
204
+ return "تم التصفير"
205
+
206
+ ESTIMATED = """
207
+ ⏱️ **تقدير الوقت المستغرق:**
208
+
209
+ | عدد الطلاب | زمن تقريبي |
210
+ |---|---|
211
+ | 10,000 | ~ 25 دقيقة |
212
+ | 50,000 | ~ 2 ساعة |
213
+ | 100,000 | ~ 4 ساعات |
214
+ | 500,000 | ~ 20 ساعة |
215
+ | 1,000,000 | ~ 40 ساعة (~1.7 يوم) |
216
+
217
+ بافتراض 10 اتصالات متزامنة، متوسط ~3 ثواني لكل طالب في أسوأ الأحوال.
218
+ """
219
+
220
+ with gr.Blocks(title="سكريب نتائج الصف التاسع - الدقهلية", theme=gr.themes.Soft(primary_hue="indigo")) as demo:
221
+ gr.Markdown("# 📊 سكريب نتائج الصف التاسع الأساسي - الدقهلية")
222
+ gr.Markdown("استخراج بيانات الطلاب من 1 إلى 1,000,000 من موقع النتيجة")
223
+
224
+ with gr.Row():
225
+ with gr.Column(scale=2):
226
+ start_box = gr.Number(label="بداية من رقم الجلوس", value=1, minimum=1, maximum=1_000_000, step=1)
227
+ end_box = gr.Number(label="نهاية عند رقم الجلوس", value=1_000_000, minimum=1, maximum=1_000_000, step=1)
228
+
229
+ with gr.Column(scale=1):
230
+ gr.Markdown(ESTIMATED)
231
+
232
+ with gr.Row():
233
+ start_btn = gr.Button("▶️ بدء السكريب", variant="primary", size="lg")
234
+ stop_btn = gr.Button("⏹️ إيقاف", variant="stop", size="lg")
235
+ reset_btn = gr.Button("🔄 تصفير", size="lg")
236
+
237
+ with gr.Row():
238
+ with gr.Column(scale=2):
239
+ progress_bar = gr.HTML(label="التقدم")
240
+ stage_display = gr.Textbox(label="المرحلة", interactive=False)
241
+
242
+ with gr.Column(scale=1):
243
+ stats = gr.JSON(label="الإحصائيات", value={
244
+ 'تم السكريب': 0, 'موجود': 0, 'غير موجود': 0, 'أخطاء': 0,
245
+ 'الوقت المنقضي': '00:00:00', 'المتبقي': '--:--:--', 'المعدل': 0
246
+ })
247
+
248
+ log_display = gr.Textbox(label="سجل الأحداث", lines=15, max_lines=30, interactive=False)
249
+ output_files = gr.File(label="ملفات الإخراج", visible=True)
250
+
251
+ def format_progress(pct):
252
+ filled = int(pct / 5)
253
+ bar = "█" * filled + "░" * (20 - filled)
254
+ return f"""
255
+ <div style="background:#1f2937;border-radius:12px;padding:16px;text-align:center">
256
+ <div style="font-size:2rem;font-weight:800;color:#818cf8">{pct:.1f}%</div>
257
+ <div style="font-family:monospace;font-size:1.1rem;color:#e2e8f0;letter-spacing:2px">{bar}</div>
258
+ </div>
259
+ """
260
+
261
+ def on_start(start, end):
262
+ if start > end:
263
+ return [
264
+ format_progress(0), "⚠️ البداية أكبر من النهاية",
265
+ {'error': 'تأكد من الأرقام'}, '', None
266
+ ]
267
+ if state['running']:
268
+ return [
269
+ format_progress(state['progress_pct']),
270
+ "⚠️ السكريب يعمل بالفعل", stats.value, '', None
271
+ ]
272
+ state['running'] = True
273
+ for upd in run_scrape(int(start), int(end)):
274
+ stats_val = {
275
+ 'تم السكريب': upd['scraped'],
276
+ 'موجود': upd['found'],
277
+ 'غير موجود': upd['not_found'],
278
+ 'أخطاء': upd['errors'],
279
+ 'الوقت المنقضي': upd['elapsed'],
280
+ 'المتبقي': upd['remaining'],
281
+ 'المعدل': upd['rate'],
282
+ }
283
+ log_text = "\n".join(state['logs'][-30:])
284
+ yield [
285
+ format_progress(upd['progress']),
286
+ f"{upd['stage']} — {upd['scraped']:,} / 1,000,000",
287
+ stats_val,
288
+ log_text,
289
+ OUTPUT_DIR / "students_full.csv" if Path(OUTPUT_DIR / "students_full.csv").exists() else None
290
+ ]
291
+
292
+ start_btn.click(
293
+ fn=on_start,
294
+ inputs=[start_box, end_box],
295
+ outputs=[progress_bar, stage_display, stats, log_display, output_files]
296
+ )
297
+
298
+ stop_btn.click(fn=stop_scrape, outputs=[stage_display])
299
+
300
+ def on_reset():
301
+ reset_state()
302
+ return [
303
+ format_progress(0), "🔄 تم التصفير - جاهز",
304
+ {'تم السكريب': 0, 'موجود': 0, 'غير موجود': 0, 'أخطاء': 0,
305
+ 'الوقت المنقضي': '00:00:00', 'المتبقي': '--:--:--', 'المعدل': 0},
306
+ "", None
307
+ ]
308
+
309
+ reset_btn.click(fn=on_reset, outputs=[progress_bar, stage_display, stats, log_display, output_files])
310
+
311
+ iface = demo
312
+
313
+ if __name__ == "__main__":
314
+ iface.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==5.23.3
2
+ aiohttp==3.11.16
3
+ pandas==3.0.3
4
+ openpyxl==3.1.5