Spaces:
Sleeping
Sleeping
| 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() |