Spaces:
Paused
Paused
| # app.py - سیستم پیشرفته استخراج و تحلیل اسناد حقوقی فارسی | |
| import os | |
| import gc | |
| import sys | |
| import time | |
| import json | |
| import logging | |
| import resource | |
| import requests | |
| import threading | |
| import re | |
| import random | |
| from pathlib import Path | |
| from datetime import datetime | |
| from typing import List, Dict, Any, Optional, Tuple, Union | |
| from dataclasses import dataclass | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from urllib.parse import urljoin, urlparse | |
| import hashlib | |
| import gradio as gr | |
| import pandas as pd | |
| import torch | |
| from bs4 import BeautifulSoup | |
| from transformers import ( | |
| AutoTokenizer, | |
| AutoModelForSequenceClassification, | |
| pipeline, | |
| logging as transformers_logging | |
| ) | |
| import warnings | |
| # تنظیمات اولیه | |
| warnings.filterwarnings('ignore') | |
| transformers_logging.set_verbosity_error() | |
| # محدودیت حافظه برای HF Spaces | |
| try: | |
| resource.setrlimit(resource.RLIMIT_AS, (2*1024*1024*1024, 2*1024*1024*1024)) | |
| except: | |
| pass | |
| # تنظیمات محیط | |
| os.environ['TRANSFORMERS_CACHE'] = '/tmp/hf_cache' | |
| os.environ['HF_HOME'] = '/tmp/hf_cache' | |
| os.environ['TORCH_HOME'] = '/tmp/torch_cache' | |
| os.environ['TOKENIZERS_PARALLELISM'] = 'false' | |
| # تنظیم لاگ | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| # SVG Icons | |
| SVG_ICONS = { | |
| 'search': '🔍', | |
| 'document': '📄', | |
| 'analyze': '🤖', | |
| 'export': '📊', | |
| 'settings': '⚙️', | |
| 'preview': '👁️', | |
| 'link': '🔗', | |
| 'success': '✅', | |
| 'error': '❌', | |
| 'warning': '⚠️' | |
| } | |
| # منابع حقوقی معتبر ایران | |
| LEGAL_SOURCES_CONFIG = { | |
| "مجلس شورای اسلامی": { | |
| "base_url": "https://rc.majlis.ir", | |
| "patterns": ["/fa/law/", "/fa/report/", "/fa/news/"], | |
| "selectors": [".main-content", ".article-body", ".content", "article"], | |
| "delay_range": (2, 5), | |
| "max_depth": 2 | |
| }, | |
| "پورتال ملی قوانین": { | |
| "base_url": "https://www.dotic.ir", | |
| "patterns": ["/portal/", "/law/", "/regulation/"], | |
| "selectors": [".content-area", ".law-content", ".main-text"], | |
| "delay_range": (1, 4), | |
| "max_depth": 2 | |
| }, | |
| "قوه قضاییه": { | |
| "base_url": "https://www.judiciary.ir", | |
| "patterns": ["/fa/news/", "/fa/verdict/", "/fa/law/"], | |
| "selectors": [".news-content", ".verdict-text", ".main-content"], | |
| "delay_range": (3, 6), | |
| "max_depth": 2 | |
| }, | |
| "وزارت دادگستری": { | |
| "base_url": "https://www.moj.ir", | |
| "patterns": ["/fa/news/", "/fa/law/", "/fa/regulation/"], | |
| "selectors": [".news-body", ".law-text", ".content"], | |
| "delay_range": (2, 4), | |
| "max_depth": 2 | |
| }, | |
| "دیوان عدالت اداری": { | |
| "base_url": "https://www.adcourt.ir", | |
| "patterns": ["/fa/verdict/", "/fa/news/"], | |
| "selectors": [".verdict-content", ".news-text"], | |
| "delay_range": (1, 3), | |
| "max_depth": 2 | |
| } | |
| } | |
| # واژگان حقوقی فارسی پیشرفته | |
| PERSIAN_LEGAL_DICTIONARY = { | |
| "قوانین_اساسی": [ | |
| "قانون اساسی", "اصول قانون اساسی", "مجلس شورای اسلامی", "شورای نگهبان", | |
| "رهبری", "جمهوری اسلامی", "حاکمیت ملی", "ولایت فقیه" | |
| ], | |
| "قوانین_عادی": [ | |
| "ماده", "تبصره", "اصل", "فصل", "باب", "قسمت", "بخش", "کتاب", | |
| "قانون مدنی", "قانون جزا", "قانون آیین دادرسی", "قانون تجارت" | |
| ], | |
| "مقررات_اجرایی": [ | |
| "آییننامه", "مقرره", "دستورالعمل", "شیوهنامه", "بند", "جزء", "فقره", | |
| "ضابطه", "رهنمود", "دستور", "اعلامیه", "ابلاغیه" | |
| ], | |
| "اصطلاحات_حقوقی": [ | |
| "شخص حقیقی", "شخص حقوقی", "حق", "تکلیف", "مسئولیت", "جرم", "مجازات", | |
| "دعوا", "طرف دعوا", "خواهان", "خوانده", "شاکی", "متهم", "مجنیعلیه" | |
| ], | |
| "نهادهای_قضایی": [ | |
| "دادگاه", "قاضی", "دادرس", "مدعیالعموم", "وکیل", "کارشناس", "مترجم", | |
| "رای", "حکم", "قرار", "اجرائیه", "کیفرخواست", "لایحه دفاعیه" | |
| ], | |
| "اصطلاحات_اداری": [ | |
| "وزارت", "اداره", "سازمان", "مدیر", "مقام", "مسئول", "کارمند", "کارگزار", | |
| "بخشنامه", "تصویبنامه", "مصوبه", "تصمیم", "نظریه", "استعلام" | |
| ], | |
| "مفاهیم_مالی": [ | |
| "مالیات", "عوارض", "پرداخت", "وجه", "ریال", "درهم", "خسارت", "دیه", | |
| "تأمین", "ضمانت", "وثیقه", "سپرده", "جریمه", "جزای نقدی" | |
| ] | |
| } | |
| class ProcessingProgress: | |
| current_step: str = "" | |
| progress: float = 0.0 | |
| total_documents: int = 0 | |
| processed_documents: int = 0 | |
| status: str = "آماده" | |
| error: Optional[str] = None | |
| class MemoryManager: | |
| """مدیریت هوشمند حافظه برای HF Spaces""" | |
| def get_memory_usage() -> float: | |
| try: | |
| with open('/proc/self/status') as f: | |
| for line in f: | |
| if line.startswith('VmRSS:'): | |
| return float(line.split()[1]) / 1024 | |
| return 0.0 | |
| except: | |
| return 0.0 | |
| def check_memory_available(required_mb: float) -> bool: | |
| try: | |
| current_usage = MemoryManager.get_memory_usage() | |
| return current_usage + required_mb < 1800 | |
| except: | |
| return True | |
| def cleanup_memory(): | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| class SmartTextProcessor: | |
| """پردازشگر هوشمند متن با قابلیتهای پیشرفته""" | |
| def __init__(self): | |
| self.sentence_endings = ['۔', '.', '!', '؟', '?', ';', '؛'] | |
| self.paragraph_indicators = ['ماده', 'تبصره', 'بند', 'الف', 'ب', 'ج', 'د', 'ه', 'و'] | |
| # الگوهای شناسایی ارجاعات حقوقی | |
| self.citation_patterns = [ | |
| r'ماده\s*(\d+)', | |
| r'تبصره\s*(\d+)', | |
| r'بند\s*([الف-ی]|\d+)', | |
| r'فصل\s*(\d+)', | |
| r'باب\s*(\d+)', | |
| r'قسمت\s*(\d+)', | |
| r'اصل\s*(\d+)' | |
| ] | |
| # الگوهای پاکسازی متن | |
| self.cleanup_patterns = [ | |
| (r'\s+', ' '), | |
| (r'([۰-۹])\s+([۰-۹])', r'\1\2'), | |
| (r'([a-zA-Z])\s+([a-zA-Z])', r'\1\2'), | |
| (r'([ا-ی])\s+(ها|های|ان|ات|ین)', r'\1\2'), | |
| (r'(می|نمی|خواهد)\s+(شود|گردد|باشد)', r'\1\2'), | |
| ] | |
| def normalize_persian_text(self, text: str) -> str: | |
| """نرمالسازی پیشرفته متن فارسی""" | |
| if not text: | |
| return "" | |
| # نرمالسازی کاراکترهای فارسی | |
| persian_normalization = { | |
| 'ي': 'ی', 'ك': 'ک', 'ة': 'ه', 'ؤ': 'و', 'إ': 'ا', 'أ': 'ا', | |
| 'ء': '', 'ئ': 'ی', '٠': '۰', '١': '۱', '٢': '۲', '٣': '۳', '٤': '۴', | |
| '٥': '۵', '٦': '۶', '٧': '۷', '٨': '۸', '٩': '۹' | |
| } | |
| for old, new in persian_normalization.items(): | |
| text = text.replace(old, new) | |
| # اعمال الگوهای پاکسازی | |
| for pattern, replacement in self.cleanup_patterns: | |
| text = re.sub(pattern, replacement, text) | |
| # حذف کاراکترهای غیرضروری | |
| text = re.sub(r'[^\u0600-\u06FF\u200C\u200D\s\w\d.,;:!؟()«»\-]', '', text) | |
| return text.strip() | |
| def detect_long_sentences(self, text: str) -> List[Dict[str, Any]]: | |
| """تشخیص جملات طولانی و پیچیده""" | |
| sentences = self._split_into_sentences(text) | |
| long_sentences = [] | |
| for i, sentence in enumerate(sentences): | |
| sentence_info = { | |
| 'index': i, | |
| 'text': sentence, | |
| 'word_count': len(sentence.split()), | |
| 'is_long': False, | |
| 'suggestions': [] | |
| } | |
| # بررسی طول جمله | |
| if sentence_info['word_count'] > 30: | |
| sentence_info['is_long'] = True | |
| sentence_info['suggestions'].append('جمله بسیار طولانی - تقسیم توصیه میشود') | |
| # بررسی پیچیدگی | |
| complexity_score = self._calculate_complexity(sentence) | |
| if complexity_score > 5: | |
| sentence_info['suggestions'].append('جمله پیچیده - سادهسازی توصیه میشود') | |
| if sentence_info['is_long'] or sentence_info['suggestions']: | |
| long_sentences.append(sentence_info) | |
| return long_sentences | |
| def _split_into_sentences(self, text: str) -> List[str]: | |
| """تقسیم متن به جملات""" | |
| boundaries = self.detect_sentence_boundaries(text) | |
| sentences = [] | |
| start = 0 | |
| for boundary in boundaries: | |
| sentence = text[start:boundary].strip() | |
| if sentence and len(sentence) > 5: | |
| sentences.append(sentence) | |
| start = boundary | |
| # جمله آخر | |
| if start < len(text): | |
| last_sentence = text[start:].strip() | |
| if last_sentence and len(last_sentence) > 5: | |
| sentences.append(last_sentence) | |
| return sentences | |
| def _calculate_complexity(self, sentence: str) -> float: | |
| """محاسبه پیچیدگی جمله""" | |
| complexity = 0 | |
| # تعداد کلمات ربط | |
| conjunctions = ['که', 'اگر', 'چون', 'زیرا', 'ولی', 'اما', 'درحالیکه', 'درصورتیکه'] | |
| complexity += sum(sentence.count(conj) for conj in conjunctions) * 0.5 | |
| # تعداد ویرگول | |
| complexity += sentence.count('،') * 0.3 | |
| # تعداد کلمات | |
| complexity += len(sentence.split()) * 0.02 | |
| # جملات تودرتو | |
| if sentence.count('(') > 0: | |
| complexity += sentence.count('(') * 0.5 | |
| return complexity | |
| def detect_sentence_boundaries(self, text: str) -> List[int]: | |
| """تشخیص هوشمند مرزهای جمله در متون حقوقی فارسی""" | |
| boundaries = [] | |
| for i, char in enumerate(text): | |
| if char in self.sentence_endings: | |
| is_real_ending = True | |
| # بررسی برای اعداد و اختصارات | |
| if i > 0 and text[i-1].isdigit() and char == '.': | |
| is_real_ending = False | |
| # بررسی برای اختصارات رایج | |
| if i > 2: | |
| prev_text = text[max(0, i-10):i].strip() | |
| if any(abbr in prev_text for abbr in ['ماده', 'بند', 'ج.ا.ا', 'ق.م', 'ق.ج']): | |
| if char == '.' and i < len(text) - 1 and not text[i+1].isspace(): | |
| is_real_ending = False | |
| if is_real_ending: | |
| if i < len(text) - 1: | |
| next_char = text[i + 1] | |
| if next_char.isspace() or next_char in '«"\'': | |
| boundaries.append(i + 1) | |
| else: | |
| boundaries.append(i + 1) | |
| return boundaries | |
| def reconstruct_legal_text(self, content_fragments: List[str]) -> str: | |
| """بازسازی هوشمند متن حقوقی از قطعات پراکنده""" | |
| if not content_fragments: | |
| return "" | |
| # مرحله 1: نرمالسازی تمام قطعات | |
| normalized_fragments = [] | |
| for fragment in content_fragments: | |
| normalized = self.normalize_persian_text(fragment) | |
| if normalized and len(normalized.strip()) > 10: | |
| normalized_fragments.append(normalized) | |
| if not normalized_fragments: | |
| return "" | |
| # مرحله 2: ادغام هوشمند قطعات | |
| combined_text = self._smart_join_fragments(normalized_fragments) | |
| # مرحله 3: اعمال قالببندی حقوقی | |
| formatted = self._apply_legal_formatting(combined_text) | |
| return formatted | |
| def _smart_join_fragments(self, fragments: List[str]) -> str: | |
| """ادغام هوشمند قطعات با در نظر گیری زمینه""" | |
| if len(fragments) == 1: | |
| return fragments[0] | |
| result = [fragments[0]] | |
| for i in range(1, len(fragments)): | |
| current_fragment = fragments[i] | |
| prev_fragment = result[-1] | |
| # بررسی ادامه جمله | |
| if self._should_continue_sentence(prev_fragment, current_fragment): | |
| result[-1] += ' ' + current_fragment | |
| # بررسی ادغام بدون فاصله (نیمفاصله) | |
| elif self._should_join_without_space(prev_fragment, current_fragment): | |
| result[-1] += current_fragment | |
| # شروع پاراگراف جدید | |
| elif self._is_new_paragraph(current_fragment): | |
| result.append('\n\n' + current_fragment) | |
| else: | |
| result.append(' ' + current_fragment) | |
| return ''.join(result) | |
| def _should_continue_sentence(self, prev: str, current: str) -> bool: | |
| """تشخیص ادامه جمله""" | |
| # کلمات ادامهدهنده | |
| continuation_words = ['که', 'تا', 'اگر', 'چون', 'زیرا', 'ولی', 'اما', 'و', 'یا'] | |
| # اگر جمله قبلی ناتمام باشد | |
| if not any(prev.endswith(end) for end in self.sentence_endings): | |
| return True | |
| # اگر جمله فعلی با کلمه ادامه شروع شود | |
| if any(current.strip().startswith(word) for word in continuation_words): | |
| return True | |
| return False | |
| def _should_join_without_space(self, prev: str, current: str) -> bool: | |
| """تشخیص ادغام بدون فاصله""" | |
| # پسوندها و پیشوندها | |
| suffixes = ['ها', 'های', 'ان', 'ات', 'ین', 'تان', 'شان', 'تون', 'شون'] | |
| prefixes = ['می', 'نمی', 'برمی', 'درمی'] | |
| current_stripped = current.strip() | |
| # بررسی پسوندها | |
| if any(current_stripped.startswith(suffix) for suffix in suffixes): | |
| return True | |
| # بررسی ادامه فعل | |
| if prev.endswith(('می', 'نمی', 'خواهد', 'است')): | |
| verb_continuations = ['شود', 'گردد', 'باشد', 'کرد', 'کند'] | |
| if any(current_stripped.startswith(cont) for cont in verb_continuations): | |
| return True | |
| return False | |
| def _is_new_paragraph(self, text: str) -> bool: | |
| """تشخیص شروع پاراگراف جدید""" | |
| text_stripped = text.strip() | |
| # شاخصهای پاراگراف در متون حقوقی | |
| paragraph_starters = [ | |
| 'ماده', 'تبصره', 'بند', 'فصل', 'باب', 'قسمت', 'کتاب', | |
| 'الف)', 'ب)', 'ج)', 'د)', 'ه)', 'و)', 'ز)', 'ح)', 'ط)', | |
| '۱-', '۲-', '۳-', '۴-', '۵-', '۶-', '۷-', '۸-', '۹-', '۱۰-' | |
| ] | |
| return any(text_stripped.startswith(starter) for starter in paragraph_starters) | |
| def _apply_legal_formatting(self, text: str) -> str: | |
| """اعمال قالببندی مخصوص اسناد حقوقی""" | |
| lines = text.split('\n') | |
| formatted_lines = [] | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| # قالببندی مواد و تبصرهها | |
| if any(line.startswith(indicator) for indicator in ['ماده', 'تبصره']): | |
| formatted_lines.append(f"\n{line}") | |
| # قالببندی بندها | |
| elif any(line.startswith(indicator) for indicator in ['الف)', 'ب)', 'ج)']): | |
| formatted_lines.append(f" {line}") | |
| # قالببندی فصول و ابواب | |
| elif any(line.startswith(indicator) for indicator in ['فصل', 'باب', 'کتاب']): | |
| formatted_lines.append(f"\n\n{line.upper()}\n") | |
| else: | |
| formatted_lines.append(line) | |
| return '\n'.join(formatted_lines) | |
| def extract_legal_entities(self, text: str) -> Dict[str, List[str]]: | |
| """استخراج موجودیتهای حقوقی از متن""" | |
| entities = { | |
| 'articles': [], | |
| 'citations': [], | |
| 'legal_terms': [], | |
| 'organizations': [], | |
| 'laws': [] | |
| } | |
| # استخراج مواد و تبصرهها | |
| for pattern in self.citation_patterns: | |
| matches = re.findall(pattern, text) | |
| if matches: | |
| entities['citations'].extend(matches) | |
| # استخراج اصطلاحات حقوقی | |
| for category, terms in PERSIAN_LEGAL_DICTIONARY.items(): | |
| found_terms = [term for term in terms if term in text] | |
| entities['legal_terms'].extend(found_terms) | |
| # استخراج نام قوانین | |
| law_patterns = [ | |
| r'قانون\s+([^۔\.\n]{5,50})', | |
| r'آییننامه\s+([^۔\.\n]{5,50})', | |
| r'مقرره\s+([^۔\.\n]{5,50})' | |
| ] | |
| for pattern in law_patterns: | |
| matches = re.findall(pattern, text) | |
| entities['laws'].extend(matches) | |
| return entities | |
| class AntiDDoSManager: | |
| """مدیریت ضد حملات DDoS و تنوع درخواستها""" | |
| def __init__(self): | |
| self.request_history = {} | |
| self.user_agents = [ | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', | |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0' | |
| ] | |
| def get_request_delay(self, domain: str) -> float: | |
| """محاسبه تأخیر مناسب برای هر منبع""" | |
| source_info = self._identify_source(domain) | |
| if source_info and 'delay_range' in source_info: | |
| min_delay, max_delay = source_info['delay_range'] | |
| return random.uniform(min_delay, max_delay) | |
| # تأخیر پیشفرض | |
| return random.uniform(1, 3) | |
| def get_random_headers(self) -> Dict[str, str]: | |
| """تولید هدرهای تصادفی""" | |
| return { | |
| 'User-Agent': random.choice(self.user_agents), | |
| 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', | |
| 'Accept-Language': random.choice(['fa,en;q=0.9', 'fa-IR,fa;q=0.9,en;q=0.8', 'fa;q=0.9,en;q=0.8']), | |
| 'Accept-Encoding': 'gzip, deflate', | |
| 'Connection': 'keep-alive', | |
| 'Upgrade-Insecure-Requests': '1', | |
| 'Cache-Control': random.choice(['no-cache', 'max-age=0', 'no-store']) | |
| } | |
| def _identify_source(self, domain: str) -> Optional[Dict]: | |
| """شناسایی منبع بر اساس دامنه""" | |
| for source_name, config in LEGAL_SOURCES_CONFIG.items(): | |
| base_domain = config['base_url'].replace('https://', '').replace('http://', '') | |
| if base_domain in domain: | |
| return config | |
| return None | |
| def should_allow_request(self, domain: str) -> bool: | |
| """بررسی اینکه آیا درخواست مجاز است""" | |
| current_time = time.time() | |
| if domain not in self.request_history: | |
| self.request_history[domain] = [] | |
| # حذف درخواستهای قدیمی (بیش از 1 ساعت) | |
| self.request_history[domain] = [ | |
| req_time for req_time in self.request_history[domain] | |
| if current_time - req_time < 3600 | |
| ] | |
| # بررسی تعداد درخواستها در ساعت گذشته | |
| if len(self.request_history[domain]) >= 50: # حداکثر 50 درخواست در ساعت | |
| return False | |
| # ثبت درخواست جدید | |
| self.request_history[domain].append(current_time) | |
| return True | |
| class ModelManager: | |
| """مدیریت پیشرفته مدلهای هوش مصنوعی""" | |
| def __init__(self): | |
| self.models = {} | |
| self.model_status = {} | |
| def load_models_progressively(self, progress_callback=None) -> Dict[str, Any]: | |
| """بارگذاری تدریجی مدلها با مدیریت حافظه""" | |
| logger.info("🚀 شروع بارگذاری مدلهای هوش مصنوعی...") | |
| if progress_callback: | |
| progress_callback("آمادهسازی محیط...", 0.05) | |
| # ایجاد دایرکتوری کش | |
| cache_dir = "/tmp/hf_cache" | |
| os.makedirs(cache_dir, exist_ok=True) | |
| # فاز 1: Tokenizer | |
| try: | |
| if MemoryManager.check_memory_available(100): | |
| logger.info("📝 بارگذاری Tokenizer...") | |
| if progress_callback: | |
| progress_callback("بارگذاری Tokenizer...", 0.2) | |
| self.models['tokenizer'] = AutoTokenizer.from_pretrained( | |
| "HooshvareLab/bert-fa-base-uncased", | |
| cache_dir=cache_dir, | |
| local_files_only=False | |
| ) | |
| self.model_status['tokenizer'] = 'loaded' | |
| logger.info("✅ Tokenizer بارگذاری شد") | |
| else: | |
| self.model_status['tokenizer'] = 'memory_insufficient' | |
| except Exception as e: | |
| logger.error(f"❌ خطا در بارگذاری Tokenizer: {e}") | |
| self.model_status['tokenizer'] = 'failed' | |
| # فاز 2: مدل طبقهبندی متن | |
| try: | |
| if MemoryManager.check_memory_available(400): | |
| logger.info("🏷️ بارگذاری مدل طبقهبندی...") | |
| if progress_callback: | |
| progress_callback("بارگذاری مدل طبقهبندی...", 0.5) | |
| self.models['classifier'] = pipeline( | |
| "text-classification", | |
| model="HooshvareLab/bert-fa-base-uncased-clf-persiannews", | |
| tokenizer="HooshvareLab/bert-fa-base-uncased-clf-persiannews", | |
| device=-1, | |
| return_all_scores=True, | |
| model_kwargs={"cache_dir": cache_dir} | |
| ) | |
| self.model_status['classifier'] = 'loaded' | |
| logger.info("✅ مدل طبقهبندی بارگذاری شد") | |
| else: | |
| self.model_status['classifier'] = 'memory_insufficient' | |
| except Exception as e: | |
| logger.error(f"❌ خطا در بارگذاری مدل طبقهبندی: {e}") | |
| self.model_status['classifier'] = 'failed' | |
| # فاز 3: مدل تشخیص موجودیت | |
| try: | |
| if MemoryManager.check_memory_available(500): | |
| logger.info("👤 بارگذاری مدل NER...") | |
| if progress_callback: | |
| progress_callback("بارگذاری مدل تشخیص موجودیت...", 0.8) | |
| self.models['ner'] = pipeline( | |
| "ner", | |
| model="HooshvareLab/bert-fa-base-uncased-ner", | |
| tokenizer="HooshvareLab/bert-fa-base-uncased-ner", | |
| device=-1, | |
| aggregation_strategy="simple", | |
| model_kwargs={"cache_dir": cache_dir} | |
| ) | |
| self.model_status['ner'] = 'loaded' | |
| logger.info("✅ مدل NER بارگذاری شد") | |
| else: | |
| self.model_status['ner'] = 'memory_insufficient' | |
| except Exception as e: | |
| logger.error(f"❌ خطا در بارگذاری مدل NER: {e}") | |
| self.model_status['ner'] = 'failed' | |
| if progress_callback: | |
| progress_callback("تکمیل بارگذاری مدلها", 1.0) | |
| # پاکسازی حافظه | |
| MemoryManager.cleanup_memory() | |
| loaded_count = sum(1 for status in self.model_status.values() if status == 'loaded') | |
| logger.info(f"🎯 {loaded_count} مدل با موفقیت بارگذاری شد") | |
| return self.models | |
| def get_model_status(self) -> str: | |
| """دریافت وضعیت مدلها""" | |
| status_lines = [] | |
| status_icons = { | |
| 'loaded': '✅', | |
| 'failed': '❌', | |
| 'memory_insufficient': '⚠️', | |
| 'not_loaded': '⏳' | |
| } | |
| for model_name, status in self.model_status.items(): | |
| icon = status_icons.get(status, '❓') | |
| status_persian = { | |
| 'loaded': 'بارگذاری شده', | |
| 'failed': 'خطا در بارگذاری', | |
| 'memory_insufficient': 'حافظه ناکافی', | |
| 'not_loaded': 'بارگذاری نشده' | |
| }.get(status, 'نامشخص') | |
| status_lines.append(f"{icon} {model_name}: {status_persian}") | |
| memory_usage = MemoryManager.get_memory_usage() | |
| status_lines.append(f"\n💾 مصرف حافظه: {memory_usage:.1f} MB") | |
| return '\n'.join(status_lines) | |
| class LegalDocumentScraper: | |
| """استخراجکننده پیشرفته اسناد حقوقی""" | |
| def __init__(self, model_manager: ModelManager, text_processor: SmartTextProcessor): | |
| self.model_manager = model_manager | |
| self.text_processor = text_processor | |
| self.anti_ddos = AntiDDoSManager() | |
| self.session = self._create_session() | |
| def _create_session(self) -> requests.Session: | |
| """ایجاد session با تنظیمات بهینه""" | |
| session = requests.Session() | |
| # تنظیم adapter برای retry | |
| from requests.adapters import HTTPAdapter | |
| from urllib3.util.retry import Retry | |
| retry_strategy = Retry( | |
| total=3, | |
| backoff_factor=1, | |
| status_forcelist=[429, 500, 502, 503, 504] | |
| ) | |
| adapter = HTTPAdapter(max_retries=retry_strategy) | |
| session.mount("http://", adapter) | |
| session.mount("https://", adapter) | |
| return session | |
| def scrape_legal_source(self, url: str, progress_callback=None) -> Dict[str, Any]: | |
| """استخراج هوشمند سند حقوقی""" | |
| try: | |
| domain = urlparse(url).netloc | |
| # بررسی مجوز | |
| if not self.anti_ddos.should_allow_request(domain): | |
| raise Exception("تعداد درخواستها از حد مجاز تجاوز کرده است") | |
| if progress_callback: | |
| progress_callback(f"🔗 اتصال به {domain}...", 0.1) | |
| # اعمال تأخیر ضد DDoS | |
| delay = self.anti_ddos.get_request_delay(domain) | |
| time.sleep(delay) | |
| # درخواست اصلی | |
| headers = self.anti_ddos.get_random_headers() | |
| response = self.session.get(url, headers=headers, timeout=30, allow_redirects=True) | |
| response.raise_for_status() | |
| response.encoding = 'utf-8' | |
| if progress_callback: | |
| progress_callback("📄 تجزیه محتوای HTML...", 0.3) | |
| # تجزیه HTML | |
| soup = BeautifulSoup(response.content, 'html.parser') | |
| self._clean_soup(soup) | |
| # شناسایی منبع | |
| source_info = self._identify_legal_source(url) | |
| if progress_callback: | |
| progress_callback("🎯 استخراج محتوای هدفمند...", 0.5) | |
| # استخراج محتوا | |
| content_fragments = self._extract_content_intelligently(soup, source_info) | |
| # بازسازی متن | |
| reconstructed_text = self.text_processor.reconstruct_legal_text(content_fragments) | |
| if not reconstructed_text or len(reconstructed_text.strip()) < 50: | |
| raise Exception("محتوای کافی استخراج نشد") | |
| if progress_callback: | |
| progress_callback("🔧 تحلیل و پردازش متن...", 0.7) | |
| # تحلیلهای پیشرفته | |
| quality_assessment = self._assess_content_quality(reconstructed_text) | |
| legal_entities = self.text_processor.extract_legal_entities(reconstructed_text) | |
| long_sentences = self.text_processor.detect_long_sentences(reconstructed_text) | |
| ai_analysis = self._apply_ai_analysis(reconstructed_text) | |
| # نتیجه نهایی | |
| result = { | |
| 'url': url, | |
| 'source_info': source_info, | |
| 'title': self._extract_title(soup), | |
| 'content': reconstructed_text, | |
| 'word_count': len(reconstructed_text.split()), | |
| 'character_count': len(reconstructed_text), | |
| 'quality_assessment': quality_assessment, | |
| 'legal_entities': legal_entities, | |
| 'long_sentences': long_sentences, | |
| 'ai_analysis': ai_analysis, | |
| 'extraction_metadata': { | |
| 'method': 'advanced_extraction', | |
| 'fragments_count': len(content_fragments), | |
| 'response_size': len(response.content), | |
| 'encoding': response.encoding, | |
| 'anti_ddos_delay': delay | |
| }, | |
| 'timestamp': datetime.now().isoformat(), | |
| 'status': 'موفق' | |
| } | |
| if progress_callback: | |
| progress_callback("✅ استخراج با موفقیت تکمیل شد", 1.0) | |
| return result | |
| except requests.RequestException as e: | |
| return self._create_error_result(url, f"خطای شبکه: {str(e)}") | |
| except Exception as e: | |
| return self._create_error_result(url, f"خطای پردازش: {str(e)}") | |
| def _identify_legal_source(self, url: str) -> Dict[str, Any]: | |
| """شناسایی منبع حقوقی""" | |
| domain = urlparse(url).netloc | |
| for source_name, config in LEGAL_SOURCES_CONFIG.items(): | |
| if config['base_url'].replace('https://', '').replace('http://', '') in domain: | |
| return { | |
| 'name': source_name, | |
| 'type': 'official', | |
| 'credibility': 'high', | |
| 'config': config | |
| } | |
| # بررسی الگوهای عمومی سایتهای حقوقی | |
| legal_indicators = ['law', 'legal', 'court', 'judiciary', 'قانون', 'حقوق', 'دادگاه'] | |
| if any(indicator in domain.lower() for indicator in legal_indicators): | |
| return { | |
| 'name': 'منبع حقوقی شناخته نشده', | |
| 'type': 'legal_related', | |
| 'credibility': 'medium', | |
| 'config': {'max_depth': 1, 'delay_range': (2, 4)} | |
| } | |
| return { | |
| 'name': 'منبع عمومی', | |
| 'type': 'general', | |
| 'credibility': 'low', | |
| 'config': {'max_depth': 0, 'delay_range': (3, 6)} | |
| } | |
| def _clean_soup(self, soup: BeautifulSoup) -> None: | |
| """پاکسازی پیشرفته HTML""" | |
| # حذف عناصر غیرضروری | |
| unwanted_tags = [ | |
| 'script', 'style', 'nav', 'footer', 'header', 'aside', | |
| 'advertisement', 'ads', 'sidebar', 'menu', 'breadcrumb', | |
| 'social', 'share', 'comment', 'popup', 'modal' | |
| ] | |
| for tag in unwanted_tags: | |
| for element in soup.find_all(tag): | |
| element.decompose() | |
| # حذف عناصر با کلاسهای مشخص | |
| unwanted_classes = [ | |
| 'ad', 'ads', 'advertisement', 'sidebar', 'menu', 'nav', | |
| 'footer', 'header', 'social', 'share', 'comment', 'popup' | |
| ] | |
| for class_name in unwanted_classes: | |
| for element in soup.find_all(class_=lambda x: x and class_name in ' '.join(x).lower()): | |
| element.decompose() | |
| def _extract_content_intelligently(self, soup: BeautifulSoup, source_info: Dict) -> List[str]: | |
| """استخراج هوشمند محتوا""" | |
| fragments = [] | |
| # استراتژی 1: استفاده از تنظیمات منبع شناخته شده | |
| if source_info.get('config') and source_info['config'].get('selectors'): | |
| for selector in source_info['config']['selectors']: | |
| elements = soup.select(selector) | |
| for element in elements: | |
| text = self._extract_clean_text(element) | |
| if self._is_valid_content(text): | |
| fragments.append(text) | |
| # استراتژی 2: جستجوی محتوای اصلی | |
| if not fragments: | |
| main_selectors = [ | |
| 'main', 'article', '.main-content', '.content', '.post-content', | |
| '.article-content', '.news-content', '.law-content', '.legal-text', | |
| '#main', '#content', '#article', '.document-content' | |
| ] | |
| for selector in main_selectors: | |
| elements = soup.select(selector) | |
| for element in elements: | |
| text = self._extract_clean_text(element) | |
| if self._is_valid_content(text): | |
| fragments.append(text) | |
| if fragments: | |
| break | |
| # استراتژی 3: استخراج از پاراگرافها | |
| if not fragments: | |
| for tag in ['p', 'div', 'section', 'article']: | |
| elements = soup.find_all(tag) | |
| for element in elements: | |
| text = self._extract_clean_text(element) | |
| if self._is_valid_content(text, min_length=30): | |
| fragments.append(text) | |
| # استراتژی 4: fallback به body | |
| if not fragments: | |
| body = soup.find('body') | |
| if body: | |
| text = self._extract_clean_text(body) | |
| if text: | |
| paragraphs = [p.strip() for p in text.split('\n') if p.strip()] | |
| fragments.extend(p for p in paragraphs if self._is_valid_content(p, min_length=20)) | |
| return fragments | |
| def _extract_clean_text(self, element) -> str: | |
| """استخراج متن تمیز""" | |
| if not element: | |
| return "" | |
| # حذف عناصر تودرتو غیرضروری | |
| for unwanted in element.find_all(['script', 'style', 'nav', 'aside']): | |
| unwanted.decompose() | |
| text = element.get_text(separator=' ', strip=True) | |
| text = re.sub(r'\s+', ' ', text) | |
| return text.strip() | |
| def _is_valid_content(self, text: str, min_length: int = 50) -> bool: | |
| """بررسی اعتبار محتوا""" | |
| if not text or len(text) < min_length: | |
| return False | |
| # بررسی نسبت کاراکترهای فارسی | |
| persian_chars = sum(1 for c in text if '\u0600' <= c <= '\u06FF') | |
| persian_ratio = persian_chars / len(text) if text else 0 | |
| if persian_ratio < 0.2: | |
| return False | |
| # بررسی کلمات کلیدی حقوقی | |
| legal_keywords = ['قانون', 'ماده', 'تبصره', 'مقرره', 'آییننامه', 'دادگاه', 'حکم', 'رای'] | |
| has_legal_content = any(keyword in text for keyword in legal_keywords) | |
| return has_legal_content or persian_ratio > 0.5 | |
| def _extract_title(self, soup: BeautifulSoup) -> str: | |
| """استخراج عنوان""" | |
| title_selectors = [ | |
| 'h1', 'h2', 'title', '.page-title', '.article-title', | |
| '.post-title', '.document-title', '.news-title', '.law-title' | |
| ] | |
| for selector in title_selectors: | |
| element = soup.select_one(selector) | |
| if element: | |
| title = element.get_text(strip=True) | |
| if title and 5 < len(title) < 200: | |
| return re.sub(r'\s+', ' ', title) | |
| return "بدون عنوان" | |
| def _assess_content_quality(self, text: str) -> Dict[str, Any]: | |
| """ارزیابی کیفیت محتوا""" | |
| assessment = { | |
| 'overall_score': 0, | |
| 'factors': {}, | |
| 'issues': [], | |
| 'strengths': [] | |
| } | |
| if not text: | |
| assessment['issues'].append('متن خالی') | |
| return assessment | |
| factors = {} | |
| # طول متن | |
| word_count = len(text.split()) | |
| if word_count >= 100: | |
| factors['length'] = min(100, word_count / 10) | |
| assessment['strengths'].append('طول مناسب متن') | |
| else: | |
| factors['length'] = word_count | |
| assessment['issues'].append('متن کوتاه') | |
| # نسبت فارسی | |
| persian_chars = sum(1 for c in text if '\u0600' <= c <= '\u06FF') | |
| persian_ratio = persian_chars / len(text) if text else 0 | |
| factors['persian_content'] = persian_ratio * 100 | |
| if persian_ratio >= 0.7: | |
| assessment['strengths'].append('محتوای فارسی غنی') | |
| elif persian_ratio < 0.3: | |
| assessment['issues'].append('محتوای فارسی کم') | |
| # محتوای حقوقی | |
| legal_terms_count = 0 | |
| for category, terms in PERSIAN_LEGAL_DICTIONARY.items(): | |
| legal_terms_count += sum(1 for term in terms if term in text) | |
| factors['legal_content'] = min(100, legal_terms_count * 5) | |
| if legal_terms_count >= 10: | |
| assessment['strengths'].append('غنی از اصطلاحات حقوقی') | |
| elif legal_terms_count < 3: | |
| assessment['issues'].append('فقر اصطلاحات حقوقی') | |
| # ساختار متن | |
| structure_score = 0 | |
| if 'ماده' in text: | |
| structure_score += 20 | |
| if any(indicator in text for indicator in ['تبصره', 'بند', 'فصل']): | |
| structure_score += 15 | |
| if re.search(r'[۰-۹]+', text): | |
| structure_score += 10 | |
| factors['structure'] = structure_score | |
| if structure_score >= 30: | |
| assessment['strengths'].append('ساختار منظم حقوقی') | |
| # محاسبه امتیاز کلی | |
| assessment['factors'] = factors | |
| assessment['overall_score'] = sum(factors.values()) / len(factors) if factors else 0 | |
| assessment['overall_score'] = min(100, max(0, assessment['overall_score'])) | |
| return assessment | |
| def _apply_ai_analysis(self, text: str) -> Dict[str, Any]: | |
| """اعمال تحلیل هوش مصنوعی""" | |
| analysis = { | |
| 'classification': None, | |
| 'entities': [], | |
| 'confidence_scores': {}, | |
| 'model_performance': {} | |
| } | |
| if not text or len(text.split()) < 10: | |
| return analysis | |
| # آمادهسازی متن | |
| text_sample = ' '.join(text.split()[:300]) | |
| # طبقهبندی متن | |
| if 'classifier' in self.model_manager.models: | |
| try: | |
| start_time = time.time() | |
| classification_result = self.model_manager.models['classifier'](text_sample) | |
| if classification_result: | |
| analysis['classification'] = classification_result[:3] | |
| analysis['confidence_scores']['classification'] = classification_result[0]['score'] | |
| analysis['model_performance']['classification_time'] = time.time() - start_time | |
| except Exception as e: | |
| logger.error(f"خطا در طبقهبندی: {e}") | |
| # تشخیص موجودیت | |
| if 'ner' in self.model_manager.models: | |
| try: | |
| start_time = time.time() | |
| ner_result = self.model_manager.models['ner'](text_sample[:1000]) | |
| if ner_result: | |
| high_confidence_entities = [ | |
| entity for entity in ner_result | |
| if entity.get('score', 0) > 0.5 | |
| ] | |
| analysis['entities'] = high_confidence_entities[:15] | |
| analysis['model_performance']['ner_time'] = time.time() - start_time | |
| except Exception as e: | |
| logger.error(f"خطا در تشخیص موجودیت: {e}") | |
| return analysis | |
| def _create_error_result(self, url: str, error_message: str) -> Dict[str, Any]: | |
| """ایجاد نتیجه خطا""" | |
| return { | |
| 'url': url, | |
| 'error': error_message, | |
| 'status': 'ناموفق', | |
| 'timestamp': datetime.now().isoformat(), | |
| 'content': '', | |
| 'word_count': 0, | |
| 'quality_assessment': {'overall_score': 0, 'issues': [error_message]} | |
| } | |
| class PersianLegalScraperApp: | |
| """اپلیکیشن اصلی با رابط کاربری پیشرفته""" | |
| def __init__(self): | |
| self.text_processor = SmartTextProcessor() | |
| self.model_manager = ModelManager() | |
| self.scraper = None | |
| self.results = [] | |
| self.processing_stats = { | |
| 'total_processed': 0, | |
| 'successful': 0, | |
| 'failed': 0, | |
| 'total_words': 0 | |
| } | |
| # مقداردهی اولیه | |
| self._initialize_system() | |
| def _initialize_system(self): | |
| """مقداردهی سیستم""" | |
| try: | |
| logger.info("🚀 مقداردهی سیستم...") | |
| self.model_manager.load_models_progressively() | |
| self.scraper = LegalDocumentScraper(self.model_manager, self.text_processor) | |
| logger.info("✅ سیستم آماده است") | |
| except Exception as e: | |
| logger.error(f"❌ خطا در مقداردهی: {e}") | |
| def process_single_url(self, url: str, progress=gr.Progress()) -> Tuple[str, str, str]: | |
| """پردازش یک URL""" | |
| if not url or not url.strip(): | |
| return "❌ خطا: آدرس معتبری وارد کنید", "", "" | |
| url = url.strip() | |
| if not url.startswith(('http://', 'https://')): | |
| url = 'https://' + url | |
| try: | |
| progress(0.0, desc="شروع پردازش...") | |
| def progress_callback(message: str, value: float): | |
| progress(value, desc=message) | |
| result = self.scraper.scrape_legal_source(url, progress_callback) | |
| if result.get('status') == 'موفق': | |
| # بهروزرسانی آمار | |
| self.processing_stats['total_processed'] += 1 | |
| self.processing_stats['successful'] += 1 | |
| self.processing_stats['total_words'] += result.get('word_count', 0) | |
| # ذخیره نتیجه | |
| self.results.append(result) | |
| # تنظیم خروجیها | |
| status_text = self._format_single_result(result) | |
| analysis_text = self._format_analysis_result(result) | |
| content_text = result.get('content', '') | |
| return status_text, content_text, analysis_text | |
| else: | |
| self.processing_stats['total_processed'] += 1 | |
| self.processing_stats['failed'] += 1 | |
| error_msg = result.get('error', 'خطای نامشخص') | |
| return f"❌ خطا: {error_msg}", "", "" | |
| except Exception as e: | |
| self.processing_stats['total_processed'] += 1 | |
| self.processing_stats['failed'] += 1 | |
| logger.error(f"خطا در پردازش: {e}") | |
| return f"❌ خطای سیستمی: {str(e)}", "", "" | |
| def _format_single_result(self, result: Dict[str, Any]) -> str: | |
| """قالببندی نتیجه با آیکونهای SVG""" | |
| quality = result.get('quality_assessment', {}) | |
| source_info = result.get('source_info', {}) | |
| legal_entities = result.get('legal_entities', {}) | |
| long_sentences = result.get('long_sentences', []) | |
| lines = [ | |
| f"{SVG_ICONS['success']} **وضعیت**: {result.get('status')}", | |
| f"{SVG_ICONS['document']} **عنوان**: {result.get('title', 'بدون عنوان')}", | |
| f"🏛️ **منبع**: {source_info.get('name', 'نامشخص')} ({source_info.get('credibility', 'نامشخص')})", | |
| f"📊 **آمار محتوا**: {result.get('word_count', 0):,} کلمه، {result.get('character_count', 0):,} کاراکتر", | |
| f"🎯 **کیفیت کلی**: {quality.get('overall_score', 0):.1f}/100", | |
| f"📈 **محتوای فارسی**: {quality.get('factors', {}).get('persian_content', 0):.1f}%", | |
| f"⚖️ **محتوای حقوقی**: {quality.get('factors', {}).get('legal_content', 0):.1f}/100", | |
| f"📚 **ارجاعات حقوقی**: {len(legal_entities.get('citations', []))} مورد", | |
| f"🏷️ **اصطلاحات حقوقی**: {len(legal_entities.get('legal_terms', []))} مورد" | |
| ] | |
| # جملات طولانی | |
| if long_sentences: | |
| lines.append(f"{SVG_ICONS['warning']} **جملات طولانی**: {len(long_sentences)} مورد") | |
| # نقاط قوت | |
| strengths = quality.get('strengths', []) | |
| if strengths: | |
| lines.append(f"\n✨ **نقاط قوت**: {' | '.join(strengths)}") | |
| # مشکلات | |
| issues = quality.get('issues', []) | |
| if issues: | |
| lines.append(f"{SVG_ICONS['warning']} **نکات**: {' | '.join(issues)}") | |
| lines.append(f"🕐 **زمان**: {result.get('timestamp', '')[:19]}") | |
| return '\n'.join(lines) | |
| def _format_analysis_result(self, result: Dict[str, Any]) -> str: | |
| """قالببندی تحلیل هوش مصنوعی""" | |
| ai_analysis = result.get('ai_analysis', {}) | |
| legal_entities = result.get('legal_entities', {}) | |
| long_sentences = result.get('long_sentences', []) | |
| lines = [ | |
| f"{SVG_ICONS['analyze']} **تحلیل هوش مصنوعی**\n", | |
| f"📊 **وضعیت مدلها**: {self.model_manager.get_model_status()}\n" | |
| ] | |
| # طبقهبندی | |
| classification = ai_analysis.get('classification') | |
| if classification: | |
| lines.append("🏷️ **طبقهبندی محتوا**:") | |
| for i, item in enumerate(classification[:3], 1): | |
| label = item.get('label', 'نامشخص') | |
| score = item.get('score', 0) | |
| lines.append(f"{i}. {label}: {score:.1%}") | |
| # موجودیتهای شناسایی شده | |
| entities = ai_analysis.get('entities', []) | |
| if entities: | |
| lines.append("\n👥 **موجودیتهای شناسایی شده**:") | |
| for entity in entities[:8]: # 8 مورد اول | |
| word = entity.get('word', '') | |
| label = entity.get('entity_group', '') | |
| score = entity.get('score', 0) | |
| lines.append(f"• {word} ({label}): {score:.1%}") | |
| # ارجاعات حقوقی | |
| citations = legal_entities.get('citations', []) | |
| if citations: | |
| lines.append(f"\n📚 **ارجاعات حقوقی**: {len(citations)} مورد") | |
| unique_citations = list(set(citations))[:10] | |
| lines.append(f"نمونه: {', '.join(unique_citations)}") | |
| # قوانین شناسایی شده | |
| laws = legal_entities.get('laws', []) | |
| if laws: | |
| lines.append(f"\n⚖️ **قوانین شناسایی شده**: {len(laws)} مورد") | |
| for law in laws[:3]: | |
| lines.append(f"• {law}") | |
| # جملات طولانی | |
| if long_sentences: | |
| lines.append(f"\n{SVG_ICONS['warning']} **جملات طولانی شناسایی شده**:") | |
| for i, sentence_info in enumerate(long_sentences[:3], 1): | |
| word_count = sentence_info.get('word_count', 0) | |
| suggestions = sentence_info.get('suggestions', []) | |
| lines.append(f"{i}. {word_count} کلمه - {', '.join(suggestions[:2])}") | |
| # عملکرد مدل | |
| performance = ai_analysis.get('model_performance', {}) | |
| if performance: | |
| lines.append(f"\n⏱️ **عملکرد**: ") | |
| if 'classification_time' in performance: | |
| lines.append(f"طبقهبندی: {performance['classification_time']:.2f}s") | |
| if 'ner_time' in performance: | |
| lines.append(f"تشخیص موجودیت: {performance['ner_time']:.2f}s") | |
| return '\n'.join(lines) | |
| def process_multiple_urls(self, urls_text: str, progress=gr.Progress()) -> Tuple[str, str]: | |
| """پردازش چندین URL""" | |
| if not urls_text or not urls_text.strip(): | |
| return "❌ لطفا لیست آدرسها را وارد کنید", "" | |
| urls = [url.strip() for url in urls_text.split('\n') if url.strip()] | |
| if not urls: | |
| return "❌ آدرس معتبری یافت نشد", "" | |
| # محدودیت تعداد برای HF Spaces | |
| if len(urls) > 10: | |
| urls = urls[:10] | |
| warning_msg = f"{SVG_ICONS['warning']} به دلیل محدودیتها، تنها 10 آدرس اول پردازش میشود.\n\n" | |
| else: | |
| warning_msg = "" | |
| results = [] | |
| total_urls = len(urls) | |
| try: | |
| progress(0.0, desc=f"شروع پردازش {total_urls} آدرس...") | |
| for i, url in enumerate(urls): | |
| if not url.startswith(('http://', 'https://')): | |
| url = 'https://' + url | |
| progress_value = i / total_urls | |
| progress(progress_value, desc=f"پردازش {i+1} از {total_urls}: {url[:50]}...") | |
| def progress_callback(message: str, value: float): | |
| overall_progress = progress_value + (value * (1/total_urls)) | |
| progress(overall_progress, desc=f"{message} ({i+1}/{total_urls})") | |
| result = self.scraper.scrape_legal_source(url, progress_callback) | |
| results.append(result) | |
| # بهروزرسانی آمار | |
| self.processing_stats['total_processed'] += 1 | |
| if result.get('status') == 'موفق': | |
| self.processing_stats['successful'] += 1 | |
| self.processing_stats['total_words'] += result.get('word_count', 0) | |
| else: | |
| self.processing_stats['failed'] += 1 | |
| # پاکسازی حافظه هر 3 درخواست | |
| if (i + 1) % 3 == 0: | |
| MemoryManager.cleanup_memory() | |
| time.sleep(1) # کمی استراحت | |
| progress(1.0, desc="تکمیل پردازش") | |
| # ذخیره نتایج | |
| self.results.extend(results) | |
| # تنظیم خروجیها | |
| summary_text = warning_msg + self._format_batch_summary(results) | |
| detailed_results = self._format_batch_details(results) | |
| return summary_text, detailed_results | |
| except Exception as e: | |
| logger.error(f"خطا در پردازش دستهای: {e}") | |
| return f"❌ خطای سیستمی: {str(e)}", "" | |
| def _format_batch_summary(self, results: List[Dict[str, Any]]) -> str: | |
| """خلاصه پردازش دستهای با آیکونها""" | |
| total = len(results) | |
| successful = sum(1 for r in results if r.get('status') == 'موفق') | |
| failed = total - successful | |
| if successful > 0: | |
| total_words = sum(r.get('word_count', 0) for r in results if r.get('status') == 'موفق') | |
| avg_quality = sum(r.get('quality_assessment', {}).get('overall_score', 0) | |
| for r in results if r.get('status') == 'موفق') / successful | |
| else: | |
| total_words = 0 | |
| avg_quality = 0 | |
| lines = [ | |
| f"{SVG_ICONS['analyze']} **خلاصه پردازش دستهای**", | |
| f"📈 **کل آدرسها**: {total}", | |
| f"{SVG_ICONS['success']} **موفق**: {successful} ({successful/total*100:.1f}%)", | |
| f"{SVG_ICONS['error']} **ناموفق**: {failed} ({failed/total*100:.1f}%)", | |
| f"📝 **کل کلمات**: {total_words:,}", | |
| f"🎯 **میانگین کیفیت**: {avg_quality:.1f}/100", | |
| f"💾 **حافظه**: {MemoryManager.get_memory_usage():.1f} MB", | |
| f"🕐 **زمان**: {datetime.now().strftime('%H:%M:%S')}" | |
| ] | |
| return '\n'.join(lines) | |
| def _format_batch_details(self, results: List[Dict[str, Any]]) -> str: | |
| """جزئیات نتایج دستهای""" | |
| lines = [] | |
| for i, result in enumerate(results, 1): | |
| url = result.get('url', '') | |
| status = result.get('status', '') | |
| if status == 'موفق': | |
| title = result.get('title', 'بدون عنوان') | |
| word_count = result.get('word_count', 0) | |
| quality = result.get('quality_assessment', {}).get('overall_score', 0) | |
| source = result.get('source_info', {}).get('name', 'نامشخص') | |
| lines.extend([ | |
| f"\n**{i}. {SVG_ICONS['success']} {title}**", | |
| f"{SVG_ICONS['link']} {url[:70]}{'...' if len(url) > 70 else ''}", | |
| f"🏛️ منبع: {source}", | |
| f"📊 {word_count:,} کلمه | کیفیت: {quality:.1f}/100" | |
| ]) | |
| else: | |
| error = result.get('error', 'خطای نامشخص') | |
| lines.extend([ | |
| f"\n**{i}. {SVG_ICONS['error']} ناموفق**", | |
| f"{SVG_ICONS['link']} {url[:70]}{'...' if len(url) > 70 else ''}", | |
| f"❗ {error[:80]}{'...' if len(error) > 80 else ''}" | |
| ]) | |
| return '\n'.join(lines) | |
| def export_results(self) -> Tuple[str, Optional[str]]: | |
| """صادرات نتایج""" | |
| if not self.results: | |
| return f"{SVG_ICONS['error']} نتیجهای برای صادرات وجود ندارد", None | |
| try: | |
| successful_results = [r for r in self.results if r.get('status') == 'موفق'] | |
| if not successful_results: | |
| return f"{SVG_ICONS['error']} نتیجه موفقی برای صادرات وجود ندارد", None | |
| export_data = [] | |
| for result in successful_results: | |
| quality = result.get('quality_assessment', {}) | |
| source_info = result.get('source_info', {}) | |
| ai_analysis = result.get('ai_analysis', {}) | |
| # استخراج اطلاعات طبقهبندی | |
| classification = ai_analysis.get('classification', []) | |
| top_class = classification[0].get('label', '') if classification else '' | |
| export_data.append({ | |
| 'آدرس': result.get('url', ''), | |
| 'عنوان': result.get('title', ''), | |
| 'منبع': source_info.get('name', ''), | |
| 'اعتبار منبع': source_info.get('credibility', ''), | |
| 'تعداد کلمات': result.get('word_count', 0), | |
| 'کیفیت کلی': round(quality.get('overall_score', 0), 1), | |
| 'محتوای فارسی (%)': round(quality.get('factors', {}).get('persian_content', 0), 1), | |
| 'محتوای حقوقی': round(quality.get('factors', {}).get('legal_content', 0), 1), | |
| 'طبقهبندی AI': top_class, | |
| 'زمان استخراج': result.get('timestamp', ''), | |
| 'محتوا': result.get('content', '')[:2000] + '...' if len(result.get('content', '')) > 2000 else result.get('content', '') | |
| }) | |
| df = pd.DataFrame(export_data) | |
| # ذخیره فایل | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| csv_path = f"/tmp/legal_scraping_results_{timestamp}.csv" | |
| df.to_csv(csv_path, index=False, encoding='utf-8-sig') | |
| summary = f"{SVG_ICONS['export']} {len(export_data)} سند با موفقیت صادر شد" | |
| return summary, csv_path | |
| except Exception as e: | |
| logger.error(f"خطا در صادرات: {e}") | |
| return f"{SVG_ICONS['error']} خطا در صادرات: {str(e)}", None | |
| def get_system_status(self) -> str: | |
| """وضعیت سیستم""" | |
| model_status = self.model_manager.get_model_status() | |
| memory_usage = MemoryManager.get_memory_usage() | |
| lines = [ | |
| f"{SVG_ICONS['settings']} **وضعیت سیستم**\n", | |
| f"💾 **حافظه**: {memory_usage:.1f} MB", | |
| f"📊 **آمار کلی**:", | |
| f" • کل پردازش شده: {self.processing_stats['total_processed']}", | |
| f" • موفق: {self.processing_stats['successful']}", | |
| f" • ناموفق: {self.processing_stats['failed']}", | |
| f" • کل کلمات: {self.processing_stats['total_words']:,}", | |
| f"\n🤖 **مدلها**:", | |
| model_status, | |
| f"\n⏰ **آخرین بروزرسانی**: {datetime.now().strftime('%Y/%m/%d %H:%M:%S')}" | |
| ] | |
| return '\n'.join(lines) | |
| def clear_results(self) -> str: | |
| """پاکسازی نتایج و حافظه""" | |
| self.results.clear() | |
| self.processing_stats = { | |
| 'total_processed': 0, | |
| 'successful': 0, | |
| 'failed': 0, | |
| 'total_words': 0 | |
| } | |
| MemoryManager.cleanup_memory() | |
| return f"{SVG_ICONS['success']} نتایج و حافظه پاکسازی شد" | |
| def create_interface(self): | |
| """ایجاد رابط کاربری Gradio""" | |
| # CSS سفارشی برای RTL و فونت فارسی | |
| custom_css = """ | |
| .rtl { | |
| direction: rtl; | |
| text-align: right; | |
| font-family: 'Vazirmatn', 'Tahoma', sans-serif; | |
| } | |
| .persian-title { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| font-family: 'Vazirmatn', 'Tahoma', sans-serif; | |
| } | |
| .status-box { | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 15px; | |
| background-color: #f9f9f9; | |
| direction: rtl; | |
| font-family: 'Vazirmatn', 'Tahoma', sans-serif; | |
| } | |
| .gradio-container { | |
| font-family: 'Vazirmatn', 'Tahoma', sans-serif !important; | |
| } | |
| """ | |
| with gr.Blocks( | |
| title="سیستم پیشرفته استخراج و تحلیل اسناد حقوقی فارسی", | |
| css=custom_css, | |
| theme=gr.themes.Soft() | |
| ) as interface: | |
| # عنوان اصلی | |
| gr.HTML(f""" | |
| <div class="persian-title"> | |
| <h1>{SVG_ICONS['document']} سیستم پیشرفته استخراج و تحلیل اسناد حقوقی فارسی</h1> | |
| <p>{SVG_ICONS['analyze']} مجهز به مدلهای BERT فارسی | 📊 تحلیل هوشمند محتوا | ⚡ بهینهسازی شده برای Hugging Face Spaces</p> | |
| <p>🎯 منابع معتبر: مجلس، قوه قضاییه، وزارت دادگستری، دیوان عدالت اداری</p> | |
| </div> | |
| """) | |
| with gr.Tabs(): | |
| # تب پردازش تک URL | |
| with gr.Tab(f"{SVG_ICONS['search']} پردازش تک آدرس"): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| single_url = gr.Textbox( | |
| label=f"{SVG_ICONS['link']} آدرس سند حقوقی", | |
| placeholder="https://rc.majlis.ir/fa/law/show/12345", | |
| lines=2, | |
| elem_classes=["rtl"] | |
| ) | |
| with gr.Row(): | |
| single_btn = gr.Button( | |
| f"{SVG_ICONS['analyze']} شروع استخراج و تحلیل", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| clear_single_btn = gr.Button( | |
| "🧹 پاک کردن", | |
| variant="secondary" | |
| ) | |
| with gr.Column(scale=1): | |
| system_status = gr.Textbox( | |
| label=f"{SVG_ICONS['settings']} وضعیت سیستم", | |
| interactive=False, | |
| lines=12, | |
| elem_classes=["rtl", "status-box"] | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| single_status = gr.Textbox( | |
| label=f"{SVG_ICONS['analyze']} خلاصه نتایج", | |
| interactive=False, | |
| lines=12, | |
| elem_classes=["rtl"] | |
| ) | |
| with gr.Column(): | |
| single_analysis = gr.Textbox( | |
| label=f"{SVG_ICONS['analyze']} تحلیل هوش مصنوعی", | |
| interactive=False, | |
| lines=12, | |
| elem_classes=["rtl"] | |
| ) | |
| single_content = gr.Textbox( | |
| label=f"{SVG_ICONS['document']} محتوای استخراج شده", | |
| interactive=False, | |
| lines=15, | |
| elem_classes=["rtl"] | |
| ) | |
| # تب پردازش چندتایی | |
| with gr.Tab(f"{SVG_ICONS['document']} پردازش دستهای"): | |
| gr.Markdown(""" | |
| ### 📝 راهنمای استفاده: | |
| - هر آدرس را در خط جداگانهای قرار دهید | |
| - حداکثر 10 آدرس به دلیل محدودیتهای سیستم | |
| - از منابع معتبر حقوقی استفاده کنید | |
| """, elem_classes=["rtl"]) | |
| multi_urls = gr.Textbox( | |
| label=f"{SVG_ICONS['document']} فهرست آدرسها (هر آدرس در خط جداگانه)", | |
| placeholder="""https://rc.majlis.ir/fa/law/show/12345 | |
| https://www.judiciary.ir/fa/news/67890 | |
| https://www.dotic.ir/portal/law/54321""", | |
| lines=8, | |
| elem_classes=["rtl"] | |
| ) | |
| with gr.Row(): | |
| multi_btn = gr.Button( | |
| f"{SVG_ICONS['analyze']} شروع پردازش دستهای", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| clear_multi_btn = gr.Button( | |
| "🧹 پاک کردن", | |
| variant="secondary" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| batch_summary = gr.Textbox( | |
| label=f"{SVG_ICONS['analyze']} خلاصه پردازش", | |
| interactive=False, | |
| lines=10, | |
| elem_classes=["rtl"] | |
| ) | |
| with gr.Column(): | |
| batch_details = gr.Textbox( | |
| label=f"{SVG_ICONS['document']} جزئیات نتایج", | |
| interactive=False, | |
| lines=10, | |
| elem_classes=["rtl"] | |
| ) | |
| # تب صادرات و مدیریت | |
| with gr.Tab(f"{SVG_ICONS['export']} مدیریت و صادرات"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown(f"### {SVG_ICONS['export']} صادرات نتایج", elem_classes=["rtl"]) | |
| export_btn = gr.Button( | |
| f"{SVG_ICONS['export']} صادرات به CSV", | |
| variant="primary" | |
| ) | |
| export_status = gr.Textbox( | |
| label="وضعیت صادرات", | |
| interactive=False, | |
| lines=3, | |
| elem_classes=["rtl"] | |
| ) | |
| export_file = gr.File( | |
| label="📁 فایل صادر شده", | |
| interactive=False | |
| ) | |
| with gr.Column(): | |
| gr.Markdown(f"### {SVG_ICONS['settings']} مدیریت سیستم", elem_classes=["rtl"]) | |
| with gr.Row(): | |
| refresh_btn = gr.Button( | |
| "🔄 بروزرسانی وضعیت", | |
| variant="secondary" | |
| ) | |
| cleanup_btn = gr.Button( | |
| "🧹 پاکسازی حافظه", | |
| variant="secondary" | |
| ) | |
| clear_results_btn = gr.Button( | |
| "🗑️ پاک کردن تمام نتایج", | |
| variant="stop" | |
| ) | |
| management_status = gr.Textbox( | |
| label="وضعیت عملیات", | |
| interactive=False, | |
| lines=5, | |
| elem_classes=["rtl"] | |
| ) | |
| # اتصال event handlerها | |
| # تک URL | |
| single_btn.click( | |
| fn=self.process_single_url, | |
| inputs=[single_url], | |
| outputs=[single_status, single_content, single_analysis], | |
| show_progress=True | |
| ) | |
| clear_single_btn.click( | |
| lambda: ("", "", "", ""), | |
| outputs=[single_url, single_status, single_content, single_analysis] | |
| ) | |
| # چندتایی | |
| multi_btn.click( | |
| fn=self.process_multiple_urls, | |
| inputs=[multi_urls], | |
| outputs=[batch_summary, batch_details], | |
| show_progress=True | |
| ) | |
| clear_multi_btn.click( | |
| lambda: ("", "", ""), | |
| outputs=[multi_urls, batch_summary, batch_details] | |
| ) | |
| # صادرات | |
| export_btn.click( | |
| fn=self.export_results, | |
| outputs=[export_status, export_file] | |
| ) | |
| # مدیریت | |
| refresh_btn.click( | |
| fn=self.get_system_status, | |
| outputs=[system_status] | |
| ) | |
| cleanup_btn.click( | |
| fn=MemoryManager.cleanup_memory, | |
| outputs=[management_status] | |
| ).then( | |
| lambda: "✅ حافظه پاکسازی شد", | |
| outputs=[management_status] | |
| ) | |
| clear_results_btn.click( | |
| fn=self.clear_results, | |
| outputs=[management_status] | |
| ) | |
| # بارگذاری اولیه وضعیت سیستم | |
| interface.load( | |
| fn=self.get_system_status, | |
| outputs=[system_status] | |
| ) | |
| return interface | |
| def main(): | |
| """تابع اصلی برای اجرای برنامه""" | |
| logger.info("🚀 راهاندازی سیستم استخراج اسناد حقوقی فارسی...") | |
| try: | |
| # ایجاد نمونه برنامه | |
| app = PersianLegalScraperApp() | |
| # ایجاد و راهاندازی رابط | |
| interface = app.create_interface() | |
| # راهاندازی با پیکربندی Hugging Face Spaces | |
| interface.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| show_error=True, | |
| show_tips=True, | |
| enable_queue=True, | |
| max_threads=2 | |
| ) | |
| except Exception as e: | |
| logger.error(f"خطا در راهاندازی برنامه: {e}") | |
| raise | |
| if __name__ == "__main__": | |
| main() |