| |
| |
|
|
| 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() |