Spaces:
Sleeping
Sleeping
| """ | |
| مستخرج الكيانات المسماة (Named Entity Extractor) | |
| =================================================== | |
| يستخرج الكيانات المسماة من النصوص العربية: أشخاص، مؤسسات، أماكن، تواريخ. | |
| يدعم الاستخراج بالأنماط والكلمات المفتاحية (بدون نموذج) أو بنموذج AraBERT NER. | |
| """ | |
| import logging | |
| import re | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| class EntityExtractor: | |
| """ | |
| مستخرج الكيانات المسماة — يستخرج الأشخاص والمؤسسات والأماكن والتواريخ من النصوص. | |
| أنواع الكيانات المدعومة: | |
| - PER: شخص (شخص، أسماء أشخاص) | |
| - ORG: مؤسسة (شركات، جامعات، وزارات) | |
| - LOC: موقع (مدن، دول، أماكن) | |
| - DATE: تاريخ (تواريخ، فترات زمنية) | |
| الخصائص: | |
| model_name (str, optional): اسم نموذج NER. | |
| device (str): الجهاز المستخدم. | |
| """ | |
| # ------------------------------------------------------------------ | |
| # أنماط الكيانات — الأشخاص (PER) | |
| # ------------------------------------------------------------------ | |
| _PERSON_PREFIXES: list[str] = [ | |
| "السيد", "السيدة", "الأستاذ", "الأستاذة", "الدكتور", | |
| "الشيخ", "السيد", "المهندس", "القاضي", "الوزير", | |
| "الأمير", "الملك", "الرئيس", "المدير", "البروفيسور", | |
| "أ.د", "د.", "م.", "أ.", | |
| ] | |
| _KNOWN_PERSONS: list[str] = [ | |
| "محمد", "أحمد", "علي", "حسن", "حسين", "إبراهيم", "يوسف", | |
| "عمر", "خالد", "عبدالله", "سعود", "فيصل", "ناصر", "سلطان", | |
| "فاطمة", "خديجة", "عائشة", "مريم", "سارة", "نورة", "هند", | |
| "محمد رسول الله", "أبو بكر", "عمر بن الخطاب", "عثمان بن عفان", | |
| "علي بن أبي طالب", | |
| ] | |
| # ------------------------------------------------------------------ | |
| # أنماط الكيانات — المؤسسات (ORG) | |
| # ------------------------------------------------------------------ | |
| _ORG_SUFFIXES: list[str] = [ | |
| "شركة", "مؤسسة", "جامعة", "وزارة", "بنك", "مستشفى", | |
| "مجلس", "هيئة", "جمعية", "نادي", "معهد", "مختبر", | |
| "منظمة", "اتحاد", "مكتبة", "متحف", "مسجد", | |
| ] | |
| _KNOWN_ORGS: list[str] = [ | |
| "الأمم المتحدة", "جامعة الدول العربية", "منظمة التعاون", | |
| "أوبك", "ناتو", "اليونسكو", "منظمة الصحة العالمية", | |
| "صندوق النقد الدولي", "البنك الدولي", | |
| ] | |
| # ------------------------------------------------------------------ | |
| # أنماط الكيانات — المواقع (LOC) | |
| # ------------------------------------------------------------------ | |
| _LOC_SUFFIXES: list[str] = [ | |
| "مدينة", "قرية", "حي", "شارع", "طريق", "ميناء", | |
| "مطار", "محافظة", "إقليم", "ولاية", "منطقة", | |
| ] | |
| _KNOWN_LOCATIONS: list[str] = [ | |
| "الرياض", "مكة", "المدينة", "جدة", "الدمام", "القاهرة", | |
| "دمشق", "بغداد", "بيروت", "عمان", "الدوحة", "الكويت", | |
| "المغرب", "تونس", "الجزائر", "السودان", "ليبيا", "اليمن", | |
| "فلسطين", "الأردن", "الإمارات", "عمان", "البحرين", | |
| "مصر", "السعودية", "تركيا", "إيران", "العراق", "سوريا", | |
| "أفغانستان", "باكستان", "الهند", "الصين", "اليابان", | |
| "أمريكا", "بريطانيا", "فرنسا", "ألمانيا", "إيطاليا", | |
| "إسبانيا", "روسيا", "كندا", "أستراليا", "البرازيل", | |
| "أبوظبي", "دبي", "Sharjah", "Ajman", | |
| ] | |
| # ------------------------------------------------------------------ | |
| # أنماط الكيانات — التواريخ (DATE) | |
| # ------------------------------------------------------------------ | |
| _DATE_PATTERNS: list[str] = [ | |
| # هجري: يوم شهر سنة هـ | |
| # ملاحظة: استخدام [\u0647\u0640]? بدلاً من هـ? لتجنب مشكلة | |
| # خاصية tatweel في regex مع أنماط Unicode الطويلة | |
| r"\d{1,2}\s+(يناير|فبراير|مارس|أبريل|مايو|يونيو|يوليو|أغسطس|سبتمبر|أكتوبر|نوفمبر|ديسمبر)\s+\d{4}\s*[\u0647\u0640]?", | |
| # ميلادي: يوم/شهر/سنة | |
| r"\d{1,2}/\d{1,2}/\d{2,4}", | |
| # سنة بالكلمات | |
| r"(?:عام|سنة)\s+\d{4}", | |
| # القرن | |
| r"القرن\s+(?:الأول|الثاني|الثالث|الرابع|الخامس|السادس|السابع|الثامن|التاسع|العاشر" | |
| r"|الحادي عشر|الثاني عشر|الثالث عشر|الرابع عشر|الخامس عشر|السادس عشر" | |
| r"|السابع عشر|الثامن عشر|التاسع عشر|العشرين|الواحد والعشرين|الثاني والعشرين)", | |
| # اليوم / الشهر / السنة | |
| r"(?:اليوم|الغد|أمس|بالأمس)", | |
| r"(?:هذا الشهر|الشهر الماضي|الشهر القادم|هذه السنة|السنة الماضية|السنة القادمة)", | |
| ] | |
| # كلمات عربية تُستخدم كحدود للأسماء (توقف التوسيع) | |
| _ARABIC_STOPWORDS: frozenset[str] = frozenset([ | |
| # حروف جر | |
| "في", "من", "إلى", "على", "عن", "مع", "ب", "ل", "ك", | |
| # أسماء إشارة وموصولة | |
| "هذا", "هذه", "ذلك", "تلك", "الذي", "التي", "الذين", | |
| "اللاتي", "اللواتي", "اللذين", "اللتين", | |
| # أفعال شائعة | |
| "كان", "كانت", "يكون", "يوم", "أمس", "غدا", | |
| "قال", "قالت", "ذهب", "جاء", "زار", "سافر", | |
| "عمل", "يعمل", "درس", "يلتقي", "التقى", "يتم", | |
| # حروف عطف وربط | |
| "ثم", "أو", "و", "ف", "حتى", "بعد", "قبل", | |
| "بين", "عند", "منذ", "خلال", "عبر", "ضد", | |
| # أدوات نفي واستفهام | |
| "لا", "لم", "لن", "ما", "أن", "إن", "هل", "أم", | |
| "بل", "لكن", "غير", "قد", "سوف", "لقد", | |
| # أسماء مكان (تُستخدم كفواصل بين الكيانات) | |
| "مدينة", "قرية", "حي", "شارع", "طريق", "منطقة", | |
| # كلمات دينية | |
| "بسم", "الله", "الرحمن", "الرحيم", "الحمد", "لله", | |
| "سبحان", "والصلاة", "والسلام", "رسول", | |
| # أخرى | |
| "حول", "دون", "ذات", "ذو", "ذي", | |
| "حيث", "كيف", "متى", "أين", "لماذا", "أي", | |
| "هو", "هي", "هم", "نحن", "أنا", "كل", "بعض", | |
| ]) | |
| def __init__( | |
| self, | |
| model_name: Optional[str] = None, | |
| device: str = "cpu", | |
| ) -> None: | |
| """ | |
| تهيئة مستخرج الكيانات المسماة. | |
| المعاملات: | |
| model_name: اسم نموذج NER (اختياري). | |
| مثال: "UBC-NLP/ARBERT" | |
| device: الجهاز المستخدم ('cpu' أو 'cuda'). | |
| """ | |
| self.model_name = model_name | |
| self.device = device | |
| self._pipeline = None | |
| self._model_available = False | |
| self._tokenizer = None | |
| # تجميع أنماط التواريخ | |
| self._compiled_date_patterns: list[re.Pattern] = [] | |
| for pat in self._DATE_PATTERNS: | |
| try: | |
| self._compiled_date_patterns.append(re.compile(pat, re.IGNORECASE)) | |
| except re.error: | |
| logger.debug("نمط تاريخ غير صالح: %s", pat) | |
| # محاولة تحميل النموذج | |
| if model_name: | |
| self._try_load_model() | |
| def _try_load_model(self) -> None: | |
| """محاولة تحميل نموذج NER من HuggingFace.""" | |
| try: | |
| from transformers import pipeline # type: ignore | |
| logger.info("جاري تحميل نموذج NER: %s ...", self.model_name) | |
| self._pipeline = pipeline( | |
| "ner", | |
| model=self.model_name, | |
| device=self.device, | |
| aggregation_strategy="simple", | |
| ) | |
| self._model_available = True | |
| logger.info("تم تحميل نموذج NER بنجاح") | |
| except ImportError: | |
| logger.warning( | |
| "مكتبة transformers غير مثبتة. سيتم الاعتماد على الأنماط فقط. " | |
| "pip install transformers torch" | |
| ) | |
| except Exception as e: | |
| logger.warning("فشل تحميل نموذج NER '%s': %s", self.model_name, e) | |
| def _trim_entity(entity_text: str, stopwords: frozenset[str]) -> str: | |
| """ | |
| قص الكيان من النهاية عند كلمات التوقف. | |
| المعاملات: | |
| entity_text: نص الكيان الخام. | |
| stopwords: مجموعة كلمات التوقف. | |
| العائد: | |
| النص المقصوص. | |
| """ | |
| words = entity_text.strip().split() | |
| while len(words) > 1 and words[-1] in stopwords: | |
| words.pop() | |
| return " ".join(words) | |
| # ------------------------------------------------------------------ | |
| # استخراج بالأنماط (يعمل دائماً) | |
| # ------------------------------------------------------------------ | |
| def _extract_dates(self, text: str) -> list[dict]: | |
| """ | |
| استخراج التواريخ من النص. | |
| المعاملات: | |
| text: النص المراد استخراج التواريخ منه. | |
| العائد: | |
| قائمة بقواميس الكيانات. | |
| """ | |
| entities: list[dict] = [] | |
| seen_spans: set[tuple[int, int]] = set() | |
| for pattern in self._compiled_date_patterns: | |
| for match in pattern.finditer(text): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": match.group().strip(), | |
| "type": "DATE", | |
| "start": match.start(), | |
| "end": match.end(), | |
| }) | |
| return entities | |
| def _extract_locations(self, text: str) -> list[dict]: | |
| """ | |
| استخراج المواقع من النص. | |
| المعاملات: | |
| text: النص المراد استخراج المواقع منه. | |
| العائد: | |
| قائمة بقواميس الكيانات. | |
| """ | |
| entities: list[dict] = [] | |
| seen_spans: set[tuple[int, int]] = set() | |
| # البحث عن مواقع معروفة | |
| for loc in self._KNOWN_LOCATIONS: | |
| for match in re.finditer(re.escape(loc), text, re.IGNORECASE): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": match.group(), | |
| "type": "LOC", | |
| "start": match.start(), | |
| "end": match.end(), | |
| }) | |
| # البحث عن كلمات موقع متبوعة باسم | |
| for suffix in self._LOC_SUFFIXES: | |
| # نمط: كلمة موقع متبوعة باسم عربي (حد أقصى كلمتين) | |
| pattern = re.compile( | |
| rf"(?:{re.escape(suffix)})\s+[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+)?" | |
| ) | |
| for match in pattern.finditer(text): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| trimmed = self._trim_entity(match.group(), self._ARABIC_STOPWORDS) | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": trimmed, | |
| "type": "LOC", | |
| "start": match.start(), | |
| "end": match.start() + len(trimmed), | |
| }) | |
| return entities | |
| def _extract_organizations(self, text: str) -> list[dict]: | |
| """ | |
| استخراج المؤسسات من النص. | |
| المعاملات: | |
| text: النص المراد استخراج المؤسسات منه. | |
| العائد: | |
| قائمة بقواميس الكيانات. | |
| """ | |
| entities: list[dict] = [] | |
| seen_spans: set[tuple[int, int]] = set() | |
| # البحث عن مؤسسات معروفة | |
| for org in self._KNOWN_ORGS: | |
| for match in re.finditer(re.escape(org), text, re.IGNORECASE): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": match.group(), | |
| "type": "ORG", | |
| "start": match.start(), | |
| "end": match.end(), | |
| }) | |
| # البحث عن كلمات مؤسسة متبوعة باسم | |
| for suffix in self._ORG_SUFFIXES: | |
| # نمط: كلمة مؤسسة متبوعة باسم عربي (حد أقصى 3 كلمات) | |
| pattern = re.compile( | |
| rf"(?:(?:ال|أل|لل)?{re.escape(suffix)})\s+[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){{0,2}}" | |
| ) | |
| for match in pattern.finditer(text): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| trimmed = self._trim_entity(match.group(), self._ARABIC_STOPWORDS) | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": trimmed, | |
| "type": "ORG", | |
| "start": match.start(), | |
| "end": match.start() + len(trimmed), | |
| }) | |
| return entities | |
| def _extract_persons(self, text: str) -> list[dict]: | |
| """ | |
| استخراج أسماء الأشخاص من النص. | |
| المعاملات: | |
| text: النص المراد استخراج الأشخاص منه. | |
| العائد: | |
| قائمة بقواميس الكيانات. | |
| """ | |
| entities: list[dict] = [] | |
| seen_spans: set[tuple[int, int]] = set() | |
| # البحث عن أسماء معرفة بـ ألقاب | |
| for prefix in self._PERSON_PREFIXES: | |
| # نمط: لقب متبوع باسم عربي (حد أقصى كلمتين) | |
| pattern = re.compile( | |
| rf"{re.escape(prefix)}\s+[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+)?" | |
| ) | |
| for match in pattern.finditer(text): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| trimmed = self._trim_entity(match.group(), self._ARABIC_STOPWORDS) | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": trimmed, | |
| "type": "PER", | |
| "start": match.start(), | |
| "end": match.start() + len(trimmed), | |
| }) | |
| # البحث عن أسماء أشخاص معروفة | |
| for person in self._KNOWN_PERSONS: | |
| for match in re.finditer(re.escape(person), text): | |
| span = (match.start(), match.end()) | |
| if span not in seen_spans: | |
| seen_spans.add(span) | |
| entities.append({ | |
| "entity": match.group(), | |
| "type": "PER", | |
| "start": match.start(), | |
| "end": match.end(), | |
| }) | |
| return entities | |
| def _pattern_extract(self, text: str) -> list[dict]: | |
| """ | |
| استخراج جميع الكيانات باستخدام الأنماط. | |
| المعاملات: | |
| text: النص المراد استخراج الكيانات منه. | |
| العائد: | |
| قائمة مرتبة بالكيانات المستخرجة. | |
| """ | |
| all_entities: list[dict] = [] | |
| # استخراج كل نوع | |
| all_entities.extend(self._extract_persons(text)) | |
| all_entities.extend(self._extract_organizations(text)) | |
| all_entities.extend(self._extract_locations(text)) | |
| all_entities.extend(self._extract_dates(text)) | |
| # ترتيب حسب موضع الظهور | |
| all_entities.sort(key=lambda e: e["start"]) | |
| # إزالة التداخلات | |
| cleaned: list[dict] = [] | |
| last_end = -1 | |
| for entity in all_entities: | |
| if entity["start"] >= last_end: | |
| cleaned.append(entity) | |
| last_end = entity["end"] | |
| return cleaned | |
| # ------------------------------------------------------------------ | |
| # استخراج بالنموذج (إذا توفر) | |
| # ------------------------------------------------------------------ | |
| def _model_extract(self, text: str) -> list[dict]: | |
| """ | |
| استخراج الكيانات باستخدام نموذج NER. | |
| المعاملات: | |
| text: النص المراد استخراج الكيانات منه. | |
| العائد: | |
| قائمة بقواميس الكيانات. | |
| """ | |
| if not self._pipeline: | |
| return self._pattern_extract(text) | |
| try: | |
| results = self._pipeline(text) | |
| entities: list[dict] = [] | |
| for item in results: | |
| entity_type = item.get("entity_group", item.get("entity", "MISC")) | |
| # تحويل أنواع الكيانات | |
| type_map = { | |
| "B-PER": "PER", "I-PER": "PER", | |
| "B-ORG": "ORG", "I-ORG": "ORG", | |
| "B-LOC": "LOC", "I-LOC": "LOC", | |
| "B-DATE": "DATE", "I-DATE": "DATE", | |
| "PER": "PER", "ORG": "ORG", "LOC": "LOC", "DATE": "DATE", | |
| } | |
| mapped_type = type_map.get(entity_type, entity_type) | |
| entities.append({ | |
| "entity": item.get("word", "").strip(), | |
| "type": mapped_type, | |
| "start": item.get("start", 0), | |
| "end": item.get("end", 0), | |
| "score": round(item.get("score", 0.0), 4), | |
| }) | |
| return entities | |
| except Exception as e: | |
| logger.warning("فشل الاستخراج بالنموذج: %s — يتم الرجوع للأنماط", e) | |
| return self._pattern_extract(text) | |
| # ------------------------------------------------------------------ | |
| # الواجهة العامة | |
| # ------------------------------------------------------------------ | |
| def extract(self, text: str) -> list[dict]: | |
| """ | |
| استخراج الكيانات المسماة من النص. | |
| المعاملات: | |
| text: النص المراد استخراج الكيانات منه. | |
| العائد: | |
| قائمة بقواميس الكيانات: {entity, type, start, end} | |
| """ | |
| if not text or not text.strip(): | |
| return [] | |
| cleaned = text.strip() | |
| if self._model_available and self._pipeline is not None: | |
| return self._model_extract(cleaned) | |
| return self._pattern_extract(cleaned) | |
| def extract_from_document(self, text: str) -> dict: | |
| """ | |
| استخراج الكيانات من مستند كامل. | |
| المعاملات: | |
| text: نص المستند الكامل. | |
| العائد: | |
| قاموس يحتوي على: | |
| - entities: قائمة جميع الكيانات | |
| - by_type: كيانات مصنفة حسب النوع | |
| - unique_entities: الكيانات الفريدة | |
| - total_count: العدد الإجمالي | |
| """ | |
| if not text or not text.strip(): | |
| return { | |
| "entities": [], | |
| "by_type": {}, | |
| "unique_entities": [], | |
| "total_count": 0, | |
| } | |
| entities = self.extract(text) | |
| # تصنيف حسب النوع | |
| by_type: dict[str, list[dict]] = {} | |
| for entity in entities: | |
| etype = entity["type"] | |
| if etype not in by_type: | |
| by_type[etype] = [] | |
| by_type[etype].append(entity) | |
| # الكيانات الفريدة | |
| unique_names: list[str] = [] | |
| seen_names: set[str] = set() | |
| for entity in entities: | |
| name = entity["entity"] | |
| if name not in seen_names: | |
| seen_names.add(name) | |
| unique_names.append(name) | |
| return { | |
| "entities": entities, | |
| "by_type": by_type, | |
| "unique_entities": unique_names, | |
| "total_count": len(entities), | |
| } | |
| def extract_by_type(self, text: str, entity_type: str) -> list[dict]: | |
| """ | |
| استخراج كيانات من نوع محدد. | |
| المعاملات: | |
| text: النص. | |
| entity_type: نوع الكيان (PER/ORG/LOC/DATE). | |
| العائد: | |
| قائمة بالكيانات من النوع المطلوب. | |
| """ | |
| all_entities = self.extract(text) | |
| return [e for e in all_entities if e["type"] == entity_type.upper()] | |
| def get_supported_types(self) -> list[str]: | |
| """ | |
| عرض أنواع الكيانات المدعومة. | |
| العائد: | |
| قائمة بأنواع الكيانات. | |
| """ | |
| return ["PER", "ORG", "LOC", "DATE"] | |