import gradio as gr import pandas as pd import re from collections import defaultdict import io class TextAnonymizer: def __init__(self): self.person_counter = 0 self.company_counter = 0 self.amount_counter = 0 self.percent_counter = 0 # دیکشنری برای نگه‌داری تبدیل‌ها self.person_mapping = {} self.company_mapping = {} self.amount_mapping = {} self.percent_mapping = {} def reset_counters(self): """بازنشانی شمارنده‌ها برای پردازش جدید""" self.person_counter = 0 self.company_counter = 0 self.amount_counter = 0 self.percent_counter = 0 self.person_mapping.clear() self.company_mapping.clear() self.amount_mapping.clear() self.percent_mapping.clear() def detect_financial_amounts(self, text): """تشخیص مبالغ مالی (فارسی و انگلیسی)""" patterns = [ # الگوهای انگلیسی r'\$[\d,]+(?:\.\d{2})?', # $1,000.00 r'[\d,]+\s*(?:dollars?|USD|usd|Dollars?)', # 1000 dollars r'[\d,]+\s*(?:million|billion|thousand|Million|Billion|Thousand)', # 1 million r'[\d,]+(?:\.\d+)?\s*(?:M|B|K|m|b|k)', # 1.5M, 2B, 500K r'€[\d,]+(?:\.\d{2})?', # €1,000.00 r'£[\d,]+(?:\.\d{2})?', # £1,000.00 # الگوهای فارسی - ارقام فارسی r'[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:هزار|میلیون|میلیارد|تریلیون)\s*(?:و\s*[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:هزار|میلیون|میلیارد)?)?\s*(?:تومان|ریال|دلار|یورو|درهم)', r'[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:همت)', # ۳۷ همت r'[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:هزار|میلیون|میلیارد)\s*(?:تومان|ریال|دلار)', # الگوهای ترکیبی r'[\u06F0-\u06F9\u06F0-\u06F9\d]+[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]*\s*(?:هزار|میلیون|میلیارد)', r'بیش\s*از\s*[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:هزار|میلیون|میلیارد|همت)', r'حدود\s*[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:هزار|میلیون|میلیارد|همت)', r'نزدیک\s*به\s*[\u06F0-\u06F9\u06F0-\u06F9\d,\u060C]+\s*(?:هزار|میلیون|میلیارد|همت)', ] amounts = [] for pattern in patterns: matches = re.finditer(pattern, text, re.IGNORECASE) for match in matches: amounts.append((match.start(), match.end(), match.group())) return amounts def detect_percentages(self, text): """تشخیص درصدها (فارسی و انگلیسی)""" patterns = [ r'\d+(?:\.\d+)?%', # انگلیسی: 15%, 18.5% r'[\u06F0-\u06F9\u06F0-\u06F9]+(?:\.[\u06F0-\u06F9\u06F0-\u06F9]+)?\s*درصد', # فارسی: ۱۵ درصد r'[\u06F0-\u06F9\u06F0-\u06F9]+(?:\.[\u06F0-\u06F9\u06F0-\u06F9]+)?%', # ترکیبی: ۱۵% ] percentages = [] for pattern in patterns: matches = re.finditer(pattern, text) for match in matches: percentages.append((match.start(), match.end(), match.group())) return percentages def detect_names_regex(self, text): """تشخیص اسامی اشخاص (فارسی و انگلیسی)""" patterns = [ # الگوهای انگلیسی r'\b[A-Z][a-z]+ [A-Z][a-z]+\b', # John Smith r'\b[A-Z][a-z]+ [A-Z]\. [A-Z][a-z]+\b', # John M. Smith r'\b[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+\b', # John Michael Smith r'\bMr\. [A-Z][a-z]+\b', # Mr. Smith r'\bMs\. [A-Z][a-z]+\b', # Ms. Johnson r'\bDr\. [A-Z][a-z]+\b', # Dr. Brown # الگوهای فارسی - نام‌های کامل r'[\u0600-\u06FF]+\s+[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+)?(?:\s+[\u0600-\u06FF]+)?', # محمد ایرانی، جواد زارع‌پور # الگوهای خاص فارسی با عناوین r'(?:دکتر|آقای|خانم|مهندس|استاد)\s+[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){1,3}', # الگوی مدیرعامل و سمت‌ها r'[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){1,3}،\s*مدیرعامل', r'[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){1,3}،\s*(?:مدیر|رئیس|نایب)', # نام‌های بین عبارت‌ها r'(?:با\s+(?:حضور|سکانداری)\s+)[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){1,3}', r'(?:امضای\s+(?:مشترک\s+)?(?:«)?)[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){1,3}(?:»)?', ] names = [] processed_spans = set() # برای جلوگیری از تداخل for pattern in patterns: matches = re.finditer(pattern, text) for match in matches: start, end = match.start(), match.end() name = match.group().strip() # پاک کردن عناوین و کلمات اضافی name = re.sub(r'^(?:دکتر|آقای|خانم|مهندس|استاد)\s+', '', name) name = re.sub(r'،\s*(?:مدیرعامل|مدیر|رئیس|نایب).*', '', name) name = re.sub(r'^(?:با\s+(?:حضور|سکانداری)\s+)', '', name) name = re.sub(r'^(?:امضای\s+(?:مشترک\s+)?(?:«)?)', '', name) name = name.strip('،» ()') # بررسی طول نام و عدم تداخل if (len(name.split()) >= 2 and len(name) > 3 and not any(start < existing_end and end > existing_start for existing_start, existing_end in processed_spans)): names.append((start, end, name)) processed_spans.add((start, end)) return names def detect_companies_regex(self, text): """تشخیص شرکت‌ها (فارسی و انگلیسی)""" # الگوهای عمومی انگلیسی general_patterns = [ r'\b[A-Z][a-z]+ (?:Inc|Corp|LLC|Ltd|Company|Co|Corporation|Group|Technologies|Tech|Systems|Solutions|Services|International|Global|Enterprises)\.?\b', r'\b[A-Z][A-Z]+ (?:Inc|Corp|LLC|Ltd|Company|Co|Corporation)\.?\b', # مثل IBM Corp r'\b[A-Z][a-z]+ [A-Z][a-z]+ (?:Inc|Corp|LLC|Ltd|Company|Co|Corporation)\.?\b', # مثل Apple Inc ] # شرکت‌های مشهور انگلیسی tech_companies = r'\b(?:Apple|Microsoft|Google|Amazon|Facebook|Meta|Netflix|Tesla|Oracle|IBM|Intel|Cisco|Adobe|Salesforce|PayPal|Uber|Airbnb|Twitter|LinkedIn|NVIDIA|AMD|Zoom|Slack|Dropbox|Spotify)\b' auto_companies = r'\b(?:Toyota|Honda|Ford|BMW|Mercedes|Audi|Volkswagen|Nissan|Hyundai|Kia|Mazda|Subaru|Volvo|Porsche|Ferrari|Lamborghini)\b' finance_companies = r'\b(?:JPMorgan|Goldman Sachs|Morgan Stanley|Bank of America|Wells Fargo|Chase|Citibank|American Express|Visa|Mastercard|PayPal)\b' retail_companies = r'\b(?:Walmart|Target|Costco|Amazon|eBay|Alibaba|Nike|Adidas|Zara|H&M|IKEA|Starbucks|McDonalds|KFC|Subway)\b' # الگوهای فارسی - شرکت‌ها و سازمان‌ها persian_company_patterns = [ # الگوی کلی شرکت r'شرکت\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'گروه\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'هلدینگ\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'بانک\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'بیمه\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'پتروشیمی\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'صنایع\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'فولاد\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'سازمان\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', r'موسسه\s+[\u0600-\u06FF\s]+(?:[\u0600-\u06FF])', # شرکت‌های خاص از نمونه‌ها r'ایران\s*خودرو', r'همراه\s*اول', r'فولاد\s*مبارکه(?:\s+اصفهان)?', r'بانک\s+(?:ملت|پاسارگاد|سرمایه|مرکزی|کشاورزی)', r'بیمه\s+(?:پارسیان|سامان)', r'پتروشیمی\s+(?:پارس|بوعلی\s*سینا|اروند)', # الگوی با مخفف r'[\u0600-\u06FF\s]+\s*\([\u0600-\u06FF\s]+\)', # مثل تأمین (تیپیکو) # الگوی نام‌های مرکب r'[\u0600-\u06FF]+(?:\s+[\u0600-\u06FF]+){1,4}(?:\s+(?:شرکت|گروه|بانک|بیمه|صنایع))?', ] # ترکیب تمام الگوها all_patterns = general_patterns + [ tech_companies, auto_companies, finance_companies, retail_companies ] + persian_company_patterns companies = [] processed_spans = set() # برای جلوگیری از تداخل for pattern in all_patterns: matches = re.finditer(pattern, text, re.IGNORECASE) for match in matches: start, end = match.start(), match.end() company = match.group().strip() # فیلتر کردن نتایج خیلی کوتاه یا طولانی if (len(company) > 2 and len(company) < 100 and not any(start < existing_end and end > existing_start for existing_start, existing_end in processed_spans)): companies.append((start, end, company)) processed_spans.add((start, end)) return companies def anonymize_text(self, text): """ناشناس‌سازی متن با regex""" if not text or pd.isna(text): return text replacements = [] # تشخیص اسامی اشخاص names = self.detect_names_regex(text) for start, end, name in names: if name not in self.person_mapping: self.person_counter += 1 self.person_mapping[name] = f"person-{self.person_counter:02d}" replacements.append((start, end, self.person_mapping[name])) # تشخیص شرکت‌ها companies = self.detect_companies_regex(text) for start, end, company in companies: if company not in self.company_mapping: self.company_counter += 1 self.company_mapping[company] = f"company-{self.company_counter:02d}" replacements.append((start, end, self.company_mapping[company])) # تشخیص مبالغ مالی amounts = self.detect_financial_amounts(text) for start, end, amount in amounts: if amount not in self.amount_mapping: self.amount_counter += 1 self.amount_mapping[amount] = f"amount-{self.amount_counter:02d}" replacements.append((start, end, self.amount_mapping[amount])) # تشخیص درصدها percentages = self.detect_percentages(text) for start, end, percent in percentages: if percent not in self.percent_mapping: self.percent_counter += 1 self.percent_mapping[percent] = f"percent-{self.percent_counter:02d}" replacements.append((start, end, self.percent_mapping[percent])) # حذف تداخل‌ها و مرتب‌سازی replacements = self.remove_overlaps(replacements) replacements.sort(key=lambda x: x[0], reverse=True) # اعمال جایگزینی‌ها result = text for start, end, replacement in replacements: result = result[:start] + replacement + result[end:] return result def remove_overlaps(self, replacements): """حذف تداخل‌ها در جایگزینی‌ها""" if not replacements: return [] # مرتب‌سازی بر اساس موقعیت شروع replacements.sort(key=lambda x: x[0]) filtered = [] for start, end, replacement in replacements: # بررسی تداخل با آخرین جایگزینی اضافه شده if not filtered or start >= filtered[-1][1]: filtered.append((start, end, replacement)) return filtered def get_mapping_summary(self): """خلاصه‌ای از تبدیل‌های انجام شده""" summary = [] if self.person_mapping: summary.append("**اسامی اشخاص:**") for original, anonymized in self.person_mapping.items(): summary.append(f"- {original} → {anonymized}") summary.append("") if self.company_mapping: summary.append("**نام شرکت‌ها:**") for original, anonymized in self.company_mapping.items(): summary.append(f"- {original} → {anonymized}") summary.append("") if self.amount_mapping: summary.append("**مبالغ مالی:**") for original, anonymized in self.amount_mapping.items(): summary.append(f"- {original} → {anonymized}") summary.append("") if self.percent_mapping: summary.append("**درصدها:**") for original, anonymized in self.percent_mapping.items(): summary.append(f"- {original} → {anonymized}") return "\n".join(summary) if summary else "هیچ موجودیت حساسی یافت نشد." # ایجاد نمونه از کلاس ناشناس‌ساز anonymizer = TextAnonymizer() def process_csv(file): """پردازش فایل CSV""" try: # بازنشانی شمارنده‌ها anonymizer.reset_counters() # بررسی فایل if file is None: return None, "لطفاً فایل CSV آپلود کنید.", "", None # خواندن فایل CSV if file.name.endswith('.csv'): df = pd.read_csv(file.name) else: return None, "لطفاً فایل CSV آپلود کنید.", "", None # بررسی وجود ستون original_text if 'original_text' not in df.columns: available_columns = ', '.join(df.columns.tolist()) return None, f"ستون 'original_text' در فایل یافت نشد. ستون‌های موجود: {available_columns}", "", None # ایجاد کپی از دیتافریم result_df = df.copy() # ناشناس‌سازی متن‌های ستون original_text result_df['anonymized_text'] = df['original_text'].apply(anonymizer.anonymize_text) # تبدیل به CSV برای دانلود output = io.StringIO() result_df.to_csv(output, index=False, encoding='utf-8') csv_content = output.getvalue() # ایجاد فایل CSV برای دانلود output_file = "anonymized_data.csv" with open(output_file, 'w', encoding='utf-8') as f: f.write(csv_content) # نمایش نمونه از نتایج sample_df = result_df[['original_text', 'anonymized_text']].head(10) # خلاصه تبدیل‌ها mapping_summary = anonymizer.get_mapping_summary() return output_file, f"✅ پردازش کامل شد! {len(df)} ردیف پردازش شد.", mapping_summary, sample_df except Exception as e: return None, f"❌ خطا در پردازش فایل: {str(e)}", "", None def process_single_text(text): """پردازش تک متن""" if not text.strip(): return "", "لطفاً متنی وارد کنید." anonymizer.reset_counters() anonymized = anonymizer.anonymize_text(text) mapping_summary = anonymizer.get_mapping_summary() return anonymized, mapping_summary # ایجاد رابط کاربری Gradio with gr.Blocks(title="ناشناس‌سازی متن", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🔒 برنامه ناشناس‌سازی متن (نسخه Regex) ⚡ **وضعیت:** حالت سریع - بدون نیاز به spaCy این برنامه متن‌های شما را ناشناس می‌کند و اطلاعات حساس زیر را جایگزین می‌کند: - 👤 **اسامی اشخاص** → person-01, person-02, ... - 🏢 **نام شرکت‌ها** → company-01, company-02, ... - 💰 **مبالغ مالی** → amount-01, amount-02, ... - 📊 **درصدها** → percent-01, percent-02, ... **نسخه ۱:** آدرس‌ها، مکان‌ها و تاریخ‌ها ناشناس‌سازی نمی‌شوند. """) with gr.Tabs(): # تب پردازش فایل CSV with gr.TabItem("📁 پردازش فایل CSV"): gr.Markdown("### آپلود فایل CSV با ستون 'original_text'") with gr.Row(): with gr.Column(): file_input = gr.File( label="فایل CSV را انتخاب کنید", file_types=[".csv"], type="filepath" ) process_btn = gr.Button("🚀 شروع پردازش", variant="primary") with gr.Column(): status_output = gr.Textbox( label="وضعیت", interactive=False ) download_file = gr.File( label="دانلود فایل ناشناس‌سازی شده", interactive=False ) with gr.Row(): with gr.Column(): mapping_output = gr.Markdown( label="خلاصه تبدیل‌ها", value="خلاصه تبدیل‌ها اینجا نمایش داده می‌شود..." ) with gr.Column(): sample_output = gr.Dataframe( label="نمونه نتایج (۱۰ ردیف اول)", interactive=False ) # تب تست تک متن with gr.TabItem("📝 تست تک متن"): gr.Markdown("### تست ناشناس‌سازی روی یک متن") with gr.Row(): with gr.Column(): text_input = gr.Textbox( label="متن اصلی", placeholder="متن خود را اینجا وارد کنید...", lines=5 ) test_btn = gr.Button("🔍 ناشناس‌سازی", variant="primary") with gr.Column(): text_output = gr.Textbox( label="متن ناشناس‌سازی شده", lines=5, interactive=False ) text_mapping = gr.Markdown( label="تبدیل‌های انجام شده" ) # اتصال رویدادها process_btn.click( fn=process_csv, inputs=[file_input], outputs=[download_file, status_output, mapping_output, sample_output] ) test_btn.click( fn=process_single_text, inputs=[text_input], outputs=[text_output, text_mapping] ) # مثال در بخش تست gr.Examples( examples=[ ["John Smith works at Microsoft and earned $50,000 with a 15% bonus."], ["Sarah Johnson from Google Inc. reported revenues of $2.5 million, representing a 25% increase."], ["The CEO of Apple, Tim Cook, announced profits of $1.2B with 18.5% growth rate."], ["Dr. Michael Brown from IBM Corp. received €75,000 salary increase of 12%."], ["Ms. Lisa Wilson at Amazon reported quarterly results of £500K with 8.7% margin."] ], inputs=[text_input], label="نمونه متن‌ها" ) # اجرای برنامه if __name__ == "__main__": demo.launch()