Spaces:
Paused
Paused
import gradio as gr | |
import logging | |
import os | |
import requests | |
import time | |
from datetime import datetime | |
from typing import Dict, List, Tuple, Optional | |
from bs4 import BeautifulSoup | |
# Configure logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s', | |
handlers=[logging.StreamHandler()] | |
) | |
logger = logging.getLogger(__name__) | |
# Iranian legal sources | |
IRANIAN_LEGAL_SOURCES = [ | |
"https://rc.majlis.ir", | |
"https://dolat.ir", | |
"https://iribnews.ir" | |
] | |
class SimpleLegalDocument: | |
"""Simple document structure for demo""" | |
def __init__(self, title: str, content: str, source_url: str, | |
document_type: str = "web_page", importance_score: float = 0.5): | |
self.title = title | |
self.content = content | |
self.source_url = source_url | |
self.document_type = document_type | |
self.importance_score = importance_score | |
self.summary = content[:100] + "..." if len(content) > 100 else content | |
self.keywords = self._extract_keywords(content) | |
self.date_scraped = datetime.now().isoformat() | |
def _extract_keywords(self, text: str) -> List[str]: | |
"""Simple keyword extraction for demo""" | |
words = text.split() | |
word_freq = {} | |
for word in words: | |
if len(word) > 3 and word.isalpha(): | |
word_freq[word] = word_freq.get(word, 0) + 1 | |
return sorted(word_freq.keys(), key=lambda x: word_freq[x], reverse=True)[:5] | |
class SimpleLegalScraper: | |
"""Simple scraper for demo without heavy dependencies""" | |
def __init__(self, delay: float = 2.0): | |
self.delay = delay | |
self.last_request_time = 0 | |
self.session = requests.Session() | |
self.session.headers.update({ | |
'User-Agent': 'LegalScraperDemo/1.0 (Educational Research)', | |
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
'Accept-Language': 'fa,en;q=0.9' | |
}) | |
def _respect_delay(self): | |
"""Respect delay between requests""" | |
current_time = time.time() | |
time_since_last = current_time - self.last_request_time | |
if time_since_last < self.delay: | |
time.sleep(self.delay - time_since_last) | |
self.last_request_time = time.time() | |
def fetch_page(self, url: str) -> Optional[BeautifulSoup]: | |
"""Fetch and parse a web page""" | |
try: | |
self._respect_delay() | |
logger.info(f"Fetching: {url}") | |
response = self.session.get(url, timeout=15) | |
response.raise_for_status() | |
response.encoding = response.apparent_encoding or 'utf-8' | |
return BeautifulSoup(response.text, 'html.parser') | |
except Exception as e: | |
logger.warning(f"Failed to fetch {url}: {e}") | |
return None | |
def scrape_demo_content(self, urls: List[str], max_docs: int = 5) -> List[SimpleLegalDocument]: | |
"""Scrape demo content from websites""" | |
documents = [] | |
for url in urls[:3]: # Limit to 3 URLs for demo | |
try: | |
soup = self.fetch_page(url) | |
if not soup: | |
continue | |
# Extract title | |
title = soup.find('title') | |
title_text = title.get_text(strip=True) if title else "صفحه وب" | |
# Extract content | |
content_elem = soup.find('body') or soup | |
content = content_elem.get_text(strip=True) | |
content = content[:500] # Limit content length | |
# Determine document type based on URL | |
doc_type = "web_page" | |
if 'majlis' in url: | |
doc_type = "law" | |
elif 'news' in url: | |
doc_type = "news" | |
# Create document | |
doc = SimpleLegalDocument( | |
title=title_text, | |
content=content, | |
source_url=url, | |
document_type=doc_type, | |
importance_score=0.7 if doc_type == "law" else 0.5 | |
) | |
documents.append(doc) | |
logger.info(f"Scraped: {title_text[:50]}...") | |
if len(documents) >= max_docs: | |
break | |
except Exception as e: | |
logger.error(f"Error scraping {url}: {e}") | |
continue | |
return documents | |
class LegalScraperInterface: | |
def __init__(self): | |
self.scraper = SimpleLegalScraper(delay=2.0) | |
self.is_scraping = False | |
def scrape_real_sources(self, urls_text: str, max_docs: int) -> Tuple[str, str, str]: | |
"""Scrape websites from provided URLs""" | |
if self.is_scraping: | |
return "❌ اسکراپینگ در حال انجام است", "", "" | |
try: | |
self.is_scraping = True | |
urls = [url.strip() for url in urls_text.split('\n') if url.strip()] | |
if not urls: | |
urls = IRANIAN_LEGAL_SOURCES | |
documents = self.scraper.scrape_demo_content(urls, max_docs) | |
status = f"✅ اسکراپینگ کامل شد - {len(documents)} سند جمعآوری شد" | |
# Create summary | |
summary_lines = [ | |
f"📊 **خلاصه نتایج:**", | |
f"- تعداد اسناد: {len(documents)}", | |
f"- منابع پردازش شده: {len(urls)}", | |
f"- زمان: {datetime.now().strftime('%H:%M:%S')}", | |
"", | |
"📋 **اسناد جمعآوری شده:**" | |
] | |
for i, doc in enumerate(documents, 1): | |
summary_lines.append(f"{i}. {doc.title[:50]}...") | |
summary = "\n".join(summary_lines) | |
# Create preview | |
preview_lines = [] | |
for doc in documents[:3]: | |
preview_lines.extend([ | |
f"**{doc.title}**", | |
f"نوع: {doc.document_type}", | |
f"منبع: {doc.source_url}", | |
f"امتیاز اهمیت: {doc.importance_score:.2f}", | |
f"خلاصه: {doc.summary}", | |
f"کلیدواژهها: {', '.join(doc.keywords[:3])}", | |
"---" | |
]) | |
preview = "\n".join(preview_lines) if preview_lines else "هیچ سندی یافت نشد" | |
return status, summary, preview | |
except Exception as e: | |
error_msg = f"❌ خطا در اسکراپینگ: {str(e)}" | |
logger.error(error_msg) | |
return error_msg, "", "" | |
finally: | |
self.is_scraping = False | |
def get_database_stats(self) -> Tuple[str, str]: | |
"""Get simulated statistics""" | |
try: | |
stats_lines = [ | |
"📊 **آمار پایگاه داده:**", | |
f"- کل اسناد: 15", | |
"", | |
"📈 **بر اساس نوع:**", | |
"- قوانین: 6", | |
"- اخبار: 5", | |
"- صفحات وب: 4", | |
"", | |
"⭐ **توزيع اهمیت:**", | |
"- بالا (>0.7): 8", | |
"- متوسط (0.4-0.7): 5", | |
"- پایین (<0.4): 2", | |
"", | |
"🔑 **کلیدواژههای برتر:**", | |
"- قانون: 12", | |
"- حقوق: 9", | |
"- ایران: 7", | |
"- مجلس: 6", | |
"- اسلامی: 5" | |
] | |
stats_text = "\n".join(stats_lines) | |
# Simple HTML visualization | |
viz_html = """ | |
<div style='text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;'> | |
<h3>📊 نمودارهای تحلیلی</h3> | |
<div style='display: flex; justify-content: space-around; margin-top: 20px;'> | |
<div style='text-align: center;'> | |
<div style='width: 120px; height: 120px; background: linear-gradient(135deg, #3498db, #2c3e50); | |
border-radius: 50%; display: flex; align-items: center; justify-content: center; | |
color: white; font-weight: bold; font-size: 14px; margin: 0 auto;'> | |
انواع اسناد | |
</div> | |
<p style='margin-top: 10px;'>توزیع بر اساس نوع</p> | |
</div> | |
<div style='text-align: center;'> | |
<div style='width: 120px; height: 120px; background: linear-gradient(135deg, #e74c3c, #c0392b); | |
border-radius: 50%; display: flex; align-items: center; justify-content: center; | |
color: white; font-weight: bold; font-size: 14px; margin: 0 auto;'> | |
سطح اهمیت | |
</div> | |
<p style='margin-top: 10px;'>امتیازبندی اسناد</p> | |
</div> | |
</div> | |
</div> | |
""" | |
return stats_text, viz_html | |
except Exception as e: | |
error_msg = f"خطا در تولید آمار: {str(e)}" | |
return error_msg, "" | |
def search_documents(self, query: str) -> str: | |
"""Simulate search functionality""" | |
if not query.strip(): | |
return "لطفاً کلیدواژهای برای جستجو وارد کنید" | |
try: | |
# Sample search results | |
sample_results = [ | |
{ | |
"title": "قانون اساسی جمهوری اسلامی ایران", | |
"document_type": "law", | |
"similarity": 0.92, | |
"source": "https://rc.majlis.ir/fa/law/show/1", | |
"summary": "قانون اساسی جمهوری اسلامی ایران مصوب 1358 با اصلاحات بعدی" | |
}, | |
{ | |
"title": "آییننامه داخلی مجلس شورای اسلامی", | |
"document_type": "regulation", | |
"similarity": 0.85, | |
"source": "https://rc.majlis.ir/fa/regulation/show/1", | |
"summary": "مقررات مربوط به نحوه تشکیل و اداره جلسات مجلس شورای اسلامی" | |
} | |
] | |
result_lines = [f"🔍 **نتایج جستجو برای '{query}':**\n"] | |
for i, result in enumerate(sample_results, 1): | |
result_lines.extend([ | |
f"**{i}. {result['title']}**", | |
f" نوع: {result['document_type']}", | |
f" امتیاز شباهت: {result['similarity']:.2f}", | |
f" منبع: {result['source']}", | |
f" خلاصه: {result['summary']}", | |
"---" | |
]) | |
return "\n".join(result_lines) | |
except Exception as e: | |
error_msg = f"خطا در جستجو: {str(e)}" | |
return error_msg | |
def export_data(self) -> Tuple[str, str]: | |
"""Create sample export file""" | |
try: | |
import csv | |
# Create data directory if not exists | |
os.makedirs('/app/data', exist_ok=True) | |
filename = f"/app/data/legal_documents_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" | |
# Sample data | |
sample_data = [ | |
['title', 'type', 'score', 'source', 'keywords'], | |
['قانون اساسی جمهوری اسلامی ایران', 'law', '0.95', 'https://rc.majlis.ir/fa/law/show/1', 'قانون,اساسی,جمهوری'], | |
['آییننامه داخلی مجلس', 'regulation', '0.85', 'https://rc.majlis.ir/fa/regulation/show/1', 'آییننامه,مجلس,داخلی'], | |
['اخبار حقوقی روز', 'news', '0.65', 'https://iribnews.ir/news/456', 'اخبار,حقوقی,روز'], | |
['تحلیل قراردادهای بینالمللی', 'analysis', '0.75', 'https://dolat.ir/analysis/789', 'تحلیل,قرارداد,بینالمللی'] | |
] | |
with open(filename, 'w', newline='', encoding='utf-8') as csvfile: | |
writer = csv.writer(csvfile) | |
writer.writerows(sample_data) | |
return f"✅ فایل نمونه با موفقیت تولید شد: {filename}", filename | |
except Exception as e: | |
error_msg = f"❌ خطا در تولید فایل: {str(e)}" | |
return error_msg, "" | |
def create_interface(): | |
interface = LegalScraperInterface() | |
css = """ | |
.gradio-container { | |
max-width: 1200px !important; | |
margin: auto; | |
font-family: 'Tahoma', sans-serif; | |
} | |
.header { | |
background: linear-gradient(135deg, #2c3e50, #3498db); | |
color: white; | |
padding: 20px; | |
border-radius: 10px; | |
text-align: center; | |
margin-bottom: 20px; | |
} | |
.tab-content { | |
padding: 20px; | |
background: #f8f9fa; | |
border-radius: 10px; | |
margin-bottom: 20px; | |
} | |
""" | |
with gr.Blocks(css=css, title="اسکراپر اسناد حقوقی", theme=gr.themes.Soft()) as app: | |
gr.HTML(""" | |
<div class="header"> | |
<h1>🕷️ اسکراپر اسناد حقوقی</h1> | |
<p>سیستم جمعآوری و تحلیل اسناد حقوقی از منابع وب</p> | |
</div> | |
""") | |
with gr.Tab("🕷️ اسکراپینگ"): | |
gr.Markdown("## جمعآوری اسناد از منابع وب") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
urls_input = gr.Textbox( | |
label="📝 URL های منابع", | |
placeholder="هر URL را در یک خط وارد کنید:", | |
lines=5, | |
value="https://rc.majlis.ir\nhttps://dolat.ir\nhttps://iribnews.ir" | |
) | |
max_docs = gr.Slider( | |
label="حداکثر اسناد", | |
minimum=1, | |
maximum=10, | |
value=5, | |
step=1 | |
) | |
scrape_btn = gr.Button("🚀 شروع اسکراپینگ", variant="primary") | |
with gr.Column(scale=1): | |
status_output = gr.Textbox( | |
label="⚡ وضعیت", | |
interactive=False, | |
lines=3 | |
) | |
with gr.Row(): | |
summary_output = gr.Textbox( | |
label="📊 خلاصه نتایج", | |
interactive=False, | |
lines=8 | |
) | |
preview_output = gr.Textbox( | |
label="👁️ پیشنمایش اسناد", | |
interactive=False, | |
lines=8 | |
) | |
scrape_btn.click( | |
fn=interface.scrape_real_sources, | |
inputs=[urls_input, max_docs], | |
outputs=[status_output, summary_output, preview_output] | |
) | |
with gr.Tab("🔍 جستجو"): | |
gr.Markdown("## جستجو در اسناد جمعآوری شده") | |
search_input = gr.Textbox( | |
label="🔍 کلیدواژه جستجو", | |
placeholder="مثال: قانون اساسی, آییننامه, حقوق" | |
) | |
search_btn = gr.Button("🔍 جستجو", variant="primary") | |
search_results = gr.Textbox( | |
label="📋 نتایج جستجو", | |
interactive=False, | |
lines=15 | |
) | |
search_btn.click( | |
fn=interface.search_documents, | |
inputs=[search_input], | |
outputs=[search_results] | |
) | |
with gr.Tab("📊 آمار و تحلیل"): | |
gr.Markdown("## آمار و تحلیل دادهها") | |
stats_btn = gr.Button("📊 بروزرسانی آمار", variant="secondary") | |
with gr.Row(): | |
stats_text = gr.Textbox( | |
label="📈 آمار متنی", | |
interactive=False, | |
lines=15 | |
) | |
stats_plot = gr.HTML( | |
label="📊 نمودارهای تحلیلی" | |
) | |
stats_btn.click( | |
fn=interface.get_database_stats, | |
outputs=[stats_text, stats_plot] | |
) | |
with gr.Tab("💾 خروجی دادهها"): | |
gr.Markdown("## ذخیرهسازی و خروجی") | |
export_btn = gr.Button("💾 تولید فایل خروجی", variant="primary") | |
export_status = gr.Textbox( | |
label="📝 وضعیت", | |
interactive=False, | |
lines=2 | |
) | |
export_file = gr.File( | |
label="📁 دانلود فایل", | |
visible=False | |
) | |
export_btn.click( | |
fn=interface.export_data, | |
outputs=[export_status, export_file] | |
) | |
with gr.Tab("📚 راهنما"): | |
gr.Markdown(""" | |
# 🕷️ راهنمای اسکراپر اسناد حقوقی | |
## ویژگیها | |
- جمعآوری اسناد از منابع وب | |
- پردازش و تحلیل محتوا | |
- جستجوی پیشرفته | |
- آمار و گزارشهای تحلیلی | |
- خروجی در قالب CSV | |
## نحوه استفاده | |
1. در تب "اسکراپینگ" URL منابع را وارد کنید | |
2. دکمه "شروع اسکراپینگ" را بزنید | |
3. از تب "جستجو" برای یافتن اسناد استفاده کنید | |
4. آمار و تحلیلها را مشاهده کنید | |
5. فایلهای خروجی را دانلود کنید | |
## منابع پیشنهادی | |
- مجلس شورای اسلامی (rc.majlis.ir) | |
- پورتال دولت (dolat.ir) | |
- خبرگزاریهای معتبر | |
⚠️ **تذکر**: این ابزار برای مقاصد آموزشی و پژوهشی ارائه شده است. | |
""") | |
return app | |
def main(): | |
"""Main entry point""" | |
print("🚀 راه اندازی اسکراپر اسناد حقوقی...") | |
# Create required directories | |
os.makedirs("/app/data", exist_ok=True) | |
# Create and launch interface | |
interface = create_interface() | |
interface.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
show_error=True, | |
debug=False | |
) | |
if __name__ == "__main__": | |
main() |