search18 / src /streamlit_app.py
shDima1's picture
Update src/streamlit_app.py
7ff573a verified
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
# --- Конфигурация страницы Streamlit ---
# Устанавливаем заголовок, иконку и макет страницы.
# 'wide' макет использует всю ширину экрана.
st.set_page_config(
page_title="Meta-Поисковик",
page_icon="🔎",
layout="wide",
initial_sidebar_state="auto"
)
# --- Кастомный CSS для красивого оформления ---
# Внедряем CSS для стилизации карточек результатов, чтобы они выглядели современно.
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)
# Применяем CSS стили
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:
# Библиотека googlesearch является генератором, поэтому мы его итерируем
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:
# max_results контролирует количество получаемых результатов
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:
# Используем `st.spinner` для отображения индикатора загрузки во время поиска
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("Введите поисковый запрос в поле выше и нажмите 'Найти!', чтобы начать.")