v3 / modules /services /specs_analyzer.py
EGYADMIN's picture
Upload 115 files
82676b8 verified
"""
خدمة تحليل المواصفات من المستندات
"""
import re
import pandas as pd
import numpy as np
import nltk
from nltk.tokenize import sent_tokenize
import config
class SpecificationsAnalyzer:
"""تحليل المواصفات الفنية في المستندات"""
def __init__(self):
# تحميل موارد NLTK إذا لم تكن موجودة
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
nltk.download('punkt')
# فئات المواصفات الرئيسية
self.specification_categories = {
'الخرسانة': [
'خرسانة', 'اسمنت', 'رتبة', 'مقاومة', 'ضغط', 'شك', 'معالجة',
'صب', 'قالب', 'قوالب', 'تسليح', 'خلطة', 'ركام', 'حصى'
],
'حديد التسليح': [
'حديد', 'تسليح', 'قضبان', 'شد', 'جهد خضوع', 'درجة', 'قطر',
'ربط', 'غطاء خرساني', 'تشكيل', 'ثني', 'شبكة'
],
'العزل المائي': [
'عزل', 'مائي', 'رطوبة', 'بيتومين', 'لفائف', 'رولات', 'طبقة',
'رش', 'تسرب', 'مانع تسرب', 'مقاومة الماء', 'حرارة'
],
'العزل الحراري': [
'عزل', 'حراري', 'صوف صخري', 'صوف زجاجي', 'فوم', 'بوليسترين',
'موصلية', 'انتقال الحرارة', 'بولي يوريثان'
],
'أعمال البلاط': [
'بلاط', 'سيراميك', 'بورسلين', 'رخام', 'جرانيت', 'ترويبة',
'لاصق', 'مونة', 'تركيب', 'مسافات', 'أبعاد'
],
'أعمال الدهان': [
'دهان', 'طلاء', 'وجه تأسيس', 'وجه نهائي', 'رش', 'فرشاة',
'رولة', 'معجون', 'مائي', 'زيتي', 'لامع', 'مطفي'
],
'المواد الكهربائية': [
'كهرباء', 'أسلاك', 'كابلات', 'لوحات', 'مفاتيح', 'تمديدات',
'جهد', 'قدرة', 'توزيع', 'تأريض', 'قواطع', 'تيار'
],
'أعمال السباكة': [
'سباكة', 'مواسير', 'صرف', 'تغذية', 'مياه', 'بي في سي',
'نحاس', 'حديد', 'خزان', 'مضخة', 'صمام', 'محبس'
],
'أعمال التكييف': [
'تكييف', 'تبريد', 'تدفئة', 'مجاري هواء', 'دكت', 'مناولة',
'تهوية', 'وحدة', 'مكيف', 'فلتر', 'مروحة'
]
}
# المواصفات القياسية المعروفة
self.standard_specs = {
'ASTM': {
'C150': 'اسمنت بورتلاندي',
'A615': 'حديد تسليح',
'D6164': 'عزل مائي بيتوميني',
'C33': 'ركام الخرسانة',
'C494': 'إضافات الخرسانة',
'C979': 'صبغات الخرسانة',
'C578': 'عزل البوليسترين'
},
'AASHTO': {
'M85': 'اسمنت بورتلاندي',
'M31': 'حديد تسليح',
'M320': 'بيتومين للطرق'
},
'IEC': {
'60502': 'كابلات الطاقة',
'60364': 'تمديدات كهربائية',
'61439': 'لوحات توزيع الطاقة'
},
'BS': {
'8500': 'الخرسانة',
'4449': 'حديد التسليح',
'6700': 'أنظمة المياه',
'5950': 'المنشآت الفولاذية'
},
'EN': {
'197-1': 'الاسمنت',
'10080': 'حديد التسليح',
'13162': 'العزل الحراري'
},
'كود البناء السعودي': {
'SBC 201': 'الأحمال',
'SBC 304': 'الخرسانة الإنشائية',
'SBC 305': 'المباني المعدنية',
'SBC 501': 'السباكة',
'SBC 401': 'الكهرباء',
'SBC 601': 'البناء الصديق للبيئة'
}
}
def analyze_specifications(self, text):
"""تحليل المواصفات الفنية من النص"""
if not text:
return {}, [], pd.DataFrame()
# تقسيم النص إلى جمل
sentences = sent_tokenize(text)
# استخراج المواصفات حسب الفئة
specs = {}
for category, keywords in self.specification_categories.items():
specs[category] = self._extract_category_specs(sentences, keywords, category)
# استخراج المتطلبات الخاصة
special_requirements = self._extract_special_requirements(sentences)
# استخراج متطلبات المحتوى المحلي
local_content = self._extract_local_content(sentences)
return specs, special_requirements, local_content
def _extract_category_specs(self, sentences, keywords, category):
"""استخراج مواصفات فئة محددة من الجمل"""
category_specs = {}
# البحث عن الجمل التي تحتوي على الكلمات المفتاحية للفئة
category_sentences = [s for s in sentences if any(k in s.lower() for k in keywords)]
if not category_sentences:
return category_specs
# استخراج المواصفات حسب نوع الفئة
if category == 'الخرسانة':
# البحث عن قوة الضغط
for s in category_sentences:
if any(term in s.lower() for term in ['قوة', 'مقاومة', 'ضغط']):
match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s)
if match:
category_specs['قوة الضغط'] = f"{match.group(1)} نيوتن/مم²"
# البحث عن نسبة الماء للأسمنت
if any(term in s.lower() for term in ['نسبة', 'ماء', 'اسمنت']):
match = re.search(r'(\d+(?:\.\d+)?)\s*(?:%|نسبة)', s)
if match:
category_specs['نسبة الماء للأسمنت'] = f"{match.group(1)} كحد أقصى"
# البحث عن المعالجة
if 'معالجة' in s.lower():
match = re.search(r'(\d+)\s*(?:يوم|أيام)', s)
if match:
category_specs['المعالجة'] = f"لا تقل عن {match.group(1)} أيام"
# البحث عن المواصفات المرجعية
for std_org, std_codes in self.standard_specs.items():
for std_code, std_desc in std_codes.items():
if std_code in s and (std_org in s or category in std_desc.lower()):
category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
elif category == 'حديد التسليح':
# البحث عن نوع الحديد
for s in category_sentences:
if any(term in s.lower() for term in ['درجة', 'جهد', 'خضوع', 'grade']):
match = re.search(r'(?:درجة|جريد|Grade)\s*(\d+)', s, re.IGNORECASE)
if match:
category_specs['نوع الحديد'] = f"عالي المقاومة للشد (Grade {match.group(1)})"
# البحث عن إجهاد الخضوع
if any(term in s.lower() for term in ['إجهاد', 'خضوع', 'شد']):
match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s)
if match:
category_specs['إجهاد الخضوع'] = f"{match.group(1)} نيوتن/مم²"
# البحث عن المواصفات المرجعية
for std_org, std_codes in self.standard_specs.items():
for std_code, std_desc in std_codes.items():
if std_code in s and (std_org in s or category in std_desc.lower()):
category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
elif category == 'العزل المائي':
# البحث عن نوع العزل
for s in category_sentences:
if any(term in s.lower() for term in ['نوع', 'بيتومين', 'بوليستر', 'رول']):
if 'بيتومين' in s.lower() and 'بوليستر' in s.lower():
category_specs['النوع'] = 'أغشية بيتومينية مدعمة بالبوليستر'
elif 'بيتومين' in s.lower():
category_specs['النوع'] = 'أغشية بيتومينية'
elif 'pvc' in s.lower():
category_specs['النوع'] = 'أغشية PVC'
# البحث عن السماكة
if any(term in s.lower() for term in ['سماكة', 'سمك', 'مم']):
match = re.search(r'(\d+(?:\.\d+)?)\s*(?:مم|mm)', s, re.IGNORECASE)
if match:
category_specs['السماكة'] = f"{match.group(1)} مم"
# البحث عن مقاومة درجة الحرارة
if any(term in s.lower() for term in ['حرارة', 'درجة', 'مقاومة']):
match = re.search(r'(\d+)\s*(?:درجة|°)', s)
if match:
category_specs['مقاومة درجة الحرارة'] = f"حتى {match.group(1)} درجة مئوية"
# البحث عن المواصفات المرجعية
for std_org, std_codes in self.standard_specs.items():
for std_code, std_desc in std_codes.items():
if std_code in s and (std_org in s or 'عزل' in std_desc.lower()):
category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
elif category == 'المواد الكهربائية':
# البحث عن نوع الكابلات
for s in category_sentences:
if any(term in s.lower() for term in ['كابل', 'سلك', 'نحاس', 'ألمنيوم']):
if 'نحاس' in s.lower() and 'xlpe' in s.lower():
category_specs['الكابلات'] = 'نحاس معزول XLPE'
elif 'نحاس' in s.lower() and 'pvc' in s.lower():
category_specs['الكابلات'] = 'نحاس معزول PVC'
elif 'نحاس' in s.lower():
category_specs['الكابلات'] = 'نحاس معزول'
elif 'ألمنيوم' in s.lower():
category_specs['الكابلات'] = 'ألمنيوم معزول'
# البحث عن المواصفات المرجعية
for std_org, std_codes in self.standard_specs.items():
for std_code, std_desc in std_codes.items():
if std_code in s and (std_org in s or 'كهربا' in std_desc.lower()):
category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
# إذا لم يتم العثور على مواصفات محددة، أضف مواصفات افتراضية للفئات الرئيسية
if not category_specs and category in ['الخرسانة', 'حديد التسليح', 'العزل المائي', 'المواد الكهربائية']:
if category == 'الخرسانة':
category_specs = {
'قوة الضغط': '30 نيوتن/مم²',
'نسبة الماء للأسمنت': '0.45 كحد أقصى',
'المعالجة': 'لا تقل عن 7 أيام',
'المواصفات المرجعية': 'ASTM C150'
}
elif category == 'حديد التسليح':
category_specs = {
'نوع الحديد': 'عالي المقاومة للشد (Grade 60)',
'إجهاد الخضوع': '420 نيوتن/مم²',
'المواصفات المرجعية': 'ASTM A615'
}
elif category == 'العزل المائي':
category_specs = {
'النوع': 'أغشية بيتومينية مدعمة بالبوليستر',
'السماكة': '4 مم',
'مقاومة درجة الحرارة': 'حتى 100 درجة مئوية',
'المواصفات المرجعية': 'ASTM D6164'
}
elif category == 'المواد الكهربائية':
category_specs = {
'الكابلات': 'نحاس معزول XLPE',
'المواصفات المرجعية': 'IEC 60502'
}
return category_specs
def _extract_special_requirements(self, sentences):
"""استخراج المتطلبات الخاصة من الجمل"""
special_requirements = []
# الكلمات المفتاحية التي تشير إلى متطلبات خاصة
special_keywords = [
'يجب', 'ضرورة', 'يلزم', 'اشتراط', 'متطلب', 'إلزامي',
'اعتماد', 'موافقة', 'تقديم', 'تأكيد', 'ضمان', 'توافق'
]
# استخراج الجمل التي تحتوي على الكلمات المفتاحية
for s in sentences:
if any(keyword in s.lower() for keyword in special_keywords):
# تنظيف الجملة
req = s.strip()
# التأكد من أن الجملة تبدأ بيجب أو إذا لم تكن كذلك أضف "يجب" في البداية
if not any(req.startswith(start) for start in ['يجب', 'ضرورة', 'يلزم']):
req = f"يجب {req}"
# التأكد من أن الجملة تنتهي بنقطة
if not req.endswith('.'):
req = f"{req}."
# إضافة المتطلب إلى القائمة إذا لم يكن موجوداً بالفعل
if req not in special_requirements:
special_requirements.append(req)
# إضافة متطلبات افتراضية إذا لم يتم العثور على متطلبات
if not special_requirements:
special_requirements = [
"يجب أن تكون جميع المواد معتمدة من المهندس المشرف قبل التوريد.",
"يجب تقديم عينات لجميع المواد المستخدمة للاعتماد.",
"يجب تقديم شهادات ضمان لمدة سنة لجميع الأعمال المنفذة.",
"يجب الالتزام بكود البناء السعودي في جميع الأعمال.",
"يجب توفير اختبارات ضبط الجودة لأعمال الخرسانة.",
"يجب الالتزام بنسبة المحتوى المحلي لا تقل عن 70%."
]
return special_requirements
def _extract_local_content(self, sentences):
"""استخراج متطلبات المحتوى المحلي من الجمل"""
local_content_df = pd.DataFrame()
# الكلمات المفتاحية للمحتوى المحلي
lc_keywords = ['محتوى محلي', 'منتج وطني', 'صناعة محلية', 'توطين']
# استخراج الجمل التي تحتوي على كلمات مفتاحية للمحتوى المحلي
lc_sentences = [s for s in sentences if any(k in s.lower() for k in lc_keywords)]
# إذا وجدت جمل متعلقة بالمحتوى المحلي
if lc_sentences:
lc_data = []
# البحث عن نسب محددة في الجمل
for s in lc_sentences:
# البحث عن نسب مئوية
percentages = re.findall(r'(\d+)(?:\.\d+)?%', s)
if percentages:
# محاولة استخراج الفئة من الجملة
if 'عمال' in s.lower() or 'قوى' in s.lower() or 'موظف' in s.lower():
lc_data.append({
'الفئة': 'القوى العاملة',
'النسبة المطلوبة': f"{percentages[0]}%",
'الملاحظات': 'تشمل العمالة والمهندسين والإداريين'
})
elif 'منتج' in s.lower() or 'صناع' in s.lower() or 'مواد' in s.lower() or 'معدات' in s.lower():
lc_data.append({
'الفئة': 'المنتجات',
'النسبة المطلوبة': f"{percentages[0]}%",
'الملاحظات': 'تشمل المواد والمعدات المصنعة محلياً'
})
elif 'خدم' in s.lower() or 'نقل' in s.lower() or 'تأمين' in s.lower():
lc_data.append({
'الفئة': 'الخدمات',
'النسبة المطلوبة': f"{percentages[0]}%",
'الملاحظات': 'تشمل خدمات النقل والتأمين والاستشارات'
})
else:
# إذا لم يتم تحديد الفئة، اعتبرها إجمالي
lc_data.append({
'الفئة': 'إجمالي المشروع',
'النسبة المطلوبة': f"{percentages[0]}%",
'الملاحظات': 'نسبة المحتوى المحلي الإجمالية للمشروع'
})
# تحويل البيانات إلى DataFrame
if lc_data:
local_content_df = pd.DataFrame(lc_data)
# إذا لم يتم العثور على متطلبات محتوى محلي، استخدم بيانات افتراضية
if local_content_df.empty:
local_content_df = pd.DataFrame({
'الفئة': ['القوى العاملة', 'المنتجات', 'الخدمات'],
'النسبة المطلوبة': ['80%', '70%', '60%'],
'الملاحظات': [
'تشمل العمالة والمهندسين والإداريين',
'تشمل المواد والمعدات المصنعة محلياً',
'تشمل خدمات النقل والتأمين والاستشارات'
]
})
return local_content_df