|
"""
|
|
خدمة تحليل المواصفات من المستندات
|
|
"""
|
|
|
|
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):
|
|
|
|
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]}%",
|
|
'الملاحظات': 'نسبة المحتوى المحلي الإجمالية للمشروع'
|
|
})
|
|
|
|
|
|
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 |