Остановка скрипта без ошибок

У меня есть скрипт, который рекурсивно собирает ссылки с сайта с некоторыми ограничениями.
В определенный момент скрипт завершает свое выполнение на этой функции. Причем в абсолютно разное время и на разных ссылках

def get_tree_free(link):
    """Получаем html дерево"""
    logging.info(f"Ссылка - {link}")
    logging.info("Начало выполнения get_tree_free")
    r = requests.get(link, headers={'User-Agent': UserAgent().chrome})
    logging.info(f"статус код - {r.status_code}")
    if r.status_code == 200:
        logging.info("Возвращаем нормальное дерево")
        return BeautifulSoup(r.content, 'html.parser')
    else:
        logging.info("Статус код НЕ 200")
    ...

Пытался отловить хотя бы какие-то исключения используя try-except и библиотеку loguru, но никаких ошибок нет. Просто программа прекращает действие.
Оперативная память не забивается, все ссылки вставляются в БД и берутся тоже оттуда. Также проверял этот пункт с помощью memory_profiler, он не показывает увеличение использованной оперативки.

Прочитал все возможные вопросы на форумах, описывающие похожую проблему, но ничего так и не помогло.

Единственное предположение - в requests есть какой-то ограничитель или баг, который не дает дальше выполнять скрипт, но, повторю, что никаких ошибок нет.

Конец лог-файла, в который я записывал информацию, используя logging:

INFO:root:ТУТ НАЧАЛОСЬ ВЫПОЛНЕНИЕ ПАРСИНГА НОВОЙ СТРАНИЦЫ
INFO:root:Выполняется функция get_tree_urls
INFO:root:Выполняется функция check_key_words и append_urls
INFO:root:Запуск get_next_url
INFO:root:Зашло в get_next_url
INFO:root:1603809398.2826648, время
INFO:root:Выполнение условия
INFO:root:Получание нового элемента из стека
INFO:root:Получение tree
INFO:root:Начало выполнения get_tree
INFO:root:Ссылка - https://www.tesli.com/catalog/eui/rozetki-i-vyklyuchateli/rozetki/?display_type=list&sort=article&%09%09%09PAGEN_1=2&PAGEN_1=3
INFO:root:Начало выполнения get_tree_free
INFO:root:статус код - 200
INFO:root:Возвращаем нормальное дерево
INFO:root:ТУТ НАЧАЛОСЬ ВЫПОЛНЕНИЕ ПАРСИНГА НОВОЙ СТРАНИЦЫ
INFO:root:Выполняется функция get_tree_urls
INFO:root:Выполняется функция check_key_words и append_urls
INFO:root:Запуск get_next_url
INFO:root:Зашло в get_next_url
INFO:root:1603809403.8433385, время
INFO:root:Выполнение условия
INFO:root:Получание нового элемента из стека
INFO:root:Получение tree
INFO:root:Начало выполнения get_tree
INFO:root:Ссылка - https://www.tesli.com/catalog/eui/rozetki-i-vyklyuchateli/rozetki/?sort=article&display_type=tile&%09%09%09PAGEN_1=2&            PAGEN_1=4
INFO:root:Начало выполнения get_tree_free
INFO:root:статус код - 200
INFO:root:Возвращаем нормальное дерево
INFO:root:ТУТ НАЧАЛОСЬ ВЫПОЛНЕНИЕ ПАРСИНГА НОВОЙ СТРАНИЦЫ
INFO:root:Выполняется функция get_tree_urls
INFO:root:Выполняется функция check_key_words и append_urls
INFO:root:Запуск get_next_url
INFO:root:Зашло в get_next_url
INFO:root:1603809409.6905885, время
INFO:root:Выполнение условия
INFO:root:Получание нового элемента из стека
INFO:root:Получение tree
INFO:root:Начало выполнения get_tree
INFO:root:Ссылка - https://www.tesli.com/catalog/eui/rozetki-i-vyklyuchateli/rozetki/?display_type=tile&sort=article&%09%09%09PAGEN_1=2&PAGEN_1=4
INFO:root:Начало выполнения get_tree_free

Если у кого-то была такая проблема, пожалуйста, отзовитесь!

Обновление

Исходный код:

class CatCollect:
    """

    Класс описывает все методы, необходимые для сбора всех
    ссылок, которые находятся глубже заданного URL

    """

    @profile()
    def __init__(self, url):
        logging.basicConfig(filename='script_log.log', level=logging.INFO)
        logging.info(f"{time.time()} start run")
        self.sql = SQLTest()
        self.sql.get_connection()
        logger.add("debug.log", format="{time} {level} {message}", level="DEBUG", rotation="10 MB", compression="zip")
        self.link = main_link  # link
        self.scheme_domain, self.url = urlparse(self.link).scheme + '://' + urlparse(self.link).hostname, \
                                       urlparse(self.link).path
        self.waste_words = ['js', 'ico', 'https', 'css', 'png']  # значения, которых не должно быть в ссылке
        self.proxies = []
        self.tree = BeautifulSoup(requests.get(self.link).content, 'html.parser')
        self.get_tree_urls()

    def get_tree(self, link):
        """Объединение методов получение html дерева"""
        logging.info('Начало выполнения get_tree')
        if self.proxies:
            return self.get_tree_proxy(link)
        else:
            return self.get_tree_free(link)

    @staticmethod
    def get_tree_free(link):
        """Получаем html дерево"""
        logging.info(f"Ссылка - {link}")
        logging.info("Начало выполнения get_tree_free")
        r = requests.get(link, headers={'User-Agent': UserAgent().chrome})
        logging.info(f"статус код - {r.status_code}")
        if r.status_code == 200:
            logging.info("Возвращаем нормальное дерево")
            return BeautifulSoup(r.content, 'html.parser')
        else:
            logging.info("Статус код НЕ 200, дальше return")
            return None

    def get_tree_proxy(self, link):
        """Получаем html дерево, используя прокси"""
        logging.info("Какого то пошло в proxy")
        pass

    @logger.catch()
    def get_tree_urls(self, tree=False):
        """Получаем все ссылки со страницы"""
        # Временный список для url со страницы
        logging.info(f"Выполняется функция get_tree_urls")
        tree = tree if tree is not False else self.tree
        links = tree.find_all(href=True)
        logging.info(f"Выполняется функция check_waste_words и append_urls")
        for link in links:
            url = link.get('href')
            self.check_waste_words(url)
        logging.info('Запуск get_next_url')
        self.get_next_url()

    def check_waste_words(self, url):
        """Проверка на наличие в ссылке ненужные значения"""
        time_dict = {}  # Временный словарь, чтобы не сохранить несколько
        for word in self.waste_words:
            if word not in url:
                time_dict[url] = 1
            else:
                try:
                    del time_dict[url]
                except KeyError:
                    break
        self.append_urls(time_dict)

    def append_urls(self, time_dict):
        """Добавляет url'ы в стеки"""
        for url in time_dict.keys():
            exist = self.sql.select_exist(url, 'links', 'link')  # Проверка существования данного url в БД
            if not exist:
                self.sql.insert_link(url, 'links')
                self.select_valid_url(url)

    def select_valid_url(self, url):
        """Отбирает ссылки, по которым можно продолжать парсинг"""
        if self.url in url:
            #  Добавляет ссылку, по которой можно продолжить собирать данные
            self.sql.insert_link(url, 'cor_links')
        else:
            return None

    def concat_domain_url(self, url):
        """Собираем рабочую ссылку из домена и url"""
        return self.scheme_domain + url

    def get_next_url(self):
        """Получает из БД следующую ссылку на сбора url'ов"""
        logging.info('Зашло в get_next_url')
        logging.info(f"{time.time()}, время")
        try:
            new_url = self.sql.select_cor_link()  # Получение новой ссылки
        except TypeError:
            return None  # Условие остановки программы
        self.sql.delete_cor_link(new_url)  # Удаление полученной новой ссылки
        full_link = self.concat_domain_url(new_url)
        try:
            logging.info('Получение tree')
            tree = self.get_tree(full_link)
        except (urllib3.exceptions.NewConnectionError, requests.exceptions.ConnectionError) as e:
            logging.info(f'Ошибка получения дерева, {e}')
            self.get_next_url()
        try:
            logging.info('ТУТ НАЧАЛОСЬ ВЫПОЛНЕНИЕ ПАРСИНГА НОВОЙ СТРАНИЦЫ')
            self.get_tree_urls(tree)
        except Exception as e:
            logging.info(f'Да я ошибку{e}')
            self.get_next_url()
        logging.info(f"Почему то тут закончилось действие")

Ответы (1 шт):

Автор решения: Alexandr Kiselev

Решение найдено! Из-за хоть и ограниченной, но очень долгой рекурсии переполнялся стек и программа завершалась. Переписал скрипт, добавив 2 новые функции, в одной из которых есть цикл while:

class CatCollect:
    """

    Класс описывает все методы, необходимые для сбора всех
    ссылок, которые находятся глубже заданного URL

    """

    @profile
    def __init__(self, url):
        self.sql = SQLTest()
        self.sql.get_connection()
        self.link = link_here  # url
        self.scheme_domain, self.url = urlparse(self.link).scheme + '://' + urlparse(self.link).hostname, \
                                       urlparse(self.link).path
        self.proxies = []
        self.first_parse()

    def first_parse(self):
        """Получение первых страниц"""
        tree = self.get_tree(self.link)
        self.get_tree_urls(tree)
        self.pars()
        return None

    def pars(self):
        """Главная функция, которая объединяет все остальные функции"""
        while True:
            new_url = self.get_next_url()
            if new_url:
                try:
                    tree = self.get_tree(new_url)
                except (requests.exceptions.ConnectionError, urllib3.exceptions.NewConnectionError):
                    continue
                self.get_tree_urls(tree)
            else:
                return None
            h = hp.heap()
            with open('memory_strict_control.txt', 'a') as f:
                print(h, file=f)
                print('\n', file=f)

    def get_tree(self, link):
        """Объединение методов получение html дерева"""
        if self.proxies:
            return self.get_tree_proxy(link)
        else:
            return self.get_tree_free(link)

    @staticmethod
    def get_tree_free(link):
        """Получаем html дерево"""
        try:
            r = requests.get(link, headers={'User-Agent': UserAgent().chrome}, timeout=2)
        except requests.exceptions.Timeout:
            return None
        if r.status_code == 200:
            return BeautifulSoup(r.content, 'html.parser')
        else:
            return None

    def get_tree_proxy(self, link):
        """Получаем html дерево, используя прокси"""
        pass

    def get_tree_urls(self, tree):
        """Получаем все ссылки со страницы"""
        # Временный список для url со страницы
        links = tree.find_all(href=True)
        self.iterate_links(links)
        return None

    def iterate_links(self, links):
        """Перебираем ссылки и добавляем их"""
        for link in links:
            url = link.get('href')
            self.append_urls(url)
        return None

    def append_urls(self, url):
        """Добавляет url'ы в стеки"""
        exist = self.sql.select_exist(url, 'links', 'link')  # Проверка существования данного url в БД
        if not exist:
            self.sql.insert_link(url, 'links')
            self.select_valid_url(url)
        return None

    def select_valid_url(self, url):
        """Отбирает ссылки, по которым можно продолжать парсинг"""
        if self.url in url:
            self.sql.insert_link(url, 'cor_links')  # Добавляет ссылку, по которой можно продолжить собирать данные
        else:
            return None

    def concat_domain_url(self, url):
        """Собираем рабочую ссылку из домена и url"""
        return self.scheme_domain + url

    def get_next_url(self):
        """Получает из БД следующую ссылку на сбора url'ов"""
        try:
            new_url = self.sql.select_cor_link()  # Получение новой ссылки
            self.sql.delete_cor_link(new_url)  # Удаление полученной новой ссылки
            full_link = self.concat_domain_url(new_url)
            return full_link
        except TypeError:
            return None  # Условие остановки программы
→ Ссылка