EGYADMIN commited on
Commit
cfb6914
·
verified ·
1 Parent(s): 854f5fb

Update modules/document_processor.py

Browse files
Files changed (1) hide show
  1. modules/document_processor.py +17 -894
modules/document_processor.py CHANGED
@@ -11,7 +11,7 @@ from datetime import datetime
11
  import docx
12
  import PyPDF2
13
  import fitz # PyMuPDF
14
- import textract
15
  import mammoth
16
  from openpyxl import load_workbook
17
  from PIL import Image
@@ -50,121 +50,15 @@ class DocumentProcessor:
50
  # الكلمات التوقفية في اللغة العربية
51
  self.arabic_stopwords = set(stopwords.words('arabic'))
52
 
53
- # تعريف أنماط التعبيرات المنتظمة
54
- self.regex_patterns = {
55
- "money": r'(\d[\d,.]*)\s*(ريال|ر\.س|SAR|ر\.س\.)',
56
- "percentage": r'(\d[\d,.]*)\s*(%|في المائة|في المئة|بالمائة|بالمئة)',
57
- "date": r'(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})|(\d{1,2})\s+(يناير|فبراير|مارس|أبريل|مايو|يونيو|يوليو|أغسطس|سبتمبر|أكتوبر|نوفمبر|ديسمبر)\s+(\d{2,4})',
58
- "email": r'[\w\.-]+@[\w\.-]+\.\w+',
59
- "phone": r'([\+]?[\d]{1,3}[\s-]?)?(\d{3,4})[\s-]?(\d{3,4})[\s-]?(\d{3,4})',
60
- "url": r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+'
61
- }
62
-
63
- # قائمة بكلمات المناقصات الهامة
64
- self.important_tender_terms = [
65
- "مناقصة", "عطاء", "ترسية", "عقد", "مشروع", "تسليم", "اجتماع", "تمهيدي",
66
- "ضمان", "كفالة", "ابتدائي", "نهائي", "غرامة", "غرامات", "جزائية", "صيانة",
67
- "ضمان", "تمديد", "تأجيل", "إلغاء", "تعديل", "ملحق", "مصنع محلي", "مستورد",
68
- "المحتوى المحلي", "التقييم الفني", "التقييم المالي", "العرض الفني", "العرض المالي"
69
- ]
70
-
71
- def _load_tender_keywords(self) -> Dict[str, List[str]]:
72
- """
73
- تحميل الكلمات الدلالية المتعلقة بالمناقصات وتصنيفها
74
- """
75
- # في التطبيق الفعلي، قد تُحمل هذه الكلمات من ملف أو قاعدة بيانات
76
- return {
77
- "requirements": [
78
- "متطلبات", "شروط", "مواصفات", "معايير",
79
- "يجب", "يتعين", "ضرورة", "إلزامي", "إلزامية",
80
- "المتطلبات الفنية", "المتطلبات الإدارية", "الاشتراطات"
81
- ],
82
- "costs": [
83
- "تكلفة", "تكاليف", "سعر", "أسعار", "ميزانية",
84
- "قيمة", "مالي", "مالية", "تمويل", "تقدير مالي",
85
- "ريال", "ريال سعودي", "سعودي"
86
- ],
87
- "dates": [
88
- "تاريخ", "مدة", "جدول زمني", "موعد", "مهلة",
89
- "التسليم", "الاستحقاق", "بداية", "نهاية", "أيام",
90
- "أسابيع", "شهور", "سنوات"
91
- ],
92
- "local_content": [
93
- "محتوى محلي", "توطين", "نطاقات", "سعودة",
94
- "وطني", "محلية", "إنتاج محلي", "صناعة محلية",
95
- "منتجات وطنية", "خدمات وطنية", "منشأ سعودي",
96
- "رؤية 2030", "رؤية المملكة"
97
- ],
98
- "supply_chain": [
99
- "سلسلة الإمداد", "توريد", "موردين", "مناولة",
100
- "لوجستيات", "مخزون", "مخازن", "شراء", "بضائع",
101
- "سلسلة التوريد", "جدولة الإمداد", "الواردات"
102
- ]
103
- }
104
-
105
- def _load_common_requirements(self) -> List[Dict[str, Any]]:
106
- """
107
- تحميل قائمة المتطلبات الشائعة للمناقصات
108
- """
109
- # في التطبيق الفعلي، قد تُحمل هذه المتطلبات من ملف أو قاعدة بيانات
110
- return [
111
- {
112
- "title": "شهادة الزكاة والدخل",
113
- "category": "إدارية",
114
- "keywords": ["زكاة", "ضريبة", "شهادة زكاة", "مصلحة الزكاة", "هيئة الزكاة", "إقرار ضريبي"]
115
- },
116
- {
117
- "title": "السجل التجاري",
118
- "category": "إدارية",
119
- "keywords": ["سجل تجاري", "الغرفة التجارية", "رخصة تجارية", "وزارة التجارة"]
120
- },
121
- {
122
- "title": "شهادة الاشتراك في التأمينات الاجتماعية",
123
- "category": "إدارية",
124
- "keywords": ["تأمينات", "تأمينات اجتماعية", "مؤسسة التأمينات", "تأمين اجتماعي"]
125
- },
126
- {
127
- "title": "تصنيف المقاولين",
128
- "category": "فنية",
129
- "keywords": ["تصنيف", "شهادة تصنيف", "المقاولين", "وزارة الإسكان", "وزارة الشؤون البلدية"]
130
- },
131
- {
132
- "title": "نسبة المحتوى المحلي",
133
- "category": "محتوى محلي",
134
- "keywords": ["محتوى محلي", "نسبة سعودة", "توطين", "نطاقات", "رؤية 2030"]
135
- },
136
- {
137
- "title": "الخبرات السابقة",
138
- "category": "فنية",
139
- "keywords": ["خبرة", "خبرات سابقة", "مشاريع مماثلة", "أعمال سابقة", "سابقة أعمال"]
140
- }
141
- ]
142
-
143
  def process_document(self, file_content: bytes, file_extension: str, file_name: str) -> Dict[str, Any]:
144
  """
145
  معالجة المستند وتحليله حسب نوعه
146
-
147
- المعاملات:
148
- ----------
149
- file_content : bytes
150
- محتوى الملف بصيغة بايت
151
- file_extension : str
152
- امتداد الملف (pdf, docx, xlsx, csv, txt)
153
- file_name : str
154
- اسم الملف
155
-
156
- المخرجات:
157
- --------
158
- Dict[str, Any]
159
- قاموس يحتوي على البيانات المستخرجة من المستند
160
  """
161
- # تخزين المحتوى في ملف مؤقت
162
  with tempfile.NamedTemporaryFile(suffix=f".{file_extension}", delete=False) as temp_file:
163
  temp_file.write(file_content)
164
  temp_path = temp_file.name
165
 
166
  try:
167
- # معالجة الملف حسب نوعه
168
  if file_extension.lower() == 'pdf':
169
  extracted_data = self._process_pdf(temp_path)
170
  elif file_extension.lower() in ['docx', 'doc']:
@@ -178,834 +72,63 @@ class DocumentProcessor:
178
  else:
179
  extracted_data = {"error": f"نوع الملف {file_extension} غير مدعوم"}
180
 
181
- # إضافة معلومات أساسية عن الملف
182
  extracted_data["file_name"] = file_name
183
  extracted_data["file_type"] = file_extension
184
  extracted_data["processed_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
185
 
186
- # تحليل إضافي للمحتوى المستخرج
187
- if "text" in extracted_data:
188
- self._analyze_text_content(extracted_data)
189
-
190
  return extracted_data
191
 
192
  finally:
193
- # حذف الملف المؤقت بعد الانتهاء
194
  if os.path.exists(temp_path):
195
  os.remove(temp_path)
196
-
197
  def _process_pdf(self, file_path: str) -> Dict[str, Any]:
198
  """
199
  معالجة ملف PDF واستخراج النص والبيانات منه
200
  """
201
- extracted_data = {
202
- "text": "",
203
- "metadata": {},
204
- "images": [],
205
- "tables": [],
206
- "pages": []
207
- }
208
-
209
  try:
210
- # استخراج النص باستخدام PyMuPDF (fitz)
211
- doc = fitz.open(file_path)
212
-
213
- # استخراج البيانات الوصفية
214
- extracted_data["metadata"] = doc.metadata
215
 
216
- # معالجة كل صفحة
217
- for page_num, page in enumerate(doc):
218
- page_text = page.get_text()
219
- extracted_data["text"] += page_text
220
-
221
- # إضافة معلومات الصفحة
222
- page_data = {
223
- "page_num": page_num + 1,
224
- "text": page_text,
225
- "dimensions": {"width": page.rect.width, "height": page.rect.height}
226
- }
227
-
228
- # استخراج الصور
229
- image_list = page.get_images(full=True)
230
- page_images = []
231
- for img_index, img in enumerate(image_list):
232
- xref = img[0]
233
- base_image = doc.extract_image(xref)
234
- image_info = {
235
- "index": img_index,
236
- "width": base_image["width"],
237
- "height": base_image["height"],
238
- "format": base_image["ext"]
239
- }
240
- page_images.append(image_info)
241
-
242
- page_data["images"] = page_images
243
-
244
- # استخراج الجداول (تقريبي - قد يحتاج لتحسين)
245
- tables = []
246
- # بالنسبة للجداول، نستخدم تعبير منتظم للبحث عن نمط من المسافات وعلامات الجدولة
247
- # هذه طريقة بسيطة وقد تحتاج لتحسين باستخدام مكتبات متخصصة
248
- table_pattern = re.compile(r'(.+?[\t|]{2,}.+?[\n\r]){3,}', re.DOTALL)
249
- for match in table_pattern.finditer(page_text):
250
- tables.append(match.group(0))
251
-
252
- page_data["tables"] = tables
253
- extracted_data["pages"].append(page_data)
254
-
255
- # جمع كل الجداول المستخرجة
256
- extracted_data["tables"].extend(tables)
257
-
258
- # إذا لم نستطع استخراج نص باستخدام PyMuPDF، نجرب PyPDF2
259
- if not extracted_data["text"].strip():
260
- with open(file_path, 'rb') as pdf_file:
261
- pdf_reader = PyPDF2.PdfReader(pdf_file)
262
- for page_num in range(len(pdf_reader.pages)):
263
- page = pdf_reader.pages[page_num]
264
- extracted_data["text"] += page.extract_text()
265
-
266
- # إذا لم نستطع استخراج نص بعد، نجرب textract
267
  if not extracted_data["text"].strip():
268
- extracted_data["text"] = textract.process(file_path).decode('utf-8', errors='ignore')
269
-
270
- # تحليل OCR إذا كان النص قليلاً أو غير موجود
271
- if len(extracted_data["text"].strip()) < 100:
272
- self._apply_ocr_to_pdf(file_path, extracted_data)
273
-
274
  except Exception as e:
275
  extracted_data["error"] = f"خطأ في معالجة ملف PDF: {str(e)}"
276
-
277
  return extracted_data
278
 
279
- def _apply_ocr_to_pdf(self, file_path: str, extracted_data: Dict[str, Any]) -> None:
280
  """
281
  تطبيق OCR على ملف PDF لاستخراج النص من الصور
282
  """
283
  try:
284
  doc = fitz.open(file_path)
285
  ocr_text = ""
286
-
287
- for page_num, page in enumerate(doc):
288
- # استخراج الصفحة كصورة
289
  pix = page.get_pixmap()
290
  img_data = pix.tobytes("png")
291
-
292
- # فتح الصورة باستخدام PIL
293
  with io.BytesIO(img_data) as img_stream:
294
  img = Image.open(img_stream)
295
-
296
- # تطبيق OCR
297
- page_text = pytesseract.image_to_string(img, lang='ara+eng')
298
- ocr_text += page_text
299
-
300
- # إضافة النص المستخرج إلى بيانات الصفحة
301
- if page_num < len(extracted_data["pages"]):
302
- extracted_data["pages"][page_num]["ocr_text"] = page_text
303
-
304
- # إضافة النص المستخرج بواسطة OCR
305
- extracted_data["ocr_text"] = ocr_text
306
-
307
- # إذا كان النص الأصلي فارغاً، استخدم نص OCR كبديل
308
- if not extracted_data["text"].strip():
309
- extracted_data["text"] = ocr_text
310
-
311
  except Exception as e:
312
- extracted_data["ocr_error"] = f"خطأ في معالجة OCR: {str(e)}"
313
 
314
  def _process_docx(self, file_path: str) -> Dict[str, Any]:
315
  """
316
  معالجة ملف Word (DOCX) واستخراج النص والبيانات منه
317
  """
318
- extracted_data = {
319
- "text": "",
320
- "metadata": {},
321
- "images": [],
322
- "tables": [],
323
- "paragraphs": []
324
- }
325
-
326
  try:
327
- # استخراج النص من ملف DOCX
328
  doc = docx.Document(file_path)
329
-
330
- # استخراج النص الكامل
331
- for para in doc.paragraphs:
332
- if para.text.strip():
333
- extracted_data["text"] += para.text + "\n"
334
- extracted_data["paragraphs"].append({
335
- "text": para.text,
336
- "style": para.style.name if para.style else "Normal"
337
- })
338
-
339
- # استخراج الجداول
340
- tables_data = []
341
- for table_idx, table in enumerate(doc.tables):
342
- table_data = []
343
- for row_idx, row in enumerate(table.rows):
344
- row_data = []
345
- for cell_idx, cell in enumerate(row.cells):
346
- row_data.append(cell.text)
347
- table_data.append(row_data)
348
- tables_data.append({
349
- "table_idx": table_idx,
350
- "data": table_data
351
- })
352
- extracted_data["tables"] = tables_data
353
-
354
- # استخراج البيانات الوصفية
355
- doc_properties = doc.core_properties
356
- extracted_data["metadata"] = {
357
- "author": doc_properties.author,
358
- "created": str(doc_properties.created) if doc_properties.created else None,
359
- "modified": str(doc_properties.modified) if doc_properties.modified else None,
360
- "title": doc_properties.title,
361
- "subject": doc_properties.subject,
362
- "keywords": doc_properties.keywords
363
- }
364
-
365
- # تجربة استخدام mammoth للحصول على نص إضافي إذا لزم الأمر
366
  if not extracted_data["text"].strip():
367
  with open(file_path, "rb") as docx_file:
368
  result = mammoth.extract_raw_text(docx_file)
369
  extracted_data["text"] = result.value
370
-
371
  except Exception as e:
372
  extracted_data["error"] = f"خطأ في معالجة ملف DOCX: {str(e)}"
373
-
374
- # محاولة استخراج النص باستخدام textract كخطة بديلة
375
- try:
376
- extracted_data["text"] = textract.process(file_path).decode('utf-8', errors='ignore')
377
- except:
378
- pass
379
-
380
- return extracted_data
381
-
382
- def _process_excel(self, file_path: str) -> Dict[str, Any]:
383
- """
384
- معالجة ملف Excel واستخراج البيانات منه
385
- """
386
- extracted_data = {
387
- "sheets": [],
388
- "tables": [],
389
- "text": ""
390
- }
391
-
392
- try:
393
- # قراءة الملف باستخدام pandas
394
- xl = pd.ExcelFile(file_path)
395
- sheet_names = xl.sheet_names
396
-
397
- # استخراج البيانات من كل ورقة
398
- all_sheets_data = {}
399
- for sheet_name in sheet_names:
400
- df = pd.read_excel(xl, sheet_name)
401
- sheet_data = df.fillna('').to_dict(orient='records')
402
- all_sheets_data[sheet_name] = sheet_data
403
-
404
- # جمع النص لتحليل المحتوى
405
- for row in sheet_data:
406
- for column, value in row.items():
407
- if isinstance(value, str) and value.strip():
408
- extracted_data["text"] += value + " "
409
-
410
- # إضافة معلومات الورقة
411
- sheet_info = {
412
- "name": sheet_name,
413
- "rows": len(df),
414
- "columns": len(df.columns),
415
- "column_names": df.columns.tolist(),
416
- "data": sheet_data
417
- }
418
- extracted_data["sheets"].append(sheet_info)
419
-
420
- # إضافة كجدول
421
- extracted_data["tables"].append({
422
- "sheet_name": sheet_name,
423
- "data": sheet_data
424
- })
425
-
426
- # استخراج البيانات الوصفية باستخدام openpyxl
427
- workbook = load_workbook(file_path, read_only=True)
428
- extracted_data["metadata"] = {
429
- "title": workbook.properties.title,
430
- "author": workbook.properties.creator,
431
- "created": str(workbook.properties.created) if workbook.properties.created else None,
432
- "modified": str(workbook.properties.modified) if workbook.properties.modified else None,
433
- "sheet_names": workbook.sheetnames
434
- }
435
-
436
- except Exception as e:
437
- extracted_data["error"] = f"خطأ في معالجة ملف Excel: {str(e)}"
438
-
439
- return extracted_data
440
-
441
- def _process_csv(self, file_path: str) -> Dict[str, Any]:
442
- """
443
- معالجة ملف CSV واستخراج البيانات منه
444
- """
445
- extracted_data = {
446
- "headers": [],
447
- "data": [],
448
- "text": ""
449
- }
450
-
451
- try:
452
- # قراءة الملف بعدة ترميزات للتعامل مع الملفات العربية
453
- encodings = ['utf-8', 'cp1256', 'iso-8859-6', 'utf-16']
454
- df = None
455
-
456
- for encoding in encodings:
457
- try:
458
- df = pd.read_csv(file_path, encoding=encoding)
459
- break
460
- except:
461
- continue
462
-
463
- if df is None:
464
- # محاولة أخيرة باستخدام ترميز لاتيني وتجاهل الأخطاء
465
- df = pd.read_csv(file_path, encoding='latin1', errors='ignore')
466
-
467
- # استخراج البيانات
468
- extracted_data["headers"] = df.columns.tolist()
469
- extracted_data["data"] = df.fillna('').to_dict(orient='records')
470
-
471
- # جمع النص لتحليل المحتوى
472
- for row in extracted_data["data"]:
473
- for column, value in row.items():
474
- if isinstance(value, str) and value.strip():
475
- extracted_data["text"] += value + " "
476
-
477
- # إضافة معلومات إحصائية
478
- extracted_data["stats"] = {
479
- "rows": len(df),
480
- "columns": len(df.columns)
481
- }
482
-
483
- except Exception as e:
484
- extracted_data["error"] = f"خطأ في معالجة ملف CSV: {str(e)}"
485
-
486
- return extracted_data
487
-
488
- def _process_txt(self, file_path: str) -> Dict[str, Any]:
489
- """
490
- معالجة ملف نص عادي واستخراج البيانات منه
491
- """
492
- extracted_data = {
493
- "text": "",
494
- "lines": []
495
- }
496
-
497
- try:
498
- # قراءة الملف بعدة ترميزات للتعامل مع الملفات العربية
499
- encodings = ['utf-8', 'cp1256', 'iso-8859-6', 'utf-16']
500
- text_content = None
501
-
502
- for encoding in encodings:
503
- try:
504
- with open(file_path, 'r', encoding=encoding) as f:
505
- text_content = f.read()
506
- break
507
- except:
508
- continue
509
-
510
- if text_content is None:
511
- # محاولة أخيرة باستخدام ترميز لاتيني وتجاهل الأخطاء
512
- with open(file_path, 'r', encoding='latin1', errors='ignore') as f:
513
- text_content = f.read()
514
-
515
- # إضافة النص والأسطر
516
- extracted_data["text"] = text_content
517
- extracted_data["lines"] = text_content.splitlines()
518
-
519
- # إضافة معلومات إحصائية
520
- extracted_data["stats"] = {
521
- "lines": len(extracted_data["lines"]),
522
- "words": len(text_content.split()),
523
- "chars": len(text_content)
524
- }
525
-
526
- except Exception as e:
527
- extracted_data["error"] = f"خطأ في معالجة ملف النص: {str(e)}"
528
-
529
- return extracted_data
530
-
531
- def _analyze_text_content(self, extracted_data: Dict[str, Any]) -> None:
532
- """
533
- تحليل محتوى النص المستخرج لاستخراج معلومات إضافية
534
- مثل المتطلبات، وتفاصيل المناقصة، والمحتوى المحلي.
535
- """
536
- text = extracted_data["text"]
537
-
538
- # استخراج الكلمات الدلالية
539
- keywords = {}
540
- for category, terms in self.tender_keywords.items():
541
- category_keywords = []
542
- for term in terms:
543
- pattern = re.compile(r'\b' + re.escape(term) + r'\b', re.IGNORECASE | re.MULTILINE)
544
- matches = pattern.findall(text)
545
- if matches:
546
- category_keywords.extend(matches)
547
- keywords[category] = category_keywords
548
-
549
- extracted_data["keywords"] = keywords
550
-
551
- # استخراج المتطلبات المحتملة
552
- requirements = self._extract_requirements(text)
553
- extracted_data["requirements"] = requirements
554
-
555
- # استخراج البيانات المالية (أرقام، مبالغ، نسب مئوية)
556
- financial_data = self._extract_financial_data(text)
557
- extracted_data["financial_data"] = financial_data
558
-
559
- # استخراج التواريخ الهامة
560
- dates = self._extract_dates(text)
561
- extracted_data["dates"] = dates
562
-
563
- # استخراج معلومات المحتوى المحلي
564
- local_content = self._extract_local_content_info(text)
565
- extracted_data["local_content"] = local_content
566
-
567
- # استخراج معلومات سلسلة الإمداد
568
- supply_chain = self._extract_supply_chain_info(text)
569
- extracted_data["supply_chain"] = supply_chain
570
-
571
- # استخراج الجهات والأطراف المعنية
572
- entities = self._extract_entities(text)
573
- extracted_data["entities"] = entities
574
-
575
- def _extract_requirements(self, text: str) -> List[Dict[str, Any]]:
576
- """
577
- استخراج المتطلبات المحتملة من النص
578
- """
579
- requirements = []
580
-
581
- # البحث عن المتطلبات بناءً على كلمات دلالية
582
- for req_keyword in self.tender_keywords["requirements"]:
583
- # كلمات البداية للمتطلبات ونهايتها
584
- pattern = re.compile(
585
- r'(' + re.escape(req_keyword) + r'[^\n.]{0,100})([\n.].{0,500}?)(?:\n\n|\.\s|$)',
586
- re.DOTALL | re.MULTILINE
587
- )
588
- matches = pattern.finditer(text)
589
-
590
- for match in matches:
591
- title = match.group(1).strip()
592
- description = match.group(2).strip()
593
-
594
- # تحديد الأهمية بناءً على وجود كلمات إلزامية
595
- importance = "عادية"
596
- for imp_word in ["يجب", "إلزامي", "ضروري", "لا بد", "إجباري"]:
597
- if imp_word in title.lower() or imp_word in description.lower():
598
- importance = "عالية"
599
- break
600
-
601
- # تحديد الفئة
602
- category = "عامة"
603
- for cat, words in [
604
- ("فنية", ["فني", "تقني", "مواصفات", "معايير", "أداء", "جودة"]),
605
- ("إدارية", ["إداري", "قانوني", "تنظيمي", "إجرائي", "شروط"]),
606
- ("مالية", ["مالي", "سعر", "تكلفة", "دفع", "تسعير", "ميزانية"]),
607
- ("محتوى محلي", ["محلي", "محتوى محلي", "توطين", "سعودة"]),
608
- ("زمنية", ["زمني", "موعد", "تاريخ", "مدة", "جدول"])
609
- ]:
610
- for word in words:
611
- if word in title.lower() or word in description.lower():
612
- category = cat
613
- break
614
-
615
- # إضافة المتطلب
616
- requirement = {
617
- "title": title,
618
- "description": description,
619
- "importance": importance,
620
- "category": category
621
- }
622
- requirements.append(requirement)
623
-
624
- # البحث عن المتطلبات من قائمة المتطلبات الشائعة
625
- for common_req in self.common_requirements:
626
- for keyword in common_req["keywords"]:
627
- if keyword in text:
628
- # التحقق من أن المتطلب لم تتم إضافته بالفعل
629
- if not any(req["title"] == common_req["title"] for req in requirements):
630
- # العثور على الفقرة المتعلقة بهذا المتطلب
631
- pattern = re.compile(
632
- r'(.{0,100}' + re.escape(keyword) + r'.{0,200})',
633
- re.DOTALL | re.MULTILINE
634
- )
635
- match = pattern.search(text)
636
-
637
- description = match.group(1).strip() if match else "تم التعرف على المتطلب ولكن التفاصيل غير متاحة"
638
-
639
- requirement = {
640
- "title": common_req["title"],
641
- "description": description,
642
- "importance": "عالية",
643
- "category": common_req["category"],
644
- "is_common": True
645
- }
646
- requirements.append(requirement)
647
- break
648
-
649
- return requirements
650
-
651
- def _extract_financial_data(self, text: str) -> Dict[str, Any]:
652
- """
653
- استخراج البيانات المالية من النص
654
- """
655
- financial_data = {
656
- "amounts": [],
657
- "percentages": [],
658
- "total_cost": None
659
- }
660
-
661
- # استخراج المبالغ المالية
662
- money_pattern = self.regex_patterns["money"]
663
- money_matches = re.finditer(money_pattern, text)
664
-
665
- for match in money_matches:
666
- amount = match.group(1)
667
- currency = match.group(2)
668
-
669
- # تنظيف الرقم
670
- amount = amount.replace(',', '')
671
- try:
672
- amount_value = float(amount)
673
- financial_data["amounts"].append({
674
- "value": amount_value,
675
- "currency": currency,
676
- "original": match.group(0),
677
- "context": text[max(0, match.start() - 50):min(len(text), match.end() + 50)]
678
- })
679
- except:
680
- pass
681
-
682
- # استخراج النسب المئوية
683
- percentage_pattern = self.regex_patterns["percentage"]
684
- percentage_matches = re.finditer(percentage_pattern, text)
685
-
686
- for match in percentage_matches:
687
- percentage = match.group(1)
688
-
689
- # تنظيف الرقم
690
- percentage = percentage.replace(',', '')
691
- try:
692
- percentage_value = float(percentage)
693
- financial_data["percentages"].append({
694
- "value": percentage_value,
695
- "original": match.group(0),
696
- "context": text[max(0, match.start() - 50):min(len(text), match.end() + 50)]
697
- })
698
- except:
699
- pass
700
-
701
- # محاولة تحديد التكلفة الإجمالية
702
- total_cost_patterns = [
703
- r'القيمة الإجمالية[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)',
704
- r'إجمالي القيمة[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)',
705
- r'المبلغ الإجمالي[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)',
706
- r'قيمة العقد[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)',
707
- r'قيمة المشروع[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)'
708
- ]
709
-
710
- for pattern in total_cost_patterns:
711
- match = re.search(pattern, text, re.IGNORECASE)
712
- if match:
713
- amount = match.group(1).replace(',', '')
714
- try:
715
- amount_value = float(amount)
716
- financial_data["total_cost"] = {
717
- "value": amount_value,
718
- "currency": match.group(2),
719
- "original": match.group(0)
720
- }
721
- break
722
- except:
723
- pass
724
-
725
- return financial_data
726
-
727
- def _extract_dates(self, text: str) -> List[Dict[str, Any]]:
728
- """
729
- استخراج التواريخ الهامة من النص
730
- """
731
- dates = []
732
-
733
- # استخراج التواريخ باستخدام التعبير المنتظم
734
- date_pattern = self.regex_patterns["date"]
735
- date_matches = re.finditer(date_pattern, text)
736
-
737
- # قاموس لتحويل أسماء الشهور العربية إلى أرقام
738
- month_to_num = {
739
- "يناير": 1, "فبراير": 2, "مارس": 3, "أبريل": 4, "مايو": 5, "يونيو": 6,
740
- "يوليو": 7, "أغسطس": 8, "سبتمبر": 9, "أكتوبر": 10, "نوفمبر": 11, "ديسمبر": 12
741
- }
742
-
743
- for match in date_matches:
744
- try:
745
- # التحقق من نوع التاريخ المستخرج (رقمي أو مع اسم الشهر)
746
- if match.group(1): # تاريخ رقمي بالكامل
747
- day = int(match.group(1))
748
- month = int(match.group(2))
749
- year = int(match.group(3))
750
- if year < 100: # تحويل سنة مختصرة
751
- year += 2000 if year < 50 else 1900
752
- else: # تاريخ مع اسم الشهر
753
- day = int(match.group(4))
754
- month = month_to_num[match.group(5)]
755
- year = int(match.group(6))
756
- if year < 100: # تحويل سنة مختصرة
757
- year += 2000 if year < 50 else 1900
758
-
759
- # التحقق من صحة التاريخ
760
- if 1 <= day <= 31 and 1 <= month <= 12 and 1900 <= year <= 2100:
761
- date_str = f"{year}-{month:02d}-{day:02d}"
762
-
763
- # محاولة تحديد نوع التاريخ بناءً على السياق
764
- context = text[max(0, match.start() - 50):min(len(text), match.end() + 50)]
765
-
766
- date_type = "غير محدد"
767
- for date_keyword, date_type_value in [
768
- (["بداية", "بدء", "بدأ", "انطلاق"], "بداية"),
769
- (["نهاية", "انتهاء", "الانتهاء", "إغلاق"], "نهاية"),
770
- (["تسليم", "استلام", "توصيل"], "تسليم"),
771
- (["إصدار", "صدور", "إصدار", "نشر"], "إصدار"),
772
- (["اجتماع", "لقاء", "تمهيدي"], "اجتماع"),
773
- (["زيارة", "معاينة", "موقع"], "زيارة ميدانية")
774
- ]:
775
- for keyword in date_keyword:
776
- if keyword in context:
777
- date_type = date_type_value
778
- break
779
- if date_type != "غير محدد":
780
- break
781
-
782
- dates.append({
783
- "date": date_str,
784
- "original": match.group(0),
785
- "context": context,
786
- "type": date_type
787
- })
788
- except:
789
- pass
790
-
791
- return dates
792
-
793
- def _extract_local_content_info(self, text: str) -> Dict[str, Any]:
794
- """
795
- استخراج معلومات المحتوى المحلي من النص
796
- """
797
- local_content = {
798
- "mentions": [],
799
- "percentages": [],
800
- "requirements": []
801
- }
802
-
803
- # كلمات دلالية متعلقة بالمحتوى المحلي
804
- keywords = [
805
- "المحتوى المحلي", "محتوى محلي", "توطين", "سعودة", "نطاقات",
806
- "رؤية 2030", "رؤية المملكة", "النسبة المحلية", "الصناعة المحلية",
807
- "سلسلة الإمداد المحلية", "المنتجات المحلية", "الخدمات المحلية"
808
- ]
809
-
810
- # البحث عن ذكر المحتوى المحلي
811
- for keyword in keywords:
812
- pattern = re.compile(
813
- r'(.{0,100}' + re.escape(keyword) + r'.{0,200})',
814
- re.DOTALL | re.MULTILINE
815
- )
816
- matches = pattern.finditer(text)
817
-
818
- for match in matches:
819
- local_content["mentions"].append({
820
- "keyword": keyword,
821
- "context": match.group(1).strip()
822
- })
823
-
824
- # استخراج النسب المئوية المتعلقة بالمحتوى المحلي
825
- for mention in local_content["mentions"]:
826
- context = mention["context"]
827
-
828
- # البحث عن نسب مئوية في سياق المحتوى المحلي
829
- percentage_pattern = self.regex_patterns["percentage"]
830
- percentage_matches = re.finditer(percentage_pattern, context)
831
-
832
- for match in percentage_matches:
833
- percentage = match.group(1)
834
-
835
- # تنظيف الرقم
836
- percentage = percentage.replace(',', '')
837
- try:
838
- percentage_value = float(percentage)
839
- local_content["percentages"].append({
840
- "value": percentage_value,
841
- "keyword": mention["keyword"],
842
- "original": match.group(0),
843
- "context": context
844
- })
845
- except:
846
- pass
847
-
848
- # استخراج متطلبات المحتوى المحلي
849
- requirement_patterns = [
850
- r'يجب أن (يكون|تكون) نسبة المحتوى المحلي.{0,100}',
851
- r'يتعين على (المورد|المقاول|المتعهد|الشركة).{0,100}محتوى محلي.{0,100}',
852
- r'الحد الأدنى للمحتوى المحلي.{0,100}',
853
- r'يلتزم (المورد|المقاول|المتعهد|الشركة).{0,100}محتوى محلي.{0,100}'
854
- ]
855
-
856
- for pattern in requirement_patterns:
857
- matches = re.finditer(pattern, text, re.IGNORECASE | re.DOTALL)
858
-
859
- for match in matches:
860
- requirement = match.group(0).strip()
861
- local_content["requirements"].append(requirement)
862
-
863
- return local_content
864
-
865
- def _extract_supply_chain_info(self, text: str) -> Dict[str, Any]:
866
- """
867
- استخراج معلومات سلسلة الإمداد من النص
868
- """
869
- supply_chain = {
870
- "mentions": [],
871
- "suppliers": [],
872
- "materials": []
873
- }
874
-
875
- # كلمات دلالية متعلقة بسلسلة الإمداد
876
- keywords = [
877
- "سلسلة الإمداد", "سلسلة التوريد", "موردين", "مناولة", "لوجستيات",
878
- "مخزون", "توريد", "استيراد", "تخزين", "خدمات لوجستية", "مواد",
879
- "منتجات", "بضائع", "شحن", "نقل", "خدمات", "مصنع", "منتج محلي"
880
- ]
881
-
882
- # البحث عن ذكر سلسلة الإمداد
883
- for keyword in keywords:
884
- pattern = re.compile(
885
- r'(.{0,100}' + re.escape(keyword) + r'.{0,200})',
886
- re.DOTALL | re.MULTILINE
887
- )
888
- matches = pattern.finditer(text)
889
-
890
- for match in matches:
891
- supply_chain["mentions"].append({
892
- "keyword": keyword,
893
- "context": match.group(1).strip()
894
- })
895
-
896
- # استخراج أسماء الموردين المحتملين
897
- supplier_patterns = [
898
- r'(شركة|مؤسسة|مصنع)\s+([^\n.,]{3,50})',
899
- r'المورد\s+([^\n.,]{3,50})',
900
- r'التوريد من\s+([^\n.,]{3,50})',
901
- r'تصنيع بواسطة\s+([^\n.,]{3,50})'
902
- ]
903
-
904
- for pattern in supplier_patterns:
905
- matches = re.finditer(pattern, text, re.IGNORECASE)
906
-
907
- for match in matches:
908
- supplier = match.group(1) + " " + match.group(2) if "شركة|مؤسسة|مصنع" in pattern else match.group(1)
909
- supplier = supplier.strip()
910
-
911
- # تجنب الإضافات المزدوجة
912
- if supplier not in [s["name"] for s in supply_chain["suppliers"]]:
913
- supply_chain["suppliers"].append({
914
- "name": supplier,
915
- "context": text[max(0, match.start() - 30):min(len(text), match.end() + 30)]
916
- })
917
-
918
- # استخراج المواد الخام أو المنتجات
919
- materials_patterns = [
920
- r'مواد\s+([^\n.,]{3,50})',
921
- r'منتجات\s+([^\n.,]{3,50})',
922
- r'توريد\s+([^\n.,]{3,50})',
923
- r'استيراد\s+([^\n.,]{3,50})'
924
- ]
925
-
926
- for pattern in materials_patterns:
927
- matches = re.finditer(pattern, text, re.IGNORECASE)
928
-
929
- for match in matches:
930
- material = match.group(1).strip()
931
-
932
- # تجنب الإضافات المزدوجة
933
- if material not in [m["name"] for m in supply_chain["materials"]]:
934
- supply_chain["materials"].append({
935
- "name": material,
936
- "context": text[max(0, match.start() - 30):min(len(text), match.end() + 30)]
937
- })
938
-
939
- return supply_chain
940
-
941
- def _extract_entities(self, text: str) -> Dict[str, List[Dict[str, str]]]:
942
- """
943
- استخراج الجهات والأطراف المعنية من النص
944
- """
945
- entities = {
946
- "organizations": [],
947
- "persons": [],
948
- "locations": []
949
- }
950
-
951
- # استخراج المنظمات
952
- org_patterns = [
953
- r'(وزارة|هيئة|شركة|مؤسسة|جامعة|معهد|مركز|بلدية|أمانة)\s+([^\n.,]{3,50})',
954
- r'(جهة|جهات)\s+(حكومية|منفذة|مشرفة|متعاقدة|مالكة)'
955
- ]
956
-
957
- for pattern in org_patterns:
958
- matches = re.finditer(pattern, text, re.IGNORECASE)
959
-
960
- for match in matches:
961
- org_name = match.group(0).strip()
962
-
963
- # تجنب الإضافات المزدوجة
964
- if org_name not in [org["name"] for org in entities["organizations"]]:
965
- entities["organizations"].append({
966
- "name": org_name,
967
- "context": text[max(0, match.start() - 30):min(len(text), match.end() + 30)]
968
- })
969
-
970
- # استخراج الأشخاص (بسيط - يمكن تحسينه)
971
- person_patterns = [
972
- r'(المهندس|الدكتور|الأستاذ|السيد|الشيخ|المدير|الرئيس)\s+([^\n.,]{3,50})',
973
- r'(مدير|رئيس|مسؤول|منسق|مشرف)\s+(المشروع|العقد|الموقع|العملية)'
974
- ]
975
-
976
- for pattern in person_patterns:
977
- matches = re.finditer(pattern, text, re.IGNORECASE)
978
-
979
- for match in matches:
980
- person_name = match.group(0).strip()
981
-
982
- # تجنب الإضافات المزدوجة
983
- if person_name not in [p["name"] for p in entities["persons"]]:
984
- entities["persons"].append({
985
- "name": person_name,
986
- "context": text[max(0, match.start() - 30):min(len(text), match.end() + 30)]
987
- })
988
-
989
- # استخراج المواقع
990
- location_patterns = [
991
- r'مدينة\s+([^\n.,]{3,50})',
992
- r'محافظة\s+([^\n.,]{3,50})',
993
- r'منطقة\s+([^\n.,]{3,50})',
994
- r'حي\s+([^\n.,]{3,50})',
995
- r'موقع (المشروع|العمل|التنفيذ)'
996
- ]
997
-
998
- for pattern in location_patterns:
999
- matches = re.finditer(pattern, text, re.IGNORECASE)
1000
-
1001
- for match in matches:
1002
- location_name = match.group(0).strip()
1003
-
1004
- # تجنب الإضافات المزدوجة
1005
- if location_name not in [loc["name"] for loc in entities["locations"]]:
1006
- entities["locations"].append({
1007
- "name": location_name,
1008
- "context": text[max(0, match.start() - 30):min(len(text), match.end() + 30)]
1009
- })
1010
-
1011
- return entities
 
11
  import docx
12
  import PyPDF2
13
  import fitz # PyMuPDF
14
+ import pdfplumber
15
  import mammoth
16
  from openpyxl import load_workbook
17
  from PIL import Image
 
50
  # الكلمات التوقفية في اللغة العربية
51
  self.arabic_stopwords = set(stopwords.words('arabic'))
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def process_document(self, file_content: bytes, file_extension: str, file_name: str) -> Dict[str, Any]:
54
  """
55
  معالجة المستند وتحليله حسب نوعه
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  """
 
57
  with tempfile.NamedTemporaryFile(suffix=f".{file_extension}", delete=False) as temp_file:
58
  temp_file.write(file_content)
59
  temp_path = temp_file.name
60
 
61
  try:
 
62
  if file_extension.lower() == 'pdf':
63
  extracted_data = self._process_pdf(temp_path)
64
  elif file_extension.lower() in ['docx', 'doc']:
 
72
  else:
73
  extracted_data = {"error": f"نوع الملف {file_extension} غير مدعوم"}
74
 
 
75
  extracted_data["file_name"] = file_name
76
  extracted_data["file_type"] = file_extension
77
  extracted_data["processed_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
78
 
 
 
 
 
79
  return extracted_data
80
 
81
  finally:
 
82
  if os.path.exists(temp_path):
83
  os.remove(temp_path)
84
+
85
  def _process_pdf(self, file_path: str) -> Dict[str, Any]:
86
  """
87
  معالجة ملف PDF واستخراج النص والبيانات منه
88
  """
89
+ extracted_data = {"text": "", "metadata": {}, "images": [], "tables": [], "pages": []}
 
 
 
 
 
 
 
90
  try:
91
+ with pdfplumber.open(file_path) as pdf:
92
+ for page in pdf.pages:
93
+ extracted_text = page.extract_text()
94
+ if extracted_text:
95
+ extracted_data["text"] += extracted_text + "\n"
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  if not extracted_data["text"].strip():
98
+ extracted_data["text"] = self._apply_ocr_to_pdf(file_path)
 
 
 
 
 
99
  except Exception as e:
100
  extracted_data["error"] = f"خطأ في معالجة ملف PDF: {str(e)}"
 
101
  return extracted_data
102
 
103
+ def _apply_ocr_to_pdf(self, file_path: str) -> str:
104
  """
105
  تطبيق OCR على ملف PDF لاستخراج النص من الصور
106
  """
107
  try:
108
  doc = fitz.open(file_path)
109
  ocr_text = ""
110
+ for page in doc:
 
 
111
  pix = page.get_pixmap()
112
  img_data = pix.tobytes("png")
 
 
113
  with io.BytesIO(img_data) as img_stream:
114
  img = Image.open(img_stream)
115
+ ocr_text += pytesseract.image_to_string(img, lang='ara+eng') + "\n"
116
+ return ocr_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  except Exception as e:
118
+ return f"خطأ في OCR: {str(e)}"
119
 
120
  def _process_docx(self, file_path: str) -> Dict[str, Any]:
121
  """
122
  معالجة ملف Word (DOCX) واستخراج النص والبيانات منه
123
  """
124
+ extracted_data = {"text": "", "metadata": {}, "images": [], "tables": [], "paragraphs": []}
 
 
 
 
 
 
 
125
  try:
 
126
  doc = docx.Document(file_path)
127
+ extracted_data["text"] = "\n".join([para.text for para in doc.paragraphs if para.text.strip()])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  if not extracted_data["text"].strip():
129
  with open(file_path, "rb") as docx_file:
130
  result = mammoth.extract_raw_text(docx_file)
131
  extracted_data["text"] = result.value
 
132
  except Exception as e:
133
  extracted_data["error"] = f"خطأ في معالجة ملف DOCX: {str(e)}"
134
+ return extracted_data