# -*- 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) -> dict: """ البحث عن آية قرآنية مع دعم الأخطاء الإملائية. المعاملات: query_text : النص المُدخَل من المستخدم (قد يحتوي أخطاء) target_type : لغة الإخراج (uthmani / english / french / ...) fuzzy_threshold : الحد الأدنى لقبول التطابق (0→1)، افتراضياً 0.72 المُخرج: dict يحتوي على: matched_segment : النص المُصحَّح بالرسم العثماني أو الترجمة full_verse : الآيات كاملة مع التوثيق similarity_score : درجة التشابه metadata : تفاصيل الآيات أو: error : رسالة الخطأ """ conn = sqlite3.connect('quran_master.db') 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) # ========================================== # البحث بالمرساة الديناميكية (من الأطول للأقصر) # ========================================== 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 # إذا لم تجد شيئاً بالمرساة الخفيفة → جرّب المرساة العميقة # (يستخدم 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 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%}). تحقق من النص المُدخل." # ) # } # ========================================== # تشكيل المخرجات # ========================================== matched_words = best_rows[best_match_idx: best_match_idx + n] # بناء matched_segment مع رقم كل آية aya_words: dict[tuple, list] = {} for w in matched_words: key = (w["sura_num"], w["aya_num"]) if key not in aya_words: aya_words[key] = [] aya_words[key].append(w["uthmani"] if lang_code == "uthmani" else w["target_text"]) if lang_code == "uthmani": seg_parts = [ " ".join(words) + f" ({a_num})" for (_, a_num), words in aya_words.items() ] matched_segment = " ".join(seg_parts) else: # للترجمات: نص الآية كامل + رقمها (بدون تكرار) seen_texts, seg_parts = set(), [] for (_, a_num), words in aya_words.items(): txt = words[0] # target_text مكرر لكل كلمة، نأخذ الأول if txt not in seen_texts: seen_texts.add(txt) seg_parts.append(f"{txt} ({a_num})") matched_segment = " ".join(seg_parts) # الآيات المشمولة — مرتبة بالترتيب 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"]} ayah_nums = [a_num for (_, a_num) in involved] sura_name = next(iter(involved.values()))["sura_name"] verse_body_parts = [] for (s_num, a_num), data in involved.items(): verse_body_parts.append(f"{data['target_text']} ({a_num})") combined_body = " ".join(verse_body_parts) if len(ayah_nums) == 1: ref = f"{sura_name}: {ayah_nums[0]}" else: nums_str = "،".join(str(x) for x in ayah_nums) ref = f"{sura_name}: {nums_str}" full_verse_formatted = f"({combined_body}) [{ref}]" matched_segment = f"({matched_segment}) [{ref}]" is_exact = best_score >= 0.999 return { "matched_segment": matched_segment, "full_verse": full_verse_formatted, }