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(""" """, 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"""

{res['title']}

{res['description']}

Источник: {res['source']}
""" # Распределяем результаты по колонкам 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("Введите поисковый запрос в поле выше и нажмите 'Найти!', чтобы начать.")