|
|
|
|
|
|
|
|
|
|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
import re |
|
|
|
|
|
class ImprovedAnonymizationEvaluator: |
|
|
def __init__(self): |
|
|
|
|
|
self.iranian_banks = [ |
|
|
'بانک ملی', 'بانک صادرات', 'بانک پاسارگاد', 'بانک کشاورزی', |
|
|
'بانک ملت', 'بانک تجارت', 'بانک صنعت و معدن', 'بانک رسالت', |
|
|
'بانک دی', 'بانک پارسیان', 'بانک کارآفرین', 'بانک سامان', |
|
|
'بانک اقتصاد نوین', 'بانک مهر اقتصاد', 'بانک آینده' |
|
|
] |
|
|
|
|
|
|
|
|
self.government_orgs = [ |
|
|
'بانک مرکزی جمهوری اسلامی ایران', |
|
|
'دفتر اسناد رسمی', |
|
|
'اداره کل مالیات', |
|
|
'تامین اجتماعی', |
|
|
'وزارت دادگستری' |
|
|
] |
|
|
|
|
|
|
|
|
self.generic_words = { |
|
|
'همین بانک', 'این بانک', 'آن بانک', 'بانک مذکور', |
|
|
'همین شرکت', 'این شرکت', 'آن شرکت', 'شرکت مذکور', |
|
|
'همین شعبه', 'این شعبه', 'آن شعبه', 'شعبه مذکور', |
|
|
'شرکت متقاضی', 'دفتر حسابداری شرکت', |
|
|
'متقاضی', 'ایشان', 'وی', 'مشتری' |
|
|
} |
|
|
|
|
|
|
|
|
self.remove_words = [ |
|
|
'در', 'که', 'با', 'به', 'از', 'را', 'و', 'یا', 'است', 'بوده', |
|
|
'نموده', 'صادر', 'ارائه', 'معرفی', 'برگزار', 'واقع', 'مربوط', |
|
|
'مطرح', 'شد', 'شده' |
|
|
] |
|
|
|
|
|
self.patterns = { |
|
|
'person_names': { |
|
|
'pattern': re.compile(r'(?:آقای|خانم|مهندس|دکتر)\s+[\u0600-\u06FF\s]+?(?=\s+با\s+کد|\s+مدیر|$|،|\.)', re.UNICODE), |
|
|
'replacement': re.compile(r'person_\d+'), |
|
|
'name': 'اسامی اشخاص' |
|
|
}, |
|
|
'national_ids': { |
|
|
'pattern': re.compile(r'(?<!09)(?<!021-)(?<![0-9])\d{10,11}(?![0-9])'), |
|
|
'replacement': re.compile(r'id_number_\d+'), |
|
|
'name': 'کدهای ملی' |
|
|
}, |
|
|
'phone_numbers': { |
|
|
'pattern': re.compile(r'(?:09\d{9}|021-\d{8}|0\d{2,3}-?\d{7,8})'), |
|
|
'replacement': re.compile(r'phone_\d+'), |
|
|
'name': 'شماره تلفنها' |
|
|
}, |
|
|
'account_numbers': { |
|
|
'pattern': re.compile(r'\b\d{3}-\d{3}-\d{3}-\d{1}\b'), |
|
|
'replacement': re.compile(r'account_\d+'), |
|
|
'name': 'شماره حسابها' |
|
|
}, |
|
|
'card_numbers': { |
|
|
'pattern': re.compile(r'\b\d{4}-\d{4}-\d{4}-\d{4}\b'), |
|
|
'replacement': re.compile(r'card_number_\d+'), |
|
|
'name': 'شماره کارتها' |
|
|
}, |
|
|
'amounts': { |
|
|
'pattern': re.compile(r'\d{6,}\s*تومان'), |
|
|
'replacement': re.compile(r'amount_\d+'), |
|
|
'name': 'مبالغ مالی' |
|
|
}, |
|
|
'dates': { |
|
|
'pattern': re.compile(r'(?:\d{4}\/\d{2}\/\d{2}|۳۰\s*اسفند\s*۱۴۰۳|\b\d{4}\b(?=\s*سال))'), |
|
|
'replacement': re.compile(r'date_\d+'), |
|
|
'name': 'تاریخها' |
|
|
}, |
|
|
'full_addresses': { |
|
|
'pattern': re.compile(r'(?:تهران|اصفهان|مشهد|شیراز|کرج|اهواز|قم|رشت|کرمان|یزد|بوشهر|ارومیه|همدان|بندر عباس|ساری|اردبیل|خرمآباد|ایلام|بیرجند|گرگان|زنجان|سنندج|شهرکرد|سبزوار|قزوین|زاهدان|خوی|مراغه|کاشان|نجفآباد|شاهینشهر|ملایر|آبادان|دزفول|بابل|آمل|شاهرود|گنبد کاووس|خرمشهر|جهرم|فسا|مرودشت|لار|داراب|فیروزآباد|کازرون|سپیدان|نیریز|استهبان|فارسان|میانه|ورامین|قرچک|ری|پاکدشت|دماوند|فیروزکوه|شهریار|اسلامشهر|ملارد|قدس|بهارستان|چهاردانگه),\s*(?:میدان|خیابان|کوچه|شهرک|بلوار|کوی|محله)\s+[\u0600-\u06FF\u200C\u0621\u06F0-\u06F9\s]+(?:،\s*(?:برج|ساختمان|مجتمع)\s+[\u0600-\u06FF\u200C\u0621\u06F0-\u06F9\s]+)?(?:،\s*(?:طبقه|واحد)\s+[\u0600-\u06FF\u200C\u0621\u06F0-\u06F9\d\s]+)?', re.UNICODE), |
|
|
'replacement': re.compile(r'(?:full_address_\d+|location_\d+)'), |
|
|
'name': 'آدرسهای کامل' |
|
|
}, |
|
|
'iranian_banks': { |
|
|
'pattern': re.compile(f"({'|'.join(re.escape(bank) for bank in self.iranian_banks)})", re.UNICODE), |
|
|
'replacement': re.compile(r'company_\d+'), |
|
|
'name': 'بانکهای ایران' |
|
|
}, |
|
|
'government_orgs': { |
|
|
'pattern': re.compile(f"({'|'.join(re.escape(org) for org in self.government_orgs)})", re.UNICODE), |
|
|
'replacement': re.compile(r'company_\d+'), |
|
|
'name': 'سازمانهای دولتی' |
|
|
}, |
|
|
'other_companies': { |
|
|
'pattern': re.compile(r'شرکت\s+[\u0600-\u06FF\u200C\u0621\u06F0-\u06F9\s]+?(?=\s|$|،|\.)', re.UNICODE), |
|
|
'replacement': re.compile(r'company_\d+'), |
|
|
'name': 'سایر شرکتها' |
|
|
}, |
|
|
'invoice_numbers': { |
|
|
'pattern': re.compile(r'(?:INV-\d{4}-\d{4}|RPT-\d{4}-\d{4})'), |
|
|
'replacement': re.compile(r'(?:invoice_\d+|report_\d+)'), |
|
|
'name': 'شماره فاکتور و گزارش' |
|
|
}, |
|
|
'document_offices': { |
|
|
'pattern': re.compile(r'دفتر\s+اسناد\s+رسمی\s+شماره\s+\d+'), |
|
|
'replacement': re.compile(r'(?:contract_\d+|office_\d+)'), |
|
|
'name': 'دفاتر اسناد رسمی' |
|
|
}, |
|
|
'cheque_numbers': { |
|
|
'pattern': re.compile(r'چک\s+شماره\s+\d+'), |
|
|
'replacement': re.compile(r'cheque_\d+'), |
|
|
'name': 'شماره چکها' |
|
|
} |
|
|
} |
|
|
|
|
|
def is_generic_word(self, text): |
|
|
"""بررسی کلمات عمومی که نباید entity محسوب شوند""" |
|
|
text_clean = text.strip().lower() |
|
|
|
|
|
if text_clean in self.generic_words: |
|
|
return True |
|
|
|
|
|
if text_clean.startswith(('همین ', 'این ', 'آن ')): |
|
|
return True |
|
|
|
|
|
return len(text.strip()) < 3 |
|
|
|
|
|
def clean_entity(self, text): |
|
|
"""تمیز کردن دقیق entity""" |
|
|
|
|
|
pattern = r'\s*(' + '|'.join(re.escape(word) for word in self.remove_words) + r')\s*$' |
|
|
text = re.sub(pattern, '', text, flags=re.IGNORECASE) |
|
|
text = re.sub(r'\s+', ' ', text).strip() |
|
|
return text |
|
|
|
|
|
def is_valid_entity(self, text, category): |
|
|
"""بررسی دقیقتر معتبر بودن entity""" |
|
|
if len(text) < 3 or len(text) > 100: |
|
|
return False |
|
|
|
|
|
if self.is_generic_word(text): |
|
|
return False |
|
|
|
|
|
forbidden_words = [ |
|
|
'شد', 'کرد', 'است', 'بود', 'در', 'که', 'با', 'از', 'به', 'را', 'و', 'یا', |
|
|
'شده', 'نموده', 'صادر', 'ارائه', 'معرفی', 'برگزار', 'مطرح', 'واقع' |
|
|
] |
|
|
|
|
|
if text.lower().strip() in forbidden_words: |
|
|
return False |
|
|
|
|
|
if category in ['iranian_banks', 'other_companies']: |
|
|
if any(word in text.lower() for word in ['برگزار', 'مطرح', 'شد', 'است', 'نموده']): |
|
|
return False |
|
|
if text.strip() in ['شرکت', 'بانک', 'شرکت در', 'بانک در']: |
|
|
return False |
|
|
|
|
|
elif category == 'person_names': |
|
|
if text.strip() in ['آقای', 'خانم', 'مهندس', 'دکتر']: |
|
|
return False |
|
|
|
|
|
return True |
|
|
|
|
|
def analyze_entities(self, original_text, anonymized_text): |
|
|
"""تحلیل موجودیتهای شناسایی شده و ناشناس شده""" |
|
|
results = {} |
|
|
|
|
|
for entity_type, config in self.patterns.items(): |
|
|
original_matches = config['pattern'].findall(original_text) |
|
|
replacement_matches = config['replacement'].findall(anonymized_text) |
|
|
|
|
|
|
|
|
clean_original = [] |
|
|
for match in original_matches: |
|
|
cleaned = self.clean_entity(match) |
|
|
if self.is_valid_entity(cleaned, entity_type): |
|
|
clean_original.append(cleaned) |
|
|
|
|
|
|
|
|
anonymized_count = 0 |
|
|
for entity in clean_original: |
|
|
if not anonymized_text.count(entity.strip()): |
|
|
anonymized_count += 1 |
|
|
|
|
|
|
|
|
if len(replacement_matches) > anonymized_count: |
|
|
anonymized_count = min(len(replacement_matches), len(clean_original)) |
|
|
|
|
|
percentage = (anonymized_count / len(clean_original) * 100) if clean_original else 0 |
|
|
|
|
|
results[entity_type] = { |
|
|
'name': config['name'], |
|
|
'total': len(clean_original), |
|
|
'anonymized': anonymized_count, |
|
|
'percentage': round(percentage, 1), |
|
|
'samples': clean_original[:3] if clean_original else [] |
|
|
} |
|
|
|
|
|
return results |
|
|
|
|
|
def evaluate_csv(self, csv_file): |
|
|
"""ارزیابی فایل CSV و تولید گزارش""" |
|
|
try: |
|
|
if csv_file is None: |
|
|
return "لطفاً یک فایل CSV آپلود کنید." |
|
|
|
|
|
try: |
|
|
df = pd.read_csv(csv_file.name, encoding='utf-8') |
|
|
except: |
|
|
try: |
|
|
df = pd.read_csv(csv_file.name, encoding='utf-8', sep='\t') |
|
|
except: |
|
|
df = pd.read_csv(csv_file.name, encoding='utf-8-sig') |
|
|
|
|
|
if 'original_text' not in df.columns or 'anonymized_text' not in df.columns: |
|
|
return "فایل CSV باید شامل ستونهای 'original_text' و 'anonymized_text' باشد." |
|
|
|
|
|
overall_stats = {} |
|
|
total_entities = 0 |
|
|
total_anonymized = 0 |
|
|
|
|
|
for _, row in df.iterrows(): |
|
|
if pd.isna(row['original_text']) or pd.isna(row['anonymized_text']): |
|
|
continue |
|
|
|
|
|
row_analysis = self.analyze_entities(str(row['original_text']), str(row['anonymized_text'])) |
|
|
|
|
|
for entity_type, data in row_analysis.items(): |
|
|
if entity_type not in overall_stats: |
|
|
overall_stats[entity_type] = { |
|
|
'name': data['name'], |
|
|
'total': 0, |
|
|
'anonymized': 0, |
|
|
'samples': [] |
|
|
} |
|
|
|
|
|
overall_stats[entity_type]['total'] += data['total'] |
|
|
overall_stats[entity_type]['anonymized'] += data['anonymized'] |
|
|
overall_stats[entity_type]['samples'].extend(data['samples']) |
|
|
|
|
|
total_entities += data['total'] |
|
|
total_anonymized += data['anonymized'] |
|
|
|
|
|
for entity_type in overall_stats: |
|
|
stats = overall_stats[entity_type] |
|
|
stats['percentage'] = round((stats['anonymized'] / stats['total'] * 100) if stats['total'] > 0 else 0, 1) |
|
|
stats['samples'] = list(set(stats['samples']))[:3] |
|
|
|
|
|
return self.generate_report(overall_stats, total_entities, total_anonymized, len(df)) |
|
|
|
|
|
except Exception as e: |
|
|
return f"خطا در پردازش فایل: {str(e)}" |
|
|
|
|
|
def generate_report(self, stats, total_entities, total_anonymized, total_rows): |
|
|
"""تولید گزارش کامل متریکها""" |
|
|
|
|
|
report = f"""# گزارش ارزیابی ناشناسسازی متن (نسخه بهبود یافته) |
|
|
|
|
|
## خلاصه کلی |
|
|
- **تعداد ردیفهای پردازش شده**: {total_rows:,} ردیف |
|
|
- **تعداد موجودیتهای حساس شناسایی شده**: {total_entities:,} مورد |
|
|
- **تعداد موجودیتهای ناشناس شده**: {total_anonymized:,} مورد |
|
|
- **درصد پوشش کلی**: {(total_anonymized/total_entities*100) if total_entities > 0 else 0:.1f}% |
|
|
|
|
|
## بهبودهای اعمال شده |
|
|
✅ **آدرسهای کامل**: شناسایی دقیق آدرسها شامل میدان، برج، طبقه و واحد |
|
|
✅ **اسامی کامل**: پشتیبانی از خانم، مهندس، دکتر |
|
|
✅ **شرکتهای پیچیده**: شناسایی "شرکت پردازش دادههای ایرانیان" |
|
|
✅ **جداسازی دقیق**: کد ملی از شماره تلفن جدا شده |
|
|
✅ **فیلتر هوشمند**: حذف "همین بانک"، "شرکت متقاضی" |
|
|
✅ **بانکهای ایران**: فهرست کامل 15 بانک اصلی |
|
|
✅ **سازمانهای دولتی**: شناسایی نهادهای رسمی |
|
|
✅ **اسناد و فاکتورها**: شماره فاکتور، چک، دفتر اسناد |
|
|
|
|
|
## تحلیل تفصیلی دستهبندی موجودیتها |
|
|
|
|
|
""" |
|
|
|
|
|
excellent = [] |
|
|
good = [] |
|
|
poor = [] |
|
|
not_found = [] |
|
|
|
|
|
for entity_type, data in stats.items(): |
|
|
if data['total'] == 0: |
|
|
not_found.append((entity_type, data)) |
|
|
elif data['percentage'] == 100: |
|
|
excellent.append((entity_type, data)) |
|
|
elif data['percentage'] >= 80: |
|
|
good.append((entity_type, data)) |
|
|
else: |
|
|
poor.append((entity_type, data)) |
|
|
|
|
|
if excellent: |
|
|
report += "### ✅ عملکرد عالی (100% موفقیت)\n" |
|
|
for entity_type, data in excellent: |
|
|
report += f"- **{data['name']}**: {data['anonymized']}/{data['total']} (100%)\n" |
|
|
report += "\n" |
|
|
|
|
|
if good: |
|
|
report += "### 🟡 عملکرد خوب (80-99% موفقیت)\n" |
|
|
for entity_type, data in good: |
|
|
report += f"- **{data['name']}**: {data['anonymized']}/{data['total']} ({data['percentage']}%)\n" |
|
|
report += "\n" |
|
|
|
|
|
if poor: |
|
|
report += "### 🔴 عملکرد ضعیف (<80% موفقیت)\n" |
|
|
for entity_type, data in poor: |
|
|
missed = data['total'] - data['anonymized'] |
|
|
report += f"- **{data['name']}**: {data['anonymized']}/{data['total']} ({data['percentage']}%) - {missed} مورد جا مانده\n" |
|
|
if data['samples']: |
|
|
report += f" نمونههای شناسایی شده: {', '.join(data['samples'][:2])}\n" |
|
|
report += "\n" |
|
|
|
|
|
if not_found: |
|
|
report += "### ⚪ موجودیتهای یافت نشده\n" |
|
|
for entity_type, data in not_found: |
|
|
report += f"- **{data['name']}**: هیچ موجودیتی یافت نشد\n" |
|
|
report += "\n" |
|
|
|
|
|
report += "## جدول خلاصه متریکها\n\n" |
|
|
report += "| دسته موجودیت | یافته شده | ناشناس شده | درصد موفقیت | موارد جا مانده |\n" |
|
|
report += "|---------------|-----------|-------------|-------------|----------------|\n" |
|
|
|
|
|
for entity_type, data in stats.items(): |
|
|
if data['total'] > 0: |
|
|
missed = data['total'] - data['anonymized'] |
|
|
report += f"| {data['name']} | {data['total']} | {data['anonymized']} | {data['percentage']}% | {missed} |\n" |
|
|
|
|
|
major_issues = [(k, v) for k, v in stats.items() if v['total'] > 0 and v['percentage'] < 80] |
|
|
major_issues.sort(key=lambda x: x[1]['total'] - x[1]['anonymized'], reverse=True) |
|
|
|
|
|
if major_issues: |
|
|
report += "\n## 🚨 مشکلات اصلی شناسایی شده\n\n" |
|
|
for i, (entity_type, data) in enumerate(major_issues, 1): |
|
|
missed = data['total'] - data['anonymized'] |
|
|
impact = round(missed / total_entities * 100, 1) if total_entities > 0 else 0 |
|
|
report += f"### {i}. {data['name']}\n" |
|
|
report += f"- **وضعیت**: {data['percentage']}% موفقیت\n" |
|
|
report += f"- **موارد جا مانده**: {missed} مورد از {data['total']} مورد\n" |
|
|
report += f"- **تاثیر بر کل**: {impact}% از کل موجودیتها\n" |
|
|
if data['samples']: |
|
|
report += f"- **نمونهها**: {', '.join(data['samples'][:3])}\n" |
|
|
report += "\n" |
|
|
|
|
|
precision = round((total_anonymized / total_entities * 100) if total_entities > 0 else 0, 1) |
|
|
|
|
|
report += f"""## 📊 آمار نهایی |
|
|
|
|
|
- **کل موجودیتهای شناسایی شده**: {total_entities:,} |
|
|
- **کل موجودیتهای ناشناس شده**: {total_anonymized:,} |
|
|
- **موجودیتهای جا مانده**: {total_entities - total_anonymized:,} |
|
|
- **دقت (Precision)**: {precision}% |
|
|
- **پوشش (Recall)**: {precision}% |
|
|
- **امتیاز F1**: {precision}% |
|
|
|
|
|
## 🆕 ویژگیهای جدید اعمال شده |
|
|
- الگوهای regex بهبود یافته و دقیقتر |
|
|
- فیلتر کلمات عمومی و غیرمعنادار |
|
|
- شناسایی آدرسهای کامل با جزئیات |
|
|
- تشخیص اسامی با عناوین مختلف |
|
|
- جداسازی دقیق انواع موجودیتها |
|
|
- پشتیبانی از شرکتهای پیچیده |
|
|
- شناسایی اسناد و فاکتورها |
|
|
""" |
|
|
|
|
|
return report |
|
|
|
|
|
def create_interface(): |
|
|
evaluator = ImprovedAnonymizationEvaluator() |
|
|
|
|
|
def process_file(csv_file): |
|
|
if csv_file is None: |
|
|
return "لطفاً یک فایل CSV آپلود کنید." |
|
|
return evaluator.evaluate_csv(csv_file) |
|
|
|
|
|
with gr.Blocks(title="ارزیاب ناشناسسازی بهبود یافته") as demo: |
|
|
gr.Markdown(""" |
|
|
# 📊 ارزیاب ناشناسسازی متن فارسی (نسخه بهبود یافته) |
|
|
|
|
|
## 🆕 ویژگیهای جدید: |
|
|
- **آدرسهای کامل**: تهران، میدان ونک، برج میلاد، طبقه 15، واحد 1503 |
|
|
- **اسامی کامل**: خانم زهرا احمدی، مدیر مالی خانم احمدی |
|
|
- **شرکتهای پیچیده**: شرکت پردازش دادههای ایرانیان |
|
|
- **فیلتر هوشمند**: حذف "همین بانک"، "شرکت متقاضی" |
|
|
- **جداسازی دقیق**: کد ملی از شماره تلفن |
|
|
- **بانکهای ایران**: 15 بانک اصلی شناسایی میشود |
|
|
- **سازمانهای دولتی**: بانک مرکزی، دفتر اسناد رسمی |
|
|
- **اسناد**: شماره فاکتور، چک، دفتر اسناد |
|
|
""") |
|
|
|
|
|
file_input = gr.File(label="آپلود فایل CSV", file_types=[".csv"]) |
|
|
analyze_btn = gr.Button("محاسبه متریکها (نسخه بهبود یافته)", variant="primary") |
|
|
output = gr.Markdown(value="فایل CSV خود را آپلود کنید.") |
|
|
|
|
|
analyze_btn.click(fn=process_file, inputs=[file_input], outputs=[output]) |
|
|
|
|
|
return demo |
|
|
|
|
|
if __name__ == "__main__": |
|
|
app = create_interface() |
|
|
app.launch() |