|
|
import streamlit as st |
|
|
import random |
|
|
from googlesearch import search |
|
|
from duckduckgo_search import DDGS |
|
|
from concurrent.futures import ThreadPoolExecutor |
|
|
from urllib.parse import urlparse |
|
|
from typing import List, Dict, Any |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Meta-Поисковик", |
|
|
page_icon="🔎", |
|
|
layout="wide", |
|
|
initial_sidebar_state="auto" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def local_css(): |
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Общий контейнер для карточки результата */ |
|
|
.result-card { |
|
|
border: 1px solid #e1e4e8; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
margin-bottom: 20px; |
|
|
background-color: #ffffff; |
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.05); |
|
|
transition: box-shadow 0.3s ease-in-out, transform 0.3s ease-in-out; |
|
|
} |
|
|
.result-card:hover { |
|
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1); |
|
|
transform: translateY(-3px); |
|
|
} |
|
|
/* Заголовок-ссылка */ |
|
|
.result-title a { |
|
|
color: #0d6efd !important; /* Синий цвет для ссылок */ |
|
|
font-size: 1.25em; |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
display: block; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
.result-title a:hover { |
|
|
text-decoration: underline; |
|
|
} |
|
|
/* URL сайта */ |
|
|
.result-link { |
|
|
color: #586069; |
|
|
font-size: 0.9em; |
|
|
margin-bottom: 10px; |
|
|
word-break: break-all; /* Перенос длинных ссылок */ |
|
|
} |
|
|
/* Описание/сниппет */ |
|
|
.result-description { |
|
|
color: #24292e; |
|
|
font-size: 1em; |
|
|
} |
|
|
/* Источник (Google/DuckDuckGo) */ |
|
|
.result-source { |
|
|
font-size: 0.8em; |
|
|
color: #6a737d; |
|
|
text-align: right; |
|
|
margin-top: 15px; |
|
|
font-weight: 500; |
|
|
} |
|
|
/* Стиль для фавиконок */ |
|
|
.favicon { |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
margin-right: 8px; |
|
|
vertical-align: middle; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
local_css() |
|
|
|
|
|
|
|
|
|
|
|
def get_favicon_url(url: str) -> str: |
|
|
""" |
|
|
Получает URL фавиконки для сайта, используя сервис Google. |
|
|
Это делает результаты визуально более привлекательными. |
|
|
""" |
|
|
try: |
|
|
domain = urlparse(url).netloc |
|
|
if domain: |
|
|
return f"https://www.google.com/s2/favicons?sz=64&domain_url={domain}" |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
return "https://i.imgur.com/t3oEtA2.png" |
|
|
|
|
|
def search_google(query: str, num_results: int) -> List[Dict[str, str]]: |
|
|
""" |
|
|
Выполняет поиск в Google и форматирует результаты. |
|
|
Использует try-except для обработки возможных ошибок сети или блокировок. |
|
|
""" |
|
|
st.write(f"🔍 Запускаю поиск в Google для '{query}'...") |
|
|
results = [] |
|
|
try: |
|
|
|
|
|
search_results = search(query, num_results=num_results, lang="ru") |
|
|
for i, url in enumerate(search_results): |
|
|
|
|
|
|
|
|
parsed_url = urlparse(url) |
|
|
title = parsed_url.path.split('/')[-1].replace('-', ' ').replace('_', ' ') or parsed_url.netloc |
|
|
results.append({ |
|
|
"title": title.capitalize(), |
|
|
"link": url, |
|
|
"description": f"Результат из Google. Для получения описания требуется дополнительный парсинг страницы.", |
|
|
"source": "Google" |
|
|
}) |
|
|
if i + 1 >= num_results: |
|
|
break |
|
|
except Exception as e: |
|
|
st.error(f"⚠️ Ошибка при поиске в Google: {e}") |
|
|
return results |
|
|
|
|
|
def search_duckduckgo(query: str, num_results: int) -> List[Dict[str, str]]: |
|
|
""" |
|
|
Выполняет поиск в DuckDuckGo и форматирует результаты. |
|
|
Эта библиотека возвращает более структурированные данные, включая описание. |
|
|
""" |
|
|
st.write(f"🦆 Запускаю поиск в DuckDuckGo для '{query}'...") |
|
|
results = [] |
|
|
try: |
|
|
with DDGS() as ddgs: |
|
|
|
|
|
search_results = ddgs.text(query, region='ru-ru', max_results=num_results) |
|
|
for r in search_results: |
|
|
results.append({ |
|
|
"title": r.get('title', ''), |
|
|
"link": r.get('href', ''), |
|
|
"description": r.get('body', ''), |
|
|
"source": "DuckDuckGo" |
|
|
}) |
|
|
except Exception as e: |
|
|
st.error(f"⚠️ Ошибка при поиске в DuckDuckGo: {e}") |
|
|
return results |
|
|
|
|
|
def display_results(results: List[Dict[str, Any]]): |
|
|
""" |
|
|
Отображает отформатированные и стилизованные результаты поиска. |
|
|
""" |
|
|
if not results: |
|
|
st.warning("Не удалось найти результаты по вашему запросу. Попробуйте изменить его.") |
|
|
return |
|
|
|
|
|
st.subheader(f"Найдено результатов: {len(results)}") |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
for i, res in enumerate(results): |
|
|
favicon = get_favicon_url(res["link"]) |
|
|
|
|
|
card_html = f""" |
|
|
<div class="result-card"> |
|
|
<h3 class="result-title"> |
|
|
<img src="{favicon}" class="favicon"> |
|
|
<a href="{res['link']}" target="_blank">{res['title']}</a> |
|
|
</h3> |
|
|
<div class="result-link">{res['link']}</div> |
|
|
<p class="result-description">{res['description']}</p> |
|
|
<div class="result-source">Источник: <strong>{res['source']}</strong></div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
if i % 2 == 0: |
|
|
with col1: |
|
|
st.markdown(card_html, unsafe_allow_html=True) |
|
|
else: |
|
|
with col2: |
|
|
st.markdown(card_html, unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.title("🔎 Meta-Поисковик") |
|
|
st.markdown("Этот поисковик объединяет и перемешивает результаты из **Google** и **DuckDuckGo**.") |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.header("⚙️ Настройки поиска") |
|
|
num_results_per_engine = st.slider( |
|
|
"Количество результатов от каждого поисковика:", |
|
|
min_value=5, |
|
|
max_value=25, |
|
|
value=10, |
|
|
step=1, |
|
|
help="Выберите, сколько ссылок запросить у Google и DuckDuckGo." |
|
|
) |
|
|
|
|
|
|
|
|
with st.form(key="search_form"): |
|
|
search_query = st.text_input( |
|
|
"Введите ваш запрос", |
|
|
placeholder="Например: лучшие фреймворки для Python", |
|
|
key="search_input" |
|
|
) |
|
|
submit_button = st.form_submit_button(label="🚀 Найти!") |
|
|
|
|
|
|
|
|
if submit_button and search_query: |
|
|
|
|
|
with st.spinner(f"Ищем '{search_query}'... Это может занять несколько секунд."): |
|
|
all_results = [] |
|
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as executor: |
|
|
|
|
|
google_future = executor.submit(search_google, search_query, num_results_per_engine) |
|
|
ddg_future = executor.submit(search_duckduckgo, search_query, num_results_per_engine) |
|
|
|
|
|
|
|
|
google_results = google_future.result() |
|
|
ddg_results = ddg_future.result() |
|
|
|
|
|
|
|
|
all_results.extend(google_results) |
|
|
all_results.extend(ddg_results) |
|
|
|
|
|
|
|
|
random.shuffle(all_results) |
|
|
|
|
|
|
|
|
st.session_state['search_results'] = all_results |
|
|
st.session_state['last_query'] = search_query |
|
|
|
|
|
|
|
|
if 'search_results' in st.session_state: |
|
|
st.markdown("---") |
|
|
display_results(st.session_state['search_results']) |
|
|
|
|
|
|
|
|
else: |
|
|
st.info("Введите поисковый запрос в поле выше и нажмите 'Найти!', чтобы начать.") |