EGYADMIN commited on
Commit
62a43ef
·
verified ·
1 Parent(s): 34dc2df

Create modules/document_processor.py

Browse files
Files changed (1) hide show
  1. modules/document_processor.py +1011 -0
modules/document_processor.py ADDED
@@ -0,0 +1,1011 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import io
4
+ import tempfile
5
+ from typing import Dict, List, Any, Union, Tuple, Optional
6
+ import pandas as pd
7
+ import numpy as np
8
+ from datetime import datetime
9
+
10
+ # المكتبات الخاصة بمعالجة أنواع المستندات المختلفة
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
18
+ import pytesseract
19
+
20
+ # المكتبات الخاصة بالمعالجة الطبيعية للغة
21
+ import nltk
22
+ from nltk.tokenize import sent_tokenize, word_tokenize
23
+ from nltk.corpus import stopwords
24
+ from nltk import ngrams
25
+
26
+ # تحميل الموارد اللازمة للغة العربية
27
+ try:
28
+ nltk.data.find('tokenizers/punkt')
29
+ nltk.data.find('corpora/stopwords')
30
+ except LookupError:
31
+ nltk.download('punkt')
32
+ nltk.download('stopwords')
33
+
34
+ class DocumentProcessor:
35
+ """
36
+ فئة لمعالجة المستندات المختلفة وتحليلها واستخراج المعلومات منها
37
+ تدعم الملفات بصيغة PDF, DOCX, XLSX, CSV, TXT
38
+ """
39
+
40
+ def __init__(self):
41
+ """
42
+ تهيئة معالج المستندات
43
+ """
44
+ # تحميل قائمة الكلمات الدلالية للمناقصات
45
+ self.tender_keywords = self._load_tender_keywords()
46
+
47
+ # تحميل قائمة المتطلبات الشائعة
48
+ self.common_requirements = self._load_common_requirements()
49
+
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']:
171
+ extracted_data = self._process_docx(temp_path)
172
+ elif file_extension.lower() in ['xlsx', 'xls']:
173
+ extracted_data = self._process_excel(temp_path)
174
+ elif file_extension.lower() == 'csv':
175
+ extracted_data = self._process_csv(temp_path)
176
+ elif file_extension.lower() == 'txt':
177
+ extracted_data = self._process_txt(temp_path)
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