# -*- coding: utf-8 -*- """Quran.py Automatically generated by Colab. Original file is located at https://colab.research.google.com/drive/1WwaR-xsFnY5iffCndJV5metzB0RS4_GP """ import sqlite3 import re from rapidfuzz.distance import Levenshtein # ========================================== # طبقة التطبيع الموسعة # ========================================== # مجموعات الحروف المتشابهة بصرياً أو صوتياً — كلها ترجع لشكل واحد _SIMILAR_GROUPS = [ ('اأإآٱ', 'ا'), ('ةه', 'ه'), ('يىئ', 'ي'), ('وؤ', 'و'), ('ذد', 'د'), # متقاربة بصرياً للمبتدئين ('زرذ', 'ر'), # أحياناً يخلط المستخدم بينها ('طت', 'ت'), ('ضظ', 'ض'), ('سص', 'س'), ('ثت', 'ت'), ('خح', 'ح'), ('غع', 'ع'), ] def _build_similarity_table(): table = {} for group, canonical in _SIMILAR_GROUPS: for ch in group: table[ch] = canonical return table _SIM_TABLE = _build_similarity_table() def normalize_arabic(text: str, *, deep: bool = False) -> str: """ تطبيع النص العربي. deep=False → تطبيع خفيف (للتخزين في DB أو المقارنة الدقيقة) deep=True → تطبيع عميق يساوي بين الحروف المتشابهة (للبحث الضبابي) """ if not text: return "" # إزالة التشكيل والعلامات القرآنية text = re.sub( r'[\u064B-\u065F\u0670\u0671\u0656' r'\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED]', '', text ) # همزات text = re.sub(r'[أإآٱ]', 'ا', text) # تاء مربوطة text = re.sub(r'ة', 'ه', text) # ألف مقصورة text = re.sub(r'ى', 'ي', text) if deep: text = ''.join(_SIM_TABLE.get(ch, ch) for ch in text) return ' '.join(text.strip().split()) # ========================================== # مطابقة ضبابية على مستوى الكلمات # ========================================== def _word_similarity(a: str, b: str) -> float: """نسبة التشابه بين كلمتين (0.0 → 1.0).""" max_len = max(len(a), len(b), 1) dist = Levenshtein.distance(a, b) return 1.0 - dist / max_len def _score_window(query_words: list[str], window_words: list[str], query_deep: list[str], window_deep: list[str]) -> float: """ احسب نسبة تطابق نافذة كلمات مع استعلام المستخدم. نستخدم نسختين: خفيفة (أولوية) وعميقة (احتياط). """ if len(window_words) != len(query_words): return 0.0 total = 0.0 for qw, ww, qd, wd in zip(query_words, window_words, query_deep, window_deep): # نأخذ أعلى نتيجة بين المقارنة الخفيفة والعميقة s_light = _word_similarity(qw, ww) s_deep = _word_similarity(qd, wd) total += max(s_light, s_deep) return total / len(query_words) # ========================================== # الدالة الرئيسية # ========================================== def search_bayan(query_text: str, target_type: str = "تدقيق الايات", fuzzy_threshold: float = 0.72, db_path: str = None) -> dict: """ البحث عن آية قرآنية مع دعم الأخطاء الإملائية. المعاملات: query_text : النص المُدخَل من المستخدم (قد يحتوي أخطاء) target_type : لغة الإخراج (uthmani / english / french / ...) fuzzy_threshold : الحد الأدنى لقبول التطابق (0→1)، افتراضياً 0.72 db_path : مسار ملف قاعدة البيانات (افتراضياً بجوار هذا الملف) المُخرج: dict يحتوي على: matched_segment : النص المُصحَّح بالرسم العثماني أو الترجمة full_verse : الآيات كاملة مع التوثيق similarity_score : درجة التشابه metadata : تفاصيل الآيات أو: error : رسالة الخطأ """ import os if db_path is None: db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'quran_master.db') conn = sqlite3.connect(db_path) cursor = conn.cursor() language_mapping = { "تدقيق الايات": "uthmani", "bengali": "bn", "bosnian": "bs", "english": "en", "french": "fr", "german": "de", "indonesian": "id", "malay": "ms", "persian": "fa", "portuguese": "pt", "russian": "ru", "spanish": "es", "turkish": "tr", "uzbek": "uz" } clean_target = str(target_type).lower().strip() lang_code = language_mapping.get(clean_target, "uthmani") verse_column = "v.text_uthmani" if lang_code == "uthmani" else f"v.lang_{lang_code}" sura_column = "s.ar" if lang_code == "uthmani" else f"s.lang_{lang_code}" # ── تطبيع الاستعلام بنسختين ── query_light = normalize_arabic(query_text, deep=False).split() query_deep = normalize_arabic(query_text, deep=True).split() if not query_light: conn.close() return {"error": "النص المُدخل فارغ"} n = len(query_light) # ── إذا بدأ الاستعلام بالبسملة → أعطِ الفاتحة أولوية ── # بدون ده، "بسم الله الرحمن الرحيم الحمد لله" بيطابق ١١٢ سورة _query_starts_basmala = False if n >= 4: first4_q = normalize_arabic(' '.join(query_light[:4]), deep=False) if first4_q in ['بسم الله الرحمن الرحيم', 'بسم لله لرحمن لرحيم', 'بسم لله لرحمـن لرحيم']: _query_starts_basmala = True # ========================================== # البحث بالمرساة الديناميكية (من الأطول للأقصر) # ========================================== candidate_starts: list[tuple[int, int]] = [] for i in range(n, 0, -1): anchor = ' '.join(query_light[:i]) # بحث LIKE عادي أولاً (سريع) cursor.execute(""" SELECT v.sura_num, v.aya_num FROM verses v WHERE v.text_clean LIKE ? ORDER BY v.sura_num, v.aya_num """, ('%' + anchor + '%',)) candidate_starts = cursor.fetchall() if candidate_starts: break # إذا بدأ بالبسملة → الفاتحة أولوية if _query_starts_basmala: fatiha = (1, 1) if fatiha not in candidate_starts: candidate_starts.insert(0, fatiha) else: candidate_starts.remove(fatiha) candidate_starts.insert(0, fatiha) # إذا لم تجد شيئاً بالمرساة الخفيفة → جرّب المرساة العميقة # (يستخدم text_deep إذا كان موجوداً، وإلا يعود لـ text_clean) if not candidate_starts: # اكتشف أعمدة الجدول cursor.execute("PRAGMA table_info(verses)") cols = {row[1] for row in cursor.fetchall()} deep_col = "v.text_deep" if "text_deep" in cols else "v.text_clean" for i in range(n, 0, -1): anchor_deep = ' '.join(query_deep[:i]) cursor.execute(f""" SELECT v.sura_num, v.aya_num FROM verses v WHERE {deep_col} LIKE ? ORDER BY v.sura_num, v.aya_num """, ('%' + anchor_deep + '%',)) rows = cursor.fetchall() if rows: candidate_starts = rows break # الملاذ الأخير: ابحث بكل كلمة على حدة وخذ الآيات الأكثر تكراراً if not candidate_starts: counts: dict[tuple, int] = {} for word in query_light: if len(word) < 3: continue cursor.execute(""" SELECT v.sura_num, v.aya_num FROM verses v WHERE v.text_clean LIKE ? """, ('%' + word + '%',)) for row in cursor.fetchall(): counts[row] = counts.get(row, 0) + 1 if counts: candidate_starts = sorted(counts, key=counts.get, reverse=True)[:15] if not candidate_starts: conn.close() return { "matched_segment": "", "full_verse": "لم يُعثر على تطابق — تحقق من النص المُدخل" } # ========================================== # النافذة المنزلقة + التقييم الضبابي # ========================================== best_score = -1.0 best_match_idx = -1 best_rows = None for start_sura, start_aya in candidate_starts: cursor.execute(f""" SELECT v.sura_num, v.aya_num, v.text_clean, v.text_uthmani, {verse_column}, {sura_column} FROM verses v JOIN suras_translated s ON v.sura_num = s.sura_number WHERE (v.sura_num = ? AND v.aya_num >= ?) OR (v.sura_num > ?) ORDER BY v.sura_num, v.aya_num LIMIT 12 """, (start_sura, start_aya, start_sura)) fetched = cursor.fetchall() QURAN_MARKS = { 'ۖ', 'ۗ', 'ۘ', 'ۙ', 'ۚ', 'ۛ', 'ۜ', '۝', '۞', '۩' } # بناء خريطة الكلمات combined_light, combined_deep, word_map = [], [], [] for row in fetched: s_num, a_num, t_clean, t_uthmani, t_target, s_name = row # ── شيل البسملة من text_clean في البحث (مش الفاتحة) ── # بدون ده، anchor "بسم الله الرحمن الرحيم" بيطابق ١١٢ سورة if a_num == 1 and s_num != 1: tc_words = t_clean.split() if len(tc_words) >= 4: first4 = normalize_arabic(' '.join(tc_words[:4]), deep=False) basmala_norms = [ 'بسم الله الرحمن الرحيم', 'بسم لله لرحمن لرحيم', 'بسم لله لرحمـن لرحيم', ] if first4 in basmala_norms: t_clean = ' '.join(tc_words[4:]) clean_w = t_clean.split() uthmani_w = [ token for token in t_uthmani.split() if token not in QURAN_MARKS ] deep_w = normalize_arabic(t_clean, deep=True).split() for j, cw in enumerate(clean_w): combined_light.append(cw) combined_deep.append(deep_w[j] if j < len(deep_w) else cw) word_map.append({ "clean": cw, "uthmani": uthmani_w[j] if j < len(uthmani_w) else cw, "sura_num": s_num, "aya_num": a_num, "target_text": t_target, "sura_name": s_name, }) total_words = len(combined_light) if total_words < n: continue # نافذة منزلقة — ابحث عن أعلى نتيجة for j in range(total_words - n + 1): score = _score_window( query_light, combined_light[j:j+n], query_deep, combined_deep[j:j+n] ) if score > best_score: best_score = score best_match_idx = j best_rows = word_map # إذا وجدنا تطابقاً كاملاً، لا داعي للاستمرار if best_score >= 0.999: break conn.close() # if best_score < fuzzy_threshold or best_match_idx == -1: # return { # "error": ( # f"أقرب تطابق وجدناه بدرجة {best_score:.0%} وهي أقل من الحد المقبول " # f"({fuzzy_threshold:.0%}). تحقق من النص المُدخل." # ) # } # ========================================== # تشكيل المخرجات # ========================================== # تحويل الأرقام اللاتينية إلى عربية def to_arabic_nums(n): """Convert 31 → ٣١""" ar_digits = '٠١٢٣٤٥٦٧٨٩' return ''.join(ar_digits[int(d)] for d in str(n)) matched_words = best_rows[best_match_idx: best_match_idx + n] # دالة تنسيق الرقم حسب اللغة def fmt_num(n): """Arabic-Indic for uthmani, Latin for translations""" return to_arabic_nums(n) if lang_code == "uthmani" else str(n) # الآيات المشمولة — مرتبة بالترتيب (من الـ matched window) involved: dict[tuple, dict] = {} for w in matched_words: key = (w["sura_num"], w["aya_num"]) if key not in involved: involved[key] = { "sura_name": w["sura_name"], "target_text": w["target_text"], "uthmani": w.get("uthmani", ""), } # منع تجاوز حدود السورة: اختر السورة الأكثر تمثيلاً فقط sura_counts: dict[int, int] = {} for w in matched_words: sura_counts[w["sura_num"]] = sura_counts.get(w["sura_num"], 0) + 1 primary_sura = max(sura_counts, key=sura_counts.get) involved = {k: v for k, v in involved.items() if k[0] == primary_sura} ayah_nums = [a_num for (_, a_num) in involved] sura_name = next(iter(involved.values()))["sura_name"] # ── بناء النص الكامل من الآيات الكاملة (مش من الـ window بس) ── # إزالة البسملة من أول آية (مدمجة في الـ DB) — ماعدا الفاتحة def _strip_basmala(text): """Remove Basmala from beginning of verse text""" words = text.split() if len(words) < 4: return text # Normalize first 4 words and check against known Basmala forms first4_norm = normalize_arabic(' '.join(words[:4]), deep=False) # ٱ (Alef Wasla) gets stripped during normalization → 'بسم لله لرحمـن لرحيم' basmala_forms = [ 'بسم الله الرحمن الرحيم', # standard alef 'بسم لله لرحمن لرحيم', # alef wasla stripped 'بسم لله لرحمـن لرحيم', # with tatweel ـ ] if first4_norm in basmala_forms: return ' '.join(words[4:]) return text verse_parts = [] for (s_num, a_num), data in involved.items(): txt = data['target_text'] # شيل البسملة من الآية الأولى (إلا سورة الفاتحة) if a_num == 1 and s_num != 1: txt = _strip_basmala(txt) verse_parts.append(f"{txt} ({fmt_num(a_num)})") combined_body = " ".join(verse_parts) # بناء المرجع: نطاق (من-إلى) بدل سرد كل الأرقام if len(ayah_nums) == 1: ref = f"{sura_name}: {fmt_num(ayah_nums[0])}" else: first = fmt_num(ayah_nums[0]) last = fmt_num(ayah_nums[-1]) ref = f"{sura_name}: {first}-{last}" result_text = f"({combined_body}) 【{ref}】" is_exact = best_score >= 0.999 return { "matched_segment": result_text, "full_verse": result_text, }