Spaces:
Sleeping
Sleeping
""" | |
خدمة تحليل المواصفات من المستندات | |
""" | |
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 |