File size: 11,259 Bytes
7ff573a
c57232a
7ff573a
 
 
c57232a
7ff573a
c57232a
7ff573a
 
 
c57232a
7ff573a
23e4ec0
c57232a
7ff573a
c57232a
 
7ff573a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23e4ec0
7ff573a
 
 
 
 
 
 
 
 
 
 
c57232a
7ff573a
 
 
 
 
 
 
23e4ec0
7ff573a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23e4ec0
7ff573a
 
c57232a
7ff573a
 
 
 
 
 
 
c57232a
23e4ec0
7ff573a
 
 
 
 
 
 
 
 
c57232a
7ff573a
 
23e4ec0
7ff573a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23e4ec0
7ff573a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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("Введите поисковый запрос в поле выше и нажмите 'Найти!', чтобы начать.")