Как рекурсивно собрать названия школ и адреса сайтов

    Задача сбора данных из иерархической структуры веб-страниц - типичная головоломка для веб-скрапинга. Допустим, у вас есть каталог школ, где навигация идёт от региона к городу, затем к району и только потом к списку школ с адресами их сайтов. Вручную перебирать все уровни вложенности неэффективно. В этой статье мы разберём рекурсивный подход на Python с использованием библиотек requests и BeautifulSoup, который избавит от кучи вложенных циклов и позволит гибко обрабатывать любую глубину иерархии.

    Понимание структуры данных

    Прежде чем писать код, нужно чётко определить типы страниц. В вашем случае есть два вида:

    Конечная цель - добраться до целевых страниц и извлечь данные. Иерархия может быть разной длины: от двух уровней (город -> школы) до четырёх (регион -> область -> район -> школы).

    Рекурсивный алгоритм обхода

    Рекурсия идеально подходит для таких задач, так как глубина иерархии заранее неизвестна. Основная идея: функция проверяет, является ли текущая страница целевой (содержит таблицу школ). Если да - парсит данные. Если нет - собирает все ссылки со страницы и рекурсивно вызывает себя для каждой из них.

    import requests
    from bs4 import BeautifulSoup
    
    def parse_schools(url, visited=None):
        if visited is None:
            visited = set()
        if url in visited:
            return []
        visited.add(url)
    
        response = requests.get(url)
        soup = BeautifulSoup(response.text, 'html.parser')
    
        # Проверяем, есть ли на странице таблица со школами
        table = soup.find('table')
        if table and table.find_all('a'):
            # Извлекаем названия школ и ссылки из таблицы
            schools = []
            for row in table.find_all('tr'):
                cells = row.find_all('td')
                if len(cells) >= 2:
                    name = cells[0].get_text(strip=True)
                    link = cells[1].find('a')
                    if link and link.get('href'):
                        schools.append({
                            'name': name,
                            'url': link['href']
                        })
            return schools
        else:
            # Иначе собираем все ссылки-навигаторы (внутри div)
            links = soup.select('div a[href]')
            all_schools = []
            for link in links:
                href = link['href']
                if not href.startswith('http'):
                    href = url.rstrip('/') + '/' + href.lstrip('/')
                all_schools.extend(parse_schools(href, visited))
            return all_schools

    Ключевые моменты реализации

    Избежание повторного посещения

    Используйте множество visited, чтобы не попасть в бесконечный цикл при наличии перекрёстных ссылок. Каждый новый URL проверяется перед обработкой.

    Обработка относительных ссылок

    Ссылки на страницах часто бывают относительными. Функция должна превращать их в абсолютные, иначе рекурсия сломается. Пример: href = url.rstrip('/') + '/' + href.lstrip('/').

    Гибкость селекторов

    В примере используется soup.select('div a[href]') для навигационных страниц. Если структура отличается, адаптируйте селектор под конкретный сайт. Для целевых страниц - поиск таблицы с ссылками.

    Оптимизация и обработка ошибок

    В реальном проекте добавьте задержки между запросами (time.sleep), чтобы не перегружать сервер. Также обрабатывайте исключения (например, ошибки соединения или неверный статус ответа). Для больших объёмов данных рассмотрите использование асинхронных запросов или очередей.

    Заключение

    Рекурсивный парсинг избавляет от необходимости писать вложенные циклы под каждый уровень иерархии. Главное - правильно определить условие остановки (целевая страница) и обрабатывать ссылки. Этот подход универсален и легко адаптируется под любую структуру каталога школ или аналогичных справочников.

    Часто задаваемые вопросы