lljz66 commited on
Commit
342a97d
·
verified ·
1 Parent(s): 3ee87fd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -103
app.py CHANGED
@@ -1,10 +1,10 @@
1
  from playwright.sync_api import sync_playwright
2
- import os, json, time, logging, zipfile, gzip
3
  from urllib.parse import urlparse, parse_qs
4
  from huggingface_hub import HfApi
5
 
6
  # =========================
7
- # LOGGING (احترافي بدون إيموجي)
8
  # =========================
9
  logging.basicConfig(
10
  level=logging.INFO,
@@ -14,105 +14,163 @@ logging.basicConfig(
14
  log = logging.getLogger(__name__)
15
 
16
  # =========================
17
- # HELPERS
18
  # =========================
 
19
  def extract_id(url):
 
20
  try:
21
  return parse_qs(urlparse(url).query).get("refConsultation", [""])[0]
22
  except:
23
  return "UNKNOWN"
24
 
25
  def extract_field_value(page, label_text):
26
- """استخراج القيمة من 'Label : Value'"""
 
 
 
27
  try:
28
- element = page.locator(f"text={label_text}").first
29
- if element.count() == 0:
30
- return ""
31
- full = element.inner_text().strip()
32
- for sep in [" : ", ":"]:
33
- if sep in full:
34
- return full.split(sep, 1)[1].strip()
35
- return full.replace(label_text, "").strip()
36
- except:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  return ""
38
 
39
  def safe_download(page, locator, file_prefix, download_dir="downloads"):
40
- """تحميل ملف وإرجاع (success, local_path, message)"""
41
  os.makedirs(download_dir, exist_ok=True)
42
  if locator.count() == 0:
43
  return False, None, "Not found"
 
44
  try:
45
- with page.expect_download(timeout=15000) as dl:
46
  locator.first.click()
47
- download = dl.value
48
- ext = os.path.splitext(download.suggested_filename or "file")[1] or ".bin"
 
 
 
 
 
 
49
  local_path = os.path.join(download_dir, f"{file_prefix}{ext}")
50
  download.save_as(local_path)
51
  return True, local_path, "Success"
52
  except Exception as e:
53
  return False, None, str(e)
54
 
55
- def compress_file(input_path, output_path=None):
56
- """ضغط ملف باستخدام gzip لتوفير المساحة"""
57
- if output_path is None:
58
- output_path = input_path + ".gz"
59
  with open(input_path, 'rb') as f_in, gzip.open(output_path, 'wb') as f_out:
60
- f_out.writelines(f_in)
61
  return output_path
62
 
63
  def upload_consultation_to_hf(consultation_id, metadata, files_dict, repo_id, api):
64
  """
65
- رفع استشارة كاملة:
66
- - ملف تعريف: data/consultations/{id}/metadata.json
67
- - ملفات مرفقة: data/consultations/{id}/files/{filename}[.gz]
68
  """
69
- base_path = f"data/consultations/{consultation_id}"
70
 
71
- # 1. رفع metadata
72
- meta_local = f"temp_meta_{consultation_id}.json"
73
- with open(meta_local, "w", encoding="utf-8") as f:
74
- json.dump(metadata, f, ensure_ascii=False, indent=2)
75
- api.upload_file(
76
- path_or_fileobj=meta_local,
77
- path_in_repo=f"{base_path}/metadata.json",
78
- repo_id=repo_id,
79
- repo_type="dataset"
80
- )
81
- os.remove(meta_local)
82
- log.info(f"[UPLOAD] {consultation_id}/metadata.json")
83
-
84
- # 2. رفع الملفات المرفقة (مع ضغط اختياري)
 
 
 
 
85
  for file_type, local_path in files_dict.items():
86
  if not local_path or not os.path.exists(local_path):
87
  continue
88
- # ضغط إذا كان الملف > 5MB لتوفير المساحة
89
- if os.path.getsize(local_path) > 5 * 1024 * 1024:
90
- compressed = compress_file(local_path)
91
- upload_path = f"{base_path}/files/{os.path.basename(compressed)}"
92
- api.upload_file(
93
- path_or_fileobj=compressed,
94
- path_in_repo=upload_path,
95
- repo_id=repo_id,
96
- repo_type="dataset"
97
- )
98
- os.remove(compressed)
99
- log.info(f"[UPLOAD] {upload_path} (compressed)")
100
- else:
101
- upload_path = f"{base_path}/files/{os.path.basename(local_path)}"
102
  api.upload_file(
103
- path_or_fileobj=local_path,
104
- path_in_repo=upload_path,
105
  repo_id=repo_id,
106
  repo_type="dataset"
107
  )
108
- log.info(f"[UPLOAD] {upload_path}")
 
 
 
 
 
 
109
 
110
  # =========================
111
- # MAIN
112
  # =========================
113
  def run():
114
  repo_id = "lljz66/opentender_morocco_data"
115
- api = HfApi() # HF_TOKEN من البيئة
 
116
 
117
  with sync_playwright() as p:
118
  browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
@@ -120,49 +178,69 @@ def run():
120
  page = context.new_page()
121
 
122
  log.info("Starting crawl...")
 
 
123
  page.goto("https://www.marchespublics.gov.ma/index.php?page=entreprise.EntrepriseAdvancedSearch&searchAnnCons&consAnnulee=1")
124
  page.wait_for_load_state("domcontentloaded")
125
 
126
- # === إعداد البحث ===
 
127
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneStart").fill("25/04/2026")
128
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneEnd").fill("25/10/2026")
129
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneCalculeStart").fill("25/10/2025")
130
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneCalculeEnd").fill("25/04/2026")
 
 
131
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_lancerRecherche").click()
132
- page.wait_for_timeout(4000)
133
 
134
- max_test = 10
135
- processed = 0
136
 
137
  for i in range(max_test):
138
  log.info(f"Processing row {i+1}/{max_test}")
139
 
140
- # ⚠️ إعادة بناء locator في كل مرة
141
  rows = page.locator("tr:has(div[id*='panelAction'])")
 
 
 
 
 
 
142
  row = rows.nth(i)
143
  link = row.locator("a[href*='EntrepriseDetailConsultation']").first
144
 
 
145
  try:
146
  url = link.get_attribute("href", timeout=5000)
147
  except:
148
- log.warning(f"Failed to get href for row {i+1}, skipping")
149
  continue
 
150
  if not url:
151
  continue
152
 
153
  full_url = "https://www.marchespublics.gov.ma/" + url
154
  log.info(f"Opening: {full_url}")
155
 
156
- # === فتح التفاصيل ===
157
  link.click()
158
  page.wait_for_load_state("domcontentloaded")
 
 
 
159
  consultation_id = extract_id(page.url)
160
  if not consultation_id or consultation_id == "UNKNOWN":
161
- log.warning("Could not extract consultation_id, skipping")
 
 
 
162
  continue
 
163
  log.info(f"Consultation ID: {consultation_id}")
164
 
165
- # === استخراج البيانات ===
166
  metadata = {
167
  "consultation_id": consultation_id,
168
  "reference": extract_field_value(page, "Référence"),
@@ -172,81 +250,105 @@ def run():
172
  "source_url": full_url,
173
  "scraped_at": time.strftime("%Y-%m-%d %H:%M:%S")
174
  }
175
- log.info(f"Extracted: ref={metadata['reference']}, date_limite={metadata['date_limite']}")
 
176
 
177
- # === التبويب الوثائق ===
178
- if page.locator("#ongletLayer1").count() > 0:
179
- page.locator("#ongletLayer1").click()
 
 
180
  page.wait_for_timeout(2000)
181
 
182
- files = {} # {type: local_path}
183
 
184
- # --- RC ---
185
- rc_ok, rc_path, rc_msg = safe_download(page, page.locator("text=Règlement de consultation"), f"rc_{consultation_id}")
 
186
  metadata["rc_downloaded"] = rc_ok
187
  if rc_ok and rc_path:
188
- files["rc"] = rc_path
189
- log.info(f"[RC] Downloaded: {rc_path}")
190
  elif rc_msg != "Not found":
191
  log.warning(f"[RC] Failed: {rc_msg}")
192
 
193
- # --- PV (نص) ---
 
194
  pv_locator = page.locator("text=Extrait de PV").first
195
- if pv_locator.count() > 0:
196
  try:
197
  pv_locator.click()
198
  page.wait_for_load_state("domcontentloaded")
199
  page.wait_for_timeout(1500)
 
200
  pv_text = page.locator("body").inner_text().strip()
201
  metadata["pv_available"] = True
202
  metadata["pv_preview"] = pv_text[:300]
203
- # حفظ النص الكامل كملف منفصل إذا رغبت
 
204
  pv_file = f"downloads/pv_{consultation_id}.txt"
205
  with open(pv_file, "w", encoding="utf-8") as f:
206
  f.write(pv_text)
207
- files["pv"] = pv_file
208
- log.info("[PV] Extracted")
209
- # عودة
 
210
  for _ in range(2):
211
- ret = page.locator("#ctl0_CONTENU_PAGE_linkRetourBas").first
212
- if ret.is_visible():
213
- ret.click()
214
  page.wait_for_load_state("domcontentloaded")
215
  page.wait_for_timeout(1000)
216
  except Exception as e:
217
- log.warning(f"[PV] Error: {e}")
218
  metadata["pv_available"] = False
 
 
 
 
 
219
  else:
220
  metadata["pv_available"] = False
221
 
222
- # --- AVIS ---
223
- avis_ok, avis_path, avis_msg = safe_download(page, page.locator("text=Fichier joint"), f"avis_{consultation_id}")
 
 
 
 
 
224
  metadata["avis_downloaded"] = avis_ok
225
  if avis_ok and avis_path:
226
- files["avis"] = avis_path
227
- log.info(f"[AVIS] Downloaded: {avis_path}")
228
  elif avis_msg != "Not found":
229
  log.warning(f"[AVIS] Failed: {avis_msg}")
230
 
231
- # === العودة للقائمة ===
232
  try:
233
- page.locator("#ctl0_CONTENU_PAGE_linkRetourBas").click()
 
 
 
 
 
 
234
  page.wait_for_load_state("domcontentloaded")
235
- page.wait_for_timeout(1500)
 
236
  except Exception as e:
237
- log.error(f"Return failed: {e}")
238
- break
239
 
240
- # === رفع فوري لهذه الاستشارة ===
241
  try:
242
- upload_consultation_to_hf(consultation_id, metadata, files, repo_id, api)
243
- processed += 1
244
- log.info(f"[COMPLETE] {consultation_id} uploaded ({processed}/{max_test})")
245
  except Exception as e:
246
- log.error(f"[UPLOAD ERROR] {consultation_id}: {e}")
247
- # متابعة مع الباقي بدلاً من التوقف
248
 
249
- log.info(f"Crawl finished. Processed: {processed}/{max_test}")
250
  browser.close()
251
 
252
  if __name__ == "__main__":
 
1
  from playwright.sync_api import sync_playwright
2
+ import os, json, time, logging, gzip, shutil
3
  from urllib.parse import urlparse, parse_qs
4
  from huggingface_hub import HfApi
5
 
6
  # =========================
7
+ # 1. إعداد السجلات (Logging)
8
  # =========================
9
  logging.basicConfig(
10
  level=logging.INFO,
 
14
  log = logging.getLogger(__name__)
15
 
16
  # =========================
17
+ # 2. الدوال المساعدة (Helpers)
18
  # =========================
19
+
20
  def extract_id(url):
21
+ """استخراج معرف الاستشارة من الرابط"""
22
  try:
23
  return parse_qs(urlparse(url).query).get("refConsultation", [""])[0]
24
  except:
25
  return "UNKNOWN"
26
 
27
  def extract_field_value(page, label_text):
28
+ """
29
+ استخراج القيمة لحقل معين بذكاء.
30
+ يبحث عن التسمية، ثم يحاول استخراج النص المجاور لها أو الذي يليها.
31
+ """
32
  try:
33
+ # نبحث عن العناصر التي تحتوي على نص التسمية
34
+ # نستخدم first للحصول على أقرب تطابق، لكن قد نحتاج لتجربة عدة عناصر إذا فشل الأول
35
+ locators = page.locator(f"text={label_text}").all()
36
+
37
+ for el in locators:
38
+ if not el.is_visible():
39
+ continue
40
+
41
+ # الطريقة 1: التحقق من النص الكامل للعنصر (غالباً التسمية والقيمة في نفس الـ td/div)
42
+ full_text = el.inner_text().strip()
43
+
44
+ # تنظيف النص من الرموز غير المرئية
45
+ clean_full = " ".join(full_text.split())
46
+
47
+ # محاولة الفصل بالنقطتين (العربية أو الإنجليزية مع مسافات)
48
+ for sep in [" : ", ":", " : "]:
49
+ if sep in clean_full:
50
+ parts = clean_full.split(sep, 1)
51
+ if len(parts) > 1:
52
+ val = parts[1].strip()
53
+ if val and val != label_text:
54
+ return val
55
+
56
+ # الطريقة 2: إذا لم نجد فاصلاً، قد تكون القيمة في عنصر sibling (td التالي مثلاً)
57
+ # نحاول الوصول للأب ثم نبحث عن td آخر
58
+ parent = el.locator("xpath=..")
59
+ if parent.count() > 0:
60
+ # البحث عن أي td أو div داخل الأب لا يحتوي على التسمية نفسها (لنفترض أنه القيمة)
61
+ # هذه طريقة تقريبية تعتمد على هيكل الجداول الشائع
62
+ siblings = parent.locator("td, div").all()
63
+ for sib in siblings:
64
+ sib_text = sib.inner_text().strip()
65
+ if sib_text and label_text not in sib_text and len(sib_text) > 2:
66
+ return sib_text
67
+
68
+ # الطريقة 3: إذا كان النص طويلاً ويحتوي على التسمية في البداية فقط
69
+ if clean_full.startswith(label_text):
70
+ val = clean_full[len(label_text):].strip()
71
+ # إزالة النقطتين في البداية إذا وجدت
72
+ val = val.lstrip(":").strip()
73
+ if val:
74
+ return val
75
+
76
+ return ""
77
+ except Exception as e:
78
+ # log.debug(f"Error extracting {label_text}: {e}")
79
  return ""
80
 
81
  def safe_download(page, locator, file_prefix, download_dir="downloads"):
82
+ """تحميل ملف بأمان وإرجاع المسار المحلي"""
83
  os.makedirs(download_dir, exist_ok=True)
84
  if locator.count() == 0:
85
  return False, None, "Not found"
86
+
87
  try:
88
+ with page.expect_download(timeout=20000) as dl_info:
89
  locator.first.click()
90
+ download = dl_info.value
91
+
92
+ # تحديد الامتداد
93
+ suggested_name = download.suggested_filename or "file"
94
+ _, ext = os.path.splitext(suggested_name)
95
+ if not ext:
96
+ ext = ".bin"
97
+
98
  local_path = os.path.join(download_dir, f"{file_prefix}{ext}")
99
  download.save_as(local_path)
100
  return True, local_path, "Success"
101
  except Exception as e:
102
  return False, None, str(e)
103
 
104
+ def compress_file_gzip(input_path):
105
+ """ضغط ملف باستخدام gzip لتوفير المساحة على HF"""
106
+ output_path = input_path + ".gz"
 
107
  with open(input_path, 'rb') as f_in, gzip.open(output_path, 'wb') as f_out:
108
+ shutil.copyfileobj(f_in, f_out)
109
  return output_path
110
 
111
  def upload_consultation_to_hf(consultation_id, metadata, files_dict, repo_id, api):
112
  """
113
+ رفع بيانات استشارة واحدة إلى Hugging Face Dataset
114
+ الهيكل: data/consultations/{ID}/metadata.json + files/
 
115
  """
116
+ base_repo_path = f"data/consultations/{consultation_id}"
117
 
118
+ # 1. رفع ملف البيانات الوصفية (Metadata)
119
+ meta_filename = f"temp_meta_{consultation_id}.json"
120
+ try:
121
+ with open(meta_filename, "w", encoding="utf-8") as f:
122
+ json.dump(metadata, f, ensure_ascii=False, indent=2)
123
+
124
+ api.upload_file(
125
+ path_or_fileobj=meta_filename,
126
+ path_in_repo=f"{base_repo_path}/metadata.json",
127
+ repo_id=repo_id,
128
+ repo_type="dataset"
129
+ )
130
+ log.info(f"[UPLOAD] {base_repo_path}/metadata.json")
131
+ finally:
132
+ if os.path.exists(meta_filename):
133
+ os.remove(meta_filename)
134
+
135
+ # 2. رفع الملفات المرفقة (Files)
136
  for file_type, local_path in files_dict.items():
137
  if not local_path or not os.path.exists(local_path):
138
  continue
139
+
140
+ file_name = os.path.basename(local_path)
141
+ repo_file_path = f"{base_repo_path}/files/{file_name}"
142
+
143
+ # ضغط الملفات الكبيرة (> 5MB) لتوفير المساحة
144
+ file_size = os.path.getsize(local_path)
145
+ upload_path = local_path
146
+
147
+ if file_size > 5 * 1024 * 1024:
148
+ log.info(f"[COMPRESS] Compressing {file_name} ({file_size/1024/1024:.2f} MB)...")
149
+ upload_path = compress_file_gzip(local_path)
150
+ repo_file_path += ".gz"
151
+
152
+ try:
153
  api.upload_file(
154
+ path_or_fileobj=upload_path,
155
+ path_in_repo=repo_file_path,
156
  repo_id=repo_id,
157
  repo_type="dataset"
158
  )
159
+ log.info(f"[UPLOAD] {repo_file_path}")
160
+ except Exception as e:
161
+ log.error(f"[UPLOAD ERROR] Failed to upload {file_name}: {e}")
162
+ finally:
163
+ # حذف الملف المضغوط المؤقت إذا وجد
164
+ if upload_path != local_path and os.path.exists(upload_path):
165
+ os.remove(upload_path)
166
 
167
  # =========================
168
+ # 3. الوظيفة الرئيسية (Main Run)
169
  # =========================
170
  def run():
171
  repo_id = "lljz66/opentender_morocco_data"
172
+ # تأكد من وجود متغير البيئة HF_TOKEN
173
+ api = HfApi()
174
 
175
  with sync_playwright() as p:
176
  browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
 
178
  page = context.new_page()
179
 
180
  log.info("Starting crawl...")
181
+
182
+ # الذهاب لصفحة البحث المتقدم
183
  page.goto("https://www.marchespublics.gov.ma/index.php?page=entreprise.EntrepriseAdvancedSearch&searchAnnCons&consAnnulee=1")
184
  page.wait_for_load_state("domcontentloaded")
185
 
186
+ # === إعداد معايير البحث ===
187
+ # ملاحظة: عدل التواريخ حسب الحاجة
188
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneStart").fill("25/04/2026")
189
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneEnd").fill("25/10/2026")
190
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneCalculeStart").fill("25/10/2025")
191
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_dateMiseEnLigneCalculeEnd").fill("25/04/2026")
192
+
193
+ # النقر على بحث
194
  page.locator("#ctl0_CONTENU_PAGE_AdvancedSearch_lancerRecherche").click()
195
+ page.wait_for_timeout(5000) # انتظار تحميل النتائج
196
 
197
+ max_test = 10 # عدد الصفوف للتجربة
198
+ processed_count = 0
199
 
200
  for i in range(max_test):
201
  log.info(f"Processing row {i+1}/{max_test}")
202
 
203
+ # ⚠️ هام: إعادة تعريف locator للصفوف في كل مرة لتجنب مشكلة Stale Element
204
  rows = page.locator("tr:has(div[id*='panelAction'])")
205
+
206
+ # التحقق من وجود الصف
207
+ if rows.count() <= i:
208
+ log.warning(f"Row {i+1} not found. Ending loop.")
209
+ break
210
+
211
  row = rows.nth(i)
212
  link = row.locator("a[href*='EntrepriseDetailConsultation']").first
213
 
214
+ # استخراج الرابط
215
  try:
216
  url = link.get_attribute("href", timeout=5000)
217
  except:
218
+ log.warning(f"Could not get href for row {i+1}. Skipping.")
219
  continue
220
+
221
  if not url:
222
  continue
223
 
224
  full_url = "https://www.marchespublics.gov.ma/" + url
225
  log.info(f"Opening: {full_url}")
226
 
227
+ # === فتح صفحة التفاصيل ===
228
  link.click()
229
  page.wait_for_load_state("domcontentloaded")
230
+ # انتظار إضافي لضمان تحميل المحتوى الديناميكي
231
+ page.wait_for_timeout(2000)
232
+
233
  consultation_id = extract_id(page.url)
234
  if not consultation_id or consultation_id == "UNKNOWN":
235
+ log.warning("Could not extract Consultation ID. Skipping.")
236
+ # محاولة العودة قبل المتابعة
237
+ try: page.go_back(); page.wait_for_timeout(2000)
238
+ except: pass
239
  continue
240
+
241
  log.info(f"Consultation ID: {consultation_id}")
242
 
243
+ # === استخراج البيانات النصية ===
244
  metadata = {
245
  "consultation_id": consultation_id,
246
  "reference": extract_field_value(page, "Référence"),
 
250
  "source_url": full_url,
251
  "scraped_at": time.strftime("%Y-%m-%d %H:%M:%S")
252
  }
253
+
254
+ log.info(f"Extracted Data -> Ref: '{metadata['reference'][:20]}...', DateLim: '{metadata['date_limite']}'")
255
 
256
+ # === التنقل لتبويب الوثائق (Onglet 1) ===
257
+ # نتحقق من وجود التبويب وننقر عليه إذا لزم الأمر
258
+ onglet = page.locator("#ongletLayer1").first
259
+ if onglet.is_visible():
260
+ onglet.click()
261
  page.wait_for_timeout(2000)
262
 
263
+ files_dict = {} # لتخزين مسارات الملفات المحلية
264
 
265
+ # --- 1. معالجة RC (Règlement de consultation) ---
266
+ rc_locator = page.locator("text=Règlement de consultation").first
267
+ rc_ok, rc_path, rc_msg = safe_download(page, rc_locator, f"rc_{consultation_id}")
268
  metadata["rc_downloaded"] = rc_ok
269
  if rc_ok and rc_path:
270
+ files_dict["rc"] = rc_path
271
+ log.info(f"[RC] Downloaded: {os.path.basename(rc_path)}")
272
  elif rc_msg != "Not found":
273
  log.warning(f"[RC] Failed: {rc_msg}")
274
 
275
+ # --- 2. معالجة PV (Extrait de PV) ---
276
+ # الـ PV غالباً صفحة نصية وليست ملف تحميل مباشر
277
  pv_locator = page.locator("text=Extrait de PV").first
278
+ if pv_locator.is_visible():
279
  try:
280
  pv_locator.click()
281
  page.wait_for_load_state("domcontentloaded")
282
  page.wait_for_timeout(1500)
283
+
284
  pv_text = page.locator("body").inner_text().strip()
285
  metadata["pv_available"] = True
286
  metadata["pv_preview"] = pv_text[:300]
287
+
288
+ # حفظ النص كملف محلي
289
  pv_file = f"downloads/pv_{consultation_id}.txt"
290
  with open(pv_file, "w", encoding="utf-8") as f:
291
  f.write(pv_text)
292
+ files_dict["pv"] = pv_file
293
+ log.info("[PV] Extracted and saved")
294
+
295
+ # العودة مرتين (كما في المنطق السابق الناجح)
296
  for _ in range(2):
297
+ ret_btn = page.locator("#ctl0_CONTENU_PAGE_linkRetourBas").first
298
+ if ret_btn.is_visible():
299
+ ret_btn.click()
300
  page.wait_for_load_state("domcontentloaded")
301
  page.wait_for_timeout(1000)
302
  except Exception as e:
303
+ log.warning(f"[PV] Error processing: {e}")
304
  metadata["pv_available"] = False
305
+ # محاولة عودة طارئة
306
+ try:
307
+ page.locator("#ctl0_CONTENU_PAGE_linkRetourBas").click()
308
+ page.wait_for_timeout(1000)
309
+ except: pass
310
  else:
311
  metadata["pv_available"] = False
312
 
313
+ # --- 3. معالجة AVIS (Fichier joint / Avis) ---
314
+ # نبحث عن "Fichier joint" أو "Avis de..."
315
+ avis_locator = page.locator("text=Fichier joint").first
316
+ if not avis_locator.is_visible():
317
+ avis_locator = page.locator("text=Avis de").first # محاولة بديلة
318
+
319
+ avis_ok, avis_path, avis_msg = safe_download(page, avis_locator, f"avis_{consultation_id}")
320
  metadata["avis_downloaded"] = avis_ok
321
  if avis_ok and avis_path:
322
+ files_dict["avis"] = avis_path
323
+ log.info(f"[AVIS] Downloaded: {os.path.basename(avis_path)}")
324
  elif avis_msg != "Not found":
325
  log.warning(f"[AVIS] Failed: {avis_msg}")
326
 
327
+ # === العودة لصفحة النتائج ===
328
  try:
329
+ # استخدام الزر المحدد للعودة
330
+ back_btn = page.locator("#ctl0_CONTENU_PAGE_linkRetourBas").first
331
+ if back_btn.is_visible():
332
+ back_btn.click()
333
+ else:
334
+ page.go_back() # بديل
335
+
336
  page.wait_for_load_state("domcontentloaded")
337
+ page.wait_for_timeout(2000) # استقرار الصفحة
338
+ log.info("Returned to search results")
339
  except Exception as e:
340
+ log.error(f"Failed to return to search page: {e}")
341
+ break # إيقاف الحلقة إذا فشل العودة
342
 
343
+ # === الرفع إلى Hugging Face ===
344
  try:
345
+ upload_consultation_to_hf(consultation_id, metadata, files_dict, repo_id, api)
346
+ processed_count += 1
347
+ log.info(f"[COMPLETE] {consultation_id} successfully processed and uploaded.")
348
  except Exception as e:
349
+ log.error(f"[FATAL UPLOAD ERROR] for {consultation_id}: {e}")
 
350
 
351
+ log.info(f"Crawl finished. Total processed: {processed_count}")
352
  browser.close()
353
 
354
  if __name__ == "__main__":