""" تطبيق Gradio يبحث في DuckDuckGo مباشرة (سحب HTML) بدون أي مفتاح API مصمم ليعمل كواجهة + API في نفس الوقت على Hugging Face Spaces """ import requests from urllib.parse import unquote, urlparse, parse_qs from bs4 import BeautifulSoup import gradio as gr DDG_URL = "https://html.duckduckgo.com/html/" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "ar,en-US;q=0.9,en;q=0.8", "Referer": "https://html.duckduckgo.com/", "Origin": "https://html.duckduckgo.com", "Content-Type": "application/x-www-form-urlencoded", } def clean_ddg_link(raw_href: str) -> str: """يطلع الرابط الحقيقي من رابط DuckDuckGo الوسيط""" if not raw_href: return "" if raw_href.startswith("//"): raw_href = "https:" + raw_href parsed = urlparse(raw_href) qs = parse_qs(parsed.query) if "uddg" in qs: return unquote(qs["uddg"][0]) return raw_href def search_duckduckgo(query: str): """ يبحث في DuckDuckGo ويرجع قائمة نتائج كـ list of dicts تُستخدم من الواجهة ومن الـ API معاً """ if not query or not query.strip(): return {"query": query, "results": [], "error": "كلمة البحث فارغة"} last_error = None response = None for attempt in range(3): try: response = requests.post( DDG_URL, data={"q": query}, headers=HEADERS, timeout=12, ) response.raise_for_status() last_error = None break except requests.exceptions.RequestException as e: last_error = e continue if last_error is not None or response is None: return { "query": query, "results": [], "error": f"فشل الاتصال بمحرك البحث بعد عدة محاولات: {str(last_error)}", } soup = BeautifulSoup(response.text, "html.parser") results = [] for result_div in soup.select(".result"): title_tag = result_div.select_one(".result__title a") or result_div.select_one("a.result__a") snippet_tag = result_div.select_one(".result__snippet") if not title_tag: continue title = title_tag.get_text(strip=True) raw_link = title_tag.get("href", "") link = clean_ddg_link(raw_link) snippet = snippet_tag.get_text(strip=True) if snippet_tag else "" if not link or not title: continue domain = urlparse(link).netloc.replace("www.", "") results.append({ "title": title, "link": link, "snippet": snippet, "displayed_link": domain, }) return {"query": query, "results": results, "error": None} def format_results_html(query: str): """يبني HTML منسق لعرض النتائج داخل واجهة Gradio نفسها (للاختبار المباشر)""" data = search_duckduckgo(query) if data["error"]: return f"
⚠️ {data['error']}
" if not data["results"]: return "
ما لقينا نتائج لهالبحث.
" html = "
" for r in data["results"]: html += f"""
{r['displayed_link']}
{r['title']}
{r['snippet']}
""" html += "
" return html # ============ واجهة Gradio (للعرض المباشر داخل الـ Space) ============ with gr.Blocks(title="غوّاص - بحث DuckDuckGo") as demo: gr.Markdown("## 🤿 غوّاص — بحث مباشر عبر DuckDuckGo") with gr.Row(): query_input = gr.Textbox(label="ابحث عن أي شيء", placeholder="اكتب كلمة البحث هنا...") search_btn = gr.Button("بحث", variant="primary") output_html = gr.HTML() search_btn.click(fn=format_results_html, inputs=query_input, outputs=output_html) query_input.submit(fn=format_results_html, inputs=query_input, outputs=output_html) # ============ API endpoint يستخدمه index.html من الخارج ============ api_output = gr.JSON(visible=False) api_trigger = gr.Textbox(visible=False) api_trigger.change(fn=search_duckduckgo, inputs=api_trigger, outputs=api_output, api_name="search") if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)