| |
| """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]) |
| |
| 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 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() |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| matched_words = best_rows[best_match_idx: best_match_idx + n] |
|
|
| |
| 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] |
| 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, |
|
|
| } |