Quran / quran.py
Abdo0431's picture
Upload 2 files
20bd4e4 verified
Raw
History Blame Contribute Delete
12.9 kB
# -*- 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,
}