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