SNS / app.py
luvici1111100's picture
Update app.py
b523967 verified
import gradio as gr
import pandas as pd
import re
import os
import tempfile
from typing import List, Dict, Any
import hashlib
import zipfile
# --- START: NEW TRANSLATION DICTIONARY ---
# This dictionary maps the unified location names to the desired English translation.
ARABIC_TO_ENGLISH_TRANSLATION = {
'New Cairo': 'New Cairo', 'القاهرة الجديدة': 'New Cairo', 'القاهره الجديده': 'New Cairo',
'Tagamoa': 'Tagamoa, 5th Settlement', 'التجمع الخامس': 'Tagamoa, 5th Settlement', 'التجمع': 'Tagamoa, 5th Settlement', 'التجمع الأول': 'Tagamoa, 5th Settlement',
'Sheikh Zayed': 'Sheikh Zayed', 'الشيخ زايد': 'Sheikh Zayed',
'6th of October': '6th of October', '6 اكتوبر': '6th of October',
'Ain Sokhna': 'Ain Sokhna', 'العين السخنة': 'Ain Sokhna', 'Sokhna': 'Ain Sokhna',
'North Coast': 'North Coast', 'الساحل الشمالي': 'North Coast',
'New Capital': 'New Capital', 'العاصمة الإدارية': 'New Capital',
'El Shorouk': 'El Shorouk City', 'مدينة الشروق': 'El Shorouk City', 'الشروق': 'El Shorouk City',
'El Obour': 'El Obour', 'العبور': 'El Obour',
'El Rehab': 'El Rehab', 'الرحاب': 'El Rehab',
'Madinaty': 'Madinaty', 'مدينتي': 'Madinaty', 'مدنتي': 'Madinaty',
'Maadi': 'Maadi', 'المعادي': 'Maadi',
'Nile Corniche': 'Nile Corniche', 'كورنيش النيل': 'Nile Corniche',
'Mostakbal City': 'Mostakbal City', 'المستقبل سيتي': 'Mostakbal City',
'South Academy': 'South Academy', 'جنوب الأكاديمية': 'South Academy', 'بجنوب الاكاديميه': 'South Academy',
'South Lotus': 'South Lotus', 'اللوتس الجنوبي': 'South Lotus',
'North Lotus': 'North Lotus', 'اللوتس الشمالية': 'North Lotus', 'اللوتس الشماليه': 'North Lotus',
'Narges': 'Narges', 'النرجس': 'Narges',
'Koronfol': 'Koronfol', 'القرنفل': 'Koronfol',
'Yasmine': 'Yasmine', 'الياسمين': 'Yasmine',
'Banafseg': 'Banafseg', 'البنفسج': 'Banafseg',
'Andalus': 'Andalus', 'بالاندلس': 'Andalus',
'West Golf': 'West Golf', 'بغرب الجولف': 'West Golf',
'Shouifat': 'Shouifat', 'الشويفات': 'Shouifat', 'بالشويفات': 'Shouifat',
'Katameya Heights': 'Katameya, Katameya Heights', 'القطاميه هايتس': 'Katameya, Katameya Heights', 'قطاميه هايتس': 'Katameya, Katameya Heights',
'Rawdet Zayed': 'Rawdet Zayed', 'روضة زايد': 'Rawdet Zayed',
'5th District': '5th District', 'الحي الخامس': '5th District',
'Sodic': 'Sodic', 'SODIC': 'Sodic', 'سوديك': 'Sodic',
'Marassi': 'Marassi', 'مراسي': 'Marassi',
'Mivida': 'Mivida', 'ميفيدا': 'Mivida', 'مافيدا': 'Mivida',
'Hyde Park': 'Hyde Park', 'هايد بارك': 'Hyde Park', 'هيدبارك': 'Hyde Park', 'هيد بارك': 'Hyde Park',
'Mountain View': 'Mountain View', 'ماونتن فيو': 'Mountain View',
'Palm Hills': 'Palm Hills', 'بالم هيلز': 'Palm Hills',
'Taj City': 'Taj City', 'تاج سيتى': 'Taj City',
'Al Borouj': 'Al Borouj', 'البروج': 'Al Borouj',
'Sarai': 'Sarai', 'سراي': 'Sarai',
'Diar': 'Diar', 'ديار': 'Diar',
'Stella': 'Stella', 'ستيلا': 'Stella',
'Telal': 'Telal El Sokhna', 'تلال السخنه': 'Telal El Sokhna', 'بتلال السخنه': 'Telal El Sokhna',
'Fouka Bay': 'Fouka Bay', 'فوكا باي': 'Fouka Bay', 'فوكا باى': 'Fouka Bay',
'Hacienda': 'Hacienda White', 'هاسيندا وايت': 'Hacienda White',
'Lake View': 'Lake View', 'ليك فيو': 'Lake View',
'Golf Porto Marina': 'Golf Porto Marina', 'بورتو جولف الساحل الشمالي': 'Golf Porto Marina',
'Swan Lake': 'Swan Lake North Coast', 'سوان ليك الساحل': 'Swan Lake North Coast',
'La Vista': 'La Vista', 'لافيستا': 'La Vista',
'Jayd': 'Jayd', 'جايد': 'Jayd',
'Zahra': 'Zahra', 'زهرة': 'Zahra',
'Gaia': 'Gaia', 'جايا': 'Gaia',
'Amwaj': 'Amwaj', 'امواج': 'Amwaj',
'Celia': 'Celia', 'سيليا': 'Celia',
'Il Bosco New Capital': 'Il Bosco New Capital', 'البوسكو العاصمه': 'Il Bosco New Capital',
'Noor City': 'Noor City', 'مدينة نور': 'Noor City',
'June Sodic': 'June Sodic', 'جون سوديك': 'June Sodic',
'Caesar Sodic': 'Caesar Sodic', 'سيزر سوديك': 'Caesar Sodic',
'CFC': 'Cairo Festival City (CFC)', 'كايرو فيستيفال سيتي': 'Cairo Festival City (CFC)',
'Eastown': 'Eastown, Sodic Eastown', 'ايستاون': 'Eastown, Sodic Eastown', 'سوديك ايستاون': 'Eastown, Sodic Eastown',
'Waterway': 'Waterway', 'واتر واي': 'Waterway',
'El Marasem': 'The Square, El Marasem', 'المراسم': 'The Square, El Marasem', 'مراسم': 'The Square, El Marasem', 'the square': 'The Square, El Marasem',
'Katameya Dunes': 'Katameya Dunes', 'قطامية دوينز': 'Katameya Dunes',
'El Nakhil': 'El Nakhil', 'النخيل': 'El Nakhil',
'Sidi Abdel Rahman': 'Sidi Abdel Rahman', 'سيدي عبد الرحمن': 'Sidi Abdel Rahman',
'Sidi Heneish': 'Sidi Heneish', 'سيدي حنيش': 'Sidi Heneish',
'Almaza Bay': 'Almaza Bay', 'الماظة باي': 'Almaza Bay',
'Marsa Baghoush': 'Marsa Baghoush', 'مرسي باغوش': 'Marsa Baghoush',
'Azha Sokhna': 'Azha Sokhna', 'ازها السخنة': 'Azha Sokhna',
'Garden Hills': 'Garden Hills', 'جاردن هيلز': 'Garden Hills',
'Talaat Moustafa': 'Talaat Moustafa', 'طلعت مصطفي': 'Talaat Moustafa',
'Golden Project': 'Golden Project', 'مشروع جولدن': 'Golden Project', 'جولدن': 'Golden Project',
'Dorra': 'Dorra', 'درة': 'Dorra',
'Marina': 'Marina', 'مارينا': 'Marina',
'Crest': 'Crest', 'كريست': 'Crest',
'Gardenia': 'Gardenia', 'جاردينيا': 'Gardenia',
'Montaza Village': 'Montaza Village', 'قرية المنتزة': 'Montaza Village',
'Pacua View': 'Pacua View', 'باكوا فيو': 'Pacua View',
'El Bustan': 'El Bustan', 'البستان': 'El Bustan',
'Family City': 'Family City', 'فاميلي سيتي': 'Family City',
'Galleria': 'Galleria', 'جلاريا': 'Galleria',
'Wesal': 'Wesal', 'وصال': 'Wesal',
}
# --- END: NEW TRANSLATION DICTIONARY ---
# --- GLOBAL MAPPINGS ---
# Defined globally to be accessible by all functions.
PROPERTY_TYPE_MAPPING = {
'Apartment': r'شقة|apartment|flat|apt|جاردن|garden',
'Villa': r'فيلا|villa',
'Townhouse': r'تاون هاوس|townhouse',
'Twinhouse': r'توين هاوس|twinhouse',
'Studio': r'ستوديو|studio',
'Shop': r'محل|shop|store',
'Office': r'مكتب|office|off',
'Land': r'أرض|land|plot',
'Chalet': r'شاليه|chalet|sh',
'Penthouse': r'بنتهاوس|penthouse',
'Duplex': r'دوبلكس|duplex',
'Triplex': r'تريبلكس|triplex',
'Roof': r'روف|roof',
'Building': r'مبنى|building'
}
LOCATION_UNIFICATION_MAP = {
'Madinaty': ['مدينتي', 'Madenty', 'Madinty', 'madinaty', 'maidnty', 'madinty', 'Madinaty', 'مدنتي', 'طلعت مصطفي', 'مجموعة', 'بمدينتي'] + [f'B{i}' for i in range(1, 112)],
'Jayd': ['Jayd', 'جايد'], 'Zahra': ['زهرة', 'ZAHRA', 'zahra', 'Zahra'], 'Al Borouj': ['Al boruje', 'البروج'],
'Mountain View': ['Mountain View i city', 'Mountain View', 'mvhp', 'LVLS Mountain view ras el hekma', 'Rhodes Mountain View Ras El Hekma', 'ماونتن فيو راس الحكمة', 'ماونتن فيو 1.1', 'ماونتن فيو السخنة', 'Mountain View executive', 'mountain view hyde park'],
'Amwaj': ['امواج', 'AMWAJ', 'Amwaj', 'amwaj'], 'Gaia': ['Gaia', 'Gaia Sabbour', 'جايا'], 'Sarai': ['S A R A I', 'Sarai', 'SARAI', 'sarai', 'سراي'],
'Diar': ['ديار', 'Diar', 'DIAR'], 'Stella': ['ستيلا', 'ستيلا العين السخنه', 'Stella Marina', 'Stella Sidi ABDEL Rahman', 'Stella El Sokhna', 'Stella'],
'New Cairo': ['القاهرة الجديدة', 'القاهره الجديده', 'التجمع الأول', 'التجمع', 'التجمع الخامس', 'New Cairo', 'القاهره الجديدة', 'بجنوب الاكاديميه', 'جنوب الأكاديمية', 'اللوتس الجنوبي', 'اللوتس الشماليه', 'النرجس', 'القرنفل', 'اللوتس الشمالية', 'بالاندلس', 'بغرب الجولف', 'الياسمين', 'البنفسج', '90 أفينيو'],
'Telal': ['تلال السخنه', 'بتلال السخنه', 'telal', 'Telal', 'TELAL'], 'Taj City': ['Taj City', 'تاج سيتى'],
'El Marasem': ['Fifth square Marasem', 'El marasem', 'El marasem 5th square', 'Fifth square', 'the square', 'مراسم', 'المراسم', 'ذا سكوير'],
'Fouka Bay': ['Fouka Bay', 'فوكا باى', 'فوكا باي'], 'El Nakhil': ['النخيل', 'El Nakhil', 'Nakhil', 'nakhil', 'Elnakhil'],
'Katameya Dunes': ['Katameya Dunes', 'قطامية دوينز'],
'Marassi': ['Safi Marassi', 'Marassi', 'Marina residence', 'Marina marassi', 'مراسي', 'Marina', 'Marina marassi', 'بلانكا مراسي', 'سيليا'],
'Hyde Park': ['HydePark', 'Hyde Park', 'Hydepark', 'هايد بارك', 'هيدبارك', 'هيد بارك'],
'Waterway': ['WaterWay', 'Water Way', 'Waterway', 'waterWay', 'واتر واي', 'The Waterway'],
'Eastown': ['EASTOWN', 'Eastown', 'eastown', 'سوديك ايستاون', 'ايستاون'],
'SODIC': ['Villette Sodic', 'SODIC Villette', 'sodic villette', 'سوديك', 'sodic', 'Sodic', 'SODIC', 'جون سوديك', 'سيزر سوديك', 'Allegria', 'Golf Extension', 'el patio 7', 'El Patio 7', 'EL Patio'],
'CFC': ['كايرو فيستيفال سيتي', 'CFC', 'Cairo festival city', 'cfc'], 'Mivida': ['MIVIDA', 'Mivida', 'mivida', 'ميفيدا', 'مافيدا'],
'Lake View': ['Lake view residence', 'Lake view Residence', 'ليك فيو'], 'Golf Porto Marina': ['Golf Porto marina', 'بورتو جولف الساحل الشمالي'],
'Sidi Abdel Rahman': ['Sidi Abdel Rahman', 'sidi abdel rahman', 'سيدي عبد الرحمن'],
'North Coast': ['الساحل الشمالي', 'seashore', 'Silversands North Coast', 'Silver Sands', 'مرسي باغوش', 'سيدي حنيش', 'الماظة باي'],
'Zayed': ['الشيخ زايد', 'zayed', 'Palm Hills Zayed', 'روضة زايد', 'Sephora Heights', 'Sephora'],
'Sokhna': ['العين السخنة', 'Sokhna', 'ازها السخنة'], 'Swan Lake': ['swan lake', 'سوان ليك الساحل'],
'Hacienda': ['Haceinda Waters', 'Hacienda bay', 'هاسيندا وايت'], '6th of October': ['6 اكتوبر', '6th of October', 'جاردن هيلز'],
'El Rehab': ['الرحاب'], 'El Obour': ['العبور'], 'El Shorouk': ['مدينة الشروق', 'الشروق'],
'Mostakbal City': ['المستقبل سيتي', 'مدينة نور'], 'New Capital': ['العاصمة الإدارية', 'البوسكو العاصمه'],
'Maadi': ['المعادي', 'كورنيش النيل'], 'Zamalek': ['Zamalek'],
'Palm Hills': ['Palm Hills', 'Capital Gardens Palm Hills', 'palm hills'],
'Katameya Heights': ['القطاميه هايتس', 'قطاميه هايتس', 'هايتس', 'heights', 'Heights'],
'Village Gate': ['Village Gate', 'VGK', 'village gate'], 'AZAD': ['AZAD'], 'Stone Residence': ['Stone residence'],
'Promenade': ['Promenade'], 'Saada': ['Saada'], 'Azzar': ['azar 1', 'azzar2'], 'Layan': ['layan'], 'New Giza': ['new giza'],
'La Vista': ['لافيستا', 'لافيستا سيتي'], 'Shouifat': ['الشويفات', 'بالشويفات'],
}
def parse_whatsapp_chat(file_path):
"""
Parses a single WhatsApp chat file. It tries multiple encodings to correctly read Arabic text.
Handles both plain text files and zip archives containing a .txt file.
"""
messages = []
actual_file_path = file_path
if zipfile.is_zipfile(file_path):
with zipfile.ZipFile(file_path, 'r') as zip_ref:
txt_files = [f for f in zip_ref.namelist() if f.endswith('.txt')]
if not txt_files:
raise ValueError("لا يوجد ملف نصي (.txt) داخل الأرشيف المضغوط.")
temp_dir = tempfile.mkdtemp()
zip_ref.extract(txt_files[0], temp_dir)
actual_file_path = os.path.join(temp_dir, txt_files[0])
encodings_to_try = ['utf-8', 'cp1256', 'latin-1']
file_content = None
for enc in encodings_to_try:
try:
with open(actual_file_path, 'r', encoding=enc) as f:
file_content = f.read()
break
except (UnicodeDecodeError, TypeError):
continue
if file_content is None:
raise ValueError(f"Could not read the file {os.path.basename(actual_file_path)} with supported encodings.")
content_lines = file_content.splitlines()
for line in content_lines:
match = re.match(r'^(\d{1,2}/\d{1,2}/\d{2,4}|\d{1,2}؟/\d{1,2}؟/\d{4}), (\d{1,2}:\d{2}\s*[ap]m|\d{1,2}:\d{2}\s*ص|\d{1,2}:\d{2}\s*م) - (.*?): (.*)$', line)
if match:
date_str = match.group(1).replace('؟', '')
if len(date_str.split('/')[2]) == 2:
date_str = date_str[:-2] + '20' + date_str[-2:]
sender = match.group(3)
message_content = match.group(4).strip()
messages.append({'date': date_str, 'sender': sender, 'content': message_content})
else:
if messages:
messages[-1]['content'] += '\n' + line.strip()
return messages
def is_real_estate_ad(message_content):
""" Checks if a message is a real estate advertisement based on keywords. """
property_types = [
'شقة', 'فيلا', 'تاون هاوس', 'توين هاوس', 'ستوديو', 'محل', 'مكتب', 'أرض', 'شاليه', 'بنتهاوس', 'دوبلكس', 'تريبلكس',
'Apartment', 'Villa', 'Townhouse', 'Twinhouse', 'Studio', 'Office', 'Chalet', 'Penthouse', 'Duplex', 'Triplex',
'روف', 'جاردن', 'ارضى', 'أرضي', 'اول', 'ثاني', 'ثالث', 'رابع', 'خامس', 'سادس', 'سابع', 'دور',
'Apt', 'flat', 'Sh', 'Building', 'مبنى'
]
transaction_keywords = [
'للبيع', 'للإيجار', 'for sale', 'for rent', 'rent', 'sale', 'مطلوب', 'سعر', 'price', 'asking price', 'total price', 'budget',
'مليون', 'ألف', 'k', 'm', 'جنيه', 'دولار', '$', 'EGP', 'SAR', 'USD'
]
area_keywords = [
'متر', 'sqm', 'm2', 'مساحة', 'area', 'build', 'بناء', 'roof', 'م²', 'فدان'
]
condition_keywords = [
'تشطيب', 'مفروش', 'بدون فرش', 'كامل التشطيب', 'بدون تشطيب', 'fully finished', 'semi finished', 'ultra super lux', 'core and shell', 'furnished', 'unfurnished',
'super lux', 'تشطيب كامل', 'نصف تشطيب', 'empty'
]
room_keywords = [
'bedroom', 'bed', 'غرفة', 'غرف', 'نوم', 'أوضه', 'Room', 'room'
]
bathroom_keywords = [
'Bathroom', 'Bath', 'bath', 'حمام'
]
has_property_type = any(re.search(r'\b' + re.escape(pt) + r'\b', message_content, re.IGNORECASE) for pt in property_types)
has_transaction_or_area = any(re.search(r'\b' + re.escape(tk) + r'\b', message_content, re.IGNORECASE) for tk in transaction_keywords + area_keywords)
has_condition = any(re.search(r'\b' + re.escape(ck) + r'\b', message_content, re.IGNORECASE) for ck in condition_keywords)
has_rooms_or_baths = any(re.search(r'\b' + re.escape(rk) + r'\b', message_content, re.IGNORECASE) for rk in room_keywords + bathroom_keywords)
is_ad = has_property_type and (has_transaction_or_area or has_condition or has_rooms_or_baths)
exclusion_patterns = [
r'Messages and calls are end-to-end encrypted', r'شوف البيت الي يناسبك', r'Media omitted',
r'مكتب عقارات', r'جروب خاص', r'محتار تبيع', r'محتار تشتري', r'السلام عليكم',
r'صباح الخير', r'مساء الخير', r'كيف الحال', 'ممكن سؤال', 'حد عنده',
r'لو سمحت', 'شكرا', 'تمام'
]
for pattern in exclusion_patterns:
if re.search(pattern, message_content, re.IGNORECASE):
return False
return is_ad
def extract_ad_details(message_content):
""" Extracts details from a real estate advertisement and translates the location. """
all_extracted_ads = []
ad_blocks = re.split(r'\n{2,}|\s*[-=]{3,}\s*|\s*(?:📸|🔑|🏠|💰|💵|💲){3,}\s*', message_content)
location_reverse_map = {alias.lower(): standard for standard, aliases in LOCATION_UNIFICATION_MAP.items() for alias in aliases}
property_type_patterns_original = {
'شقة': r'شقة|apartment|flat|apt', 'فيلا': r'فيلا|villa', 'تاون هاوس': r'تاون هاوس|townhouse',
'توين هاوس': r'توين هاوس|twinhouse', 'ستوديو': r'ستوديو|studio', 'محل': r'محل|shop|store',
'مكتب': r'مكتب|office|off', 'أرض': r'أرض|land|plot', 'شاليه': r'شاليه|chalet|sh',
'بنتهاوس': r'بنتهاوس|penthouse', 'دوبلكس': r'دوبلكس|duplex', 'تريبلكس': r'تريبلكس|triplex',
'روف': r'روف|roof', 'جاردن': r'جاردن|garden', 'مبنى': r'مبنى|building'
}
all_location_aliases = [alias for aliases in LOCATION_UNIFICATION_MAP.values() for alias in aliases]
locations_to_search = sorted(list(set(all_location_aliases)), key=len, reverse=True)
for block in ad_blocks:
block = block.strip()
if not block: continue
details = {
'نوع العقار': '', 'المنطقة': '', 'السعر': '', 'مساحة العقار': '',
'نوع العملية': '', 'Ad Text': '', 'price_value_numeric': None
}
# --- START: MODIFICATION TO EXCLUDE LAND ---
# أولاً، تحقق مما إذا كان الإعلان عن "أرض" وقم بتجاهله فوراً
if re.search(r'\b(أرض|land|plot)\b', block, re.IGNORECASE):
continue # تجاهل هذا الإعلان وانتقل إلى التالي
# --- END: MODIFICATION TO EXCLUDE LAND ---
for prop_type, pattern in property_type_patterns_original.items():
if re.search(r'\b' + pattern + r'\b', block, re.IGNORECASE):
details['نوع العقار'] = prop_type
break
unified_location = ''
for loc_alias in locations_to_search:
if re.search(r'\b' + re.escape(loc_alias) + r'\b', block, re.IGNORECASE):
unified_location = location_reverse_map.get(loc_alias.lower(), loc_alias)
break
if unified_location:
details['المنطقة'] = ARABIC_TO_ENGLISH_TRANSLATION.get(unified_location, unified_location)
area_patterns = [
r'(\d+(?:[.,]\d+)?)\s*(?:متر|م|sqm|m2|م²|meter|امتار|Meter|Area|area)',
r'(?:مساحة|مساحه|area|Area)\s*(\d+(?:[.,]\d+)?)',
r'(\d+(?:[.,]\d+)?)\s*فدان'
]
area_found = False
for pattern in area_patterns:
match = re.search(pattern, block, re.IGNORECASE)
if match:
# --- START: MODIFICATION TO EXCLUDE FEDDAN ---
# تحقق من وجود كلمة "فدان" وتجاهل الإعلان إذا وجدت
if 'فدان' in match.group(0).lower():
area_found = False # تأكد من أن هذا الإعلان سيتم تجاهله
break # اخرج من حلقة البحث عن المساحة
# --- END: MODIFICATION TO EXCLUDE FEDDAN ---
val_str = match.group(1) if match.group(1) else match.group(2)
if val_str:
val_numeric = float(val_str.translate(str.maketrans('٠١٢٣٤٥٦٧٨٩,', '0123456789.')))
unit = 'm'
if val_numeric < 21:
continue
details['مساحة العقار'] = f"{val_numeric} {unit}"
area_found = True
break
if not area_found:
continue
price_value = None
currency_symbol = "EGP"
price_pattern = r'(?:price|asking\s*price|total\s*price|سعر|ask|مطلوب|EGP|USD|\$)\s*[:\s]*((?:EGP|USD|\$|SAR)\s*)?([\d\u0660-\u0669]{1,3}(?:[.,]?[\d\u0660-\u0669]{3})*(?:[.,][\d\u0660-\u0669]+)?|[\d\u0660-\u0669.,]+)\s*(مليون|الف|ألف|k|m|milion|milon|million|جنيه|جنية|ج|دولار|Dollar|Dolar|EGP|USD|SAR)?'
price_match = re.search(price_pattern, block, re.IGNORECASE)
if price_match:
price_str = re.sub(r'[.,](?=\d{3}(?!\d))', '', price_match.group(2))
price_str = price_str.replace(',', '.')
price_str = price_str.translate(str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789'))
unit_word = (price_match.group(1) or '') + (price_match.group(3) or '')
try:
price_value = float(price_str)
if re.search(r'k|ألف|الف', unit_word, re.IGNORECASE): price_value *= 1000
elif re.search(r'm|مليون|milion|milon|million', unit_word, re.IGNORECASE): price_value *= 1000000
if re.search(r'\$|دولار|Dollar|Dolar|USD', unit_word + block, re.IGNORECASE): currency_symbol = "USD"
except (ValueError, TypeError):
price_value = None
if re.search(r'للبيع|for sale|sale|بيع|شراء', block, re.IGNORECASE): details['نوع العملية'] = 'For Sale'
elif re.search(r'للإيجار|for rent|rent|ايجار', block, re.IGNORECASE): details['نوع العملية'] = 'For Rent'
if not details['نوع العملية'] and price_value is not None:
if price_value < 100000 and currency_symbol == "EGP": details['نوع العملية'] = 'For Rent'
else: details['نوع العملية'] = 'For Sale'
if price_value is not None:
details['price_value_numeric'] = price_value
is_for_rent = details['نوع العملية'] == 'For Rent'
is_for_sale = details['نوع العملية'] == 'For Sale'
min_rent_egp, max_price_egp = 10000, 300000000
min_sale_egp = 1000000
min_rent_usd, max_price_usd = 200, 6000000
min_sale_usd = 20000
passes_filter = False
if currency_symbol == 'EGP':
if (is_for_rent and price_value >= min_rent_egp and price_value <= max_price_egp) or \
(is_for_sale and price_value >= min_sale_egp and price_value <= max_price_egp):
passes_filter = True
elif currency_symbol == 'USD':
if (is_for_rent and price_value >= min_rent_usd and price_value <= max_price_usd) or \
(is_for_sale and price_value >= min_sale_usd and price_value <= max_price_usd):
passes_filter = True
if passes_filter:
details['السعر'] = f"{int(price_value):,} {currency_symbol}"
else:
continue
else:
continue
if details['نوع العقار'] == 'جاردن':
details['نوع العقار'] = 'شقة'
for eng_name, pattern in PROPERTY_TYPE_MAPPING.items():
if re.search(r'\b' + pattern + r'\b', details['نوع العقار'], re.IGNORECASE):
details['نوع العقار'] = eng_name
break
required_fields = ['نوع العقار', 'المنطقة', 'مساحة العقار', 'نوع العملية', 'السعر']
if all(details.get(field) for field in required_fields):
del details['price_value_numeric']
details['Ad Text'] = block
all_extracted_ads.append(details)
return all_extracted_ads
def process_multiple_chat_files(files):
""" Processes multiple chat files and returns extracted real estate ads with duplicates removed. """
if not files:
return None, "لم يتم رفع أي ملفات."
all_ads = []
seen_texts = set()
removed_duplicates_count = 0
desired_columns_arabic = ['التاريخ', 'نوع العملية', 'نوع العقار', 'المنطقة', 'مساحة العقار', 'السعر', 'Ad Text']
try:
for file_obj in files:
file_path = file_obj.name
file_extension = os.path.splitext(file_path)[1].lower()
if file_extension in ['.txt', '.zip']:
messages = parse_whatsapp_chat(file_path)
for msg in messages:
if is_real_estate_ad(msg['content']):
extracted_ads_from_msg = extract_ad_details(msg['content'])
for details in extracted_ads_from_msg:
ad_text = details['Ad Text']
if ad_text in seen_texts:
removed_duplicates_count += 1
continue
seen_texts.add(ad_text)
ad_entry = {
'التاريخ': msg['date'],
'نوع العملية': details['نوع العملية'],
'نوع العقار': details['نوع العقار'],
'المنطقة': details['المنطقة'],
'مساحة العقار': details['مساحة العقار'],
'السعر': details['السعر'],
'Ad Text': ad_text
}
all_ads.append(ad_entry)
elif file_extension == '.xlsx':
df_excel = pd.read_excel(file_path, engine='openpyxl')
for index, row in df_excel.iterrows():
row_dict = row.to_dict()
details = {
'نوع العقار': row_dict.get('Property Type', row_dict.get('نوع العقار', '')),
'المنطقة': row_dict.get('Location', row_dict.get('المنطقة', '')),
'السعر': str(row_dict.get('Price', row_dict.get('السعر', ''))),
'مساحة العقار': str(row_dict.get('Area', row_dict.get('مساحة العقار', ''))),
'نوع العملية': row_dict.get('Transaction Type', row_dict.get('نوع العملية', '')),
'Ad Text': str(row_dict.get('Ad Text', ''))
}
ad_text = details['Ad Text']
if ad_text in seen_texts:
removed_duplicates_count += 1
continue
seen_texts.add(ad_text)
ad_entry = {
'التاريخ': str(row_dict.get('Date', row_dict.get('التاريخ', ''))),
'نوع العملية': details['نوع العملية'],
'نوع العقار': details['نوع العقار'],
'المنطقة': details['المنطقة'],
'مساحة العقار': details['مساحة العقار'],
'السعر': details['السعر'],
'Ad Text': ad_text
}
all_ads.append(ad_entry)
else:
return None, f"نوع الملف غير مدعوم: {file_extension}"
except Exception as e:
import traceback
error_details = traceback.format_exc()
file_name = os.path.basename(file_path) if 'file_path' in locals() else "الملف"
return None, f"خطأ في معالجة {file_name}: {str(e)}\n\nتفاصيل الخطأ:\n{error_details}"
if not all_ads:
return None, "لم يتم العثور على إعلانات عقارية كاملة ومطابقة للشروط في الملفات المرفوعة."
df = pd.DataFrame(all_ads, columns=desired_columns_arabic)
transaction_order = ['For Rent', 'For Sale']
property_type_order = ['Studio', 'Apartment', 'Chalet', 'Roof', 'Duplex', 'Triplex', 'Penthouse', 'Townhouse', 'Twinhouse', 'Villa', 'Building', 'Shop', 'Office', 'Land']
if 'نوع العملية' in df.columns:
df['نوع العملية'] = pd.Categorical(df['نوع العملية'], categories=transaction_order, ordered=True)
if 'نوع العقار' in df.columns:
df['نوع العقار'] = pd.Categorical(df['نوع العقار'], categories=property_type_order, ordered=True)
sort_by_cols = [col for col in ['نوع العملية', 'نوع العقار'] if col in df.columns]
if sort_by_cols:
df_sorted = df.sort_values(by=sort_by_cols)
else:
df_sorted = df
rename_map = {
'التاريخ': 'Date', 'نوع العملية': 'Transaction Type', 'نوع العقار': 'Property Type',
'المنطقة': 'Location', 'مساحة العقار': 'Area', 'السعر': 'Price',
'Ad Text': 'Ad Text'
}
df_sorted.rename(columns={k: v for k, v in rename_map.items() if k in df_sorted.columns}, inplace=True)
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8-sig', newline='') as temp_csv:
df_sorted.to_csv(temp_csv, index=False)
csv_file_path = temp_csv.name
return csv_file_path, f"تم استخراج {len(df_sorted)} إعلان عقاري فريد. تم إزالة {removed_duplicates_count} إعلان مكرر."
def create_interface():
with gr.Blocks(title="مُحلل إعلانات العقارات من واتساب", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# 🏠 مُحلل إعلانات العقارات من واتساب
قم برفع ملفات محادثات واتساب (.txt أو .zip) أو ملفات إكسل (.xlsx) لاستخراج وتحليل إعلانات العقارات منها.
**المميزات:**
- **توحيد وترجمة:** توحيد أسماء المناطق المختلفة وترجمتها للإنجليزية.
- **فلترة متقدمة:** قبول الإعلانات فقط ضمن نطاق سعري ومساحة محددين (أكبر من 21م، وأقل من 300 مليون).
- **ترتيب ذكي:** فرز النتائج النهائية (الإيجار أولاً ثم البيع، ثم حسب نوع العقار).
- **استبعاد المكررات:** إزالة الإعلانات المتكررة تلقائياً بناءً على النص الحرفي.
- **تصدير CSV:** تصدير النتائج النهائية مع دعم كامل للغة العربية.
""")
with gr.Row():
with gr.Column():
file_input = gr.File(
label="رفع ملفات (ملف نصي، مضغوط، أو إكسل)",
file_count="multiple",
file_types=['.txt', '.zip', '.xlsx'],
height=200
)
process_btn = gr.Button("🔍 تحليل الملفات", variant="primary", size="lg")
with gr.Column():
status_output = gr.Textbox(label="حالة المعالجة", interactive=False, lines=5)
download_file = gr.File(label="تحميل ملف النتائج (CSV)", interactive=False)
process_btn.click(
fn=process_multiple_chat_files,
inputs=[file_input],
outputs=[download_file, status_output]
)
gr.Markdown("""
---
**ملاحظات:**
- تأكد من أن الملفات النصية هي تصدير محادثات واتساب.
- يتم استبعاد الرسائل التي لا تحتوي على جميع البيانات الأساسية المطلوبة أو لا تطابق شروط السعر والمساحة.
- يتم استبعاد الإعلانات المتكررة بناءً على تطابق نص الإعلان حرفياً.
""")
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch()