Задать вопрос
Ответы пользователя по тегу Python
  • Почему модель обнаружения объектов YOLO работает медленно?

    Vindicar
    @Vindicar
    RTFM!
    Первый момент тебе уже подсказали - потоки в питоне не помогут.
    Второй момент - я фз зачем тебе supervision, попробуй сначала голый yolo, и в частности, попробуй запустить его на видяхе.
    Третий момент - yolo бывают разного размера. Чем больше модель, тем точнее, но и тем медленнее.

    Вот пример, с которым я работал

    """
    Принимаем видео с вебкамеры, и пытаемся сегментировать его с помощью YOLOv8.
    Выводим в окне маски объектов.
    Потребуется установить пакеты следующей командой:
    pip install ultralytics
    """
    from sys import argv
    from pathlib import Path
    
    import numpy
    import cv2
    import torch
    
    COLORS = [
        (64, 128, 128),
        (128, 64, 64),
        (64, 128, 64),
        (64, 64, 128),
        (128, 64, 128),
    ]
    
    VIDEO_SOURCE = 0  # 0 - вебкамера. Строка - имя файла или URL видео потока.
    script_dir = Path(argv[0]).parent.resolve()
    # определяем, на каком устройстве будет выполняться модель
    device = (
        'cuda' if torch.cuda.is_available() else
        'mps' if torch.backends.mps.is_available() else
        'cpu'
    )
    
    import ultralytics
    
    
    def text_at(  # рисует текст по центру
            img: numpy.ndarray,  # на чём
            text: str,  # что
            pos: tuple[int, int],  # где центр
            font=cv2.FONT_HERSHEY_COMPLEX,  # каким шрифтом
            font_scale: float = 1.0,  # в каком масштабе (не кегль!)
            font_thickness: int = 2,  # насколько толстые линии
            text_color=(255, 0, 0),  # каким цветом
            bg_color=None  # на каком фоне
            ):
        x, y = pos
        (text_w, text_h), _ = cv2.getTextSize(text, font, font_scale, font_thickness)
        if bg_color is not None:
            cv2.rectangle(img,
                          (x - text_w // 2, y - text_h // 2),
                          (x + text_w // 2, y + text_h // 2),
                          bg_color, -1)
        cv2.putText(img,
                    text,
                    (x - text_w // 2, y + text_h // 2 - 1),
                    font, font_scale, text_color, font_thickness)
    
    
    model = ultralytics.YOLO(
        script_dir / 'yolov8m-seg.pt'  # имя файла модели указывает на её тип
    )
    
    # цикл работы с видео
    video = cv2.VideoCapture(VIDEO_SOURCE)
    if not video.isOpened():
        raise SystemExit('Не удалось открыть источник видео')
    overlay_image: numpy.ndarray = numpy.zeros(  # изображение-оверлей для отображения разметки кадра
        (
            int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)),
            int(video.get(cv2.CAP_PROP_FRAME_WIDTH)),
            3
         ),
        numpy.uint8
    )
    
    try:
        while True:
            success, frame = video.read()  # читаем кадр
            if not success:
                raise SystemExit('Видео закончилось')
            # для YOLO предобработка не требуется
            outputs = model.predict(  # получаем отклик сети
                source=frame,
                conf=0.15,  # минимальная степень уверенности в объекте
                device=device,  # устройство
                verbose=False  # чтобы не спамило логом в консоль
            )
            output = outputs[0]  # модель всегда возвращает список результатов, даже для одного изображения
            if output.masks is not None:  # если были получены маски
                masks = output.masks.cpu().xy  # список масок как координат вершин многоугольников (контуров)
                boxes = output.boxes.cpu()  # ограничивающие прямоугольники
                image_output = []  # список аннотаций, которые нужно будет вывести
                for box, mask in zip(boxes, masks):  # формируем список аннотаций
                    class_id = int(box.cls.item())  # номер класса, соответствующего очередной области
                    class_name = model.names[class_id]  # имя класса, соответствующее области
                    pts = numpy.array(mask).astype(numpy.int32)  # массив координат точек контура Nx2
                    image_output.append((class_id, class_name, pts))
                overlay_image.fill(0)  # очищаем изображение-оверлей
                for (class_id, class_name, pts) in image_output:  # закрашиваем каждую маску цветом
                    cv2.fillPoly(
                        img=overlay_image,
                        pts=pts[numpy.newaxis, ...],  # для работы fillPoly() массив должен быть вида 1xNx2
                        color=COLORS[class_id % len(COLORS)],  # цвет массива определяем по номеру класса для стабильности
                    )
                for (class_id, class_name, pts) in image_output:  # выводим надписи отдельным циклом, чтобы их не закрасило
                    M = cv2.moments(pts)  # вычисляем моменты контура
                    # используем их для расчёта координат центроида контура
                    cX = int(M["m10"] / M["m00"])
                    cY = int(M["m01"] / M["m00"])
                    # выводим имя класса
                    text_at(overlay_image, class_name,
                            (cX, cY),
                            text_color=(255, 255, 255),
                            bg_color=(1, 1, 1))
                # накладываем оверлей с разметкой на кадр
                alpha = 0.8  # вес оверлея, чем меньше - тем он прозрачнее.
                apply = overlay_image > 0  # изменяем только те части кадра, где есть оверлей
                frame[apply] = alpha * overlay_image[apply] + (1 - alpha) * frame[apply]  # обожаю numpy
            cv2.imshow('Press Esc to exit', frame)  # показываем результат
            if cv2.waitKey(10) == 27:  # если пользователь нажал Esc, выходим
                break
    finally:
        video.release()  # не забываем отключиться от источника видео в итоге

    Ответ написан
    Комментировать
  • Как сделать нажатие кнопки при парсинге в python?

    Vindicar
    @Vindicar
    RTFM!
    Да, можно.
    Используй консоль разработчика в браузере, чтобы понять, какой именно запрос отправляется на сервер. Обычно нужно выяснить метод (GET/POST), URL, параметры, тело. Либа requests и аналоги позволяют всё это задать, просто почитай доки по конкретной теме.
    Ответ написан
    Комментировать
  • Как запустить отдельный скрипт в субпроцессах, получив интерфейс типа concurrent.futures.Executor?

    Vindicar
    @Vindicar Автор вопроса
    RTFM!
    Плохая новость - готового решения сформулированной в вопросе задачи, похоже, нет. Нужно использовать брокеры или что-то подобное.

    Хорошая новость - я снова попался на проблему XY. Всё нормально, если спрятать тяжёлые импорты в теле основной программы вот так:
    # ------------------- main.py -------------------
    async def main():
        import urllib3  # как бы тяжелый модуль, нужный только главному скрипту
        import importlib
        ...  # получаем исходные данные
        # динамически подтаскиваем нужный модуль
        mod = importlib.import_module('modules.submodule')  
        m = mod.Module(x=-1)  # используем класс из него
        # этот метод под капотом использует мультипроцессинг
        results = await m.run_tasks(some_data)
        ...  # что-то делаем с результатами
    
    if __name__ == '__main__':
        import asyncio
        asyncio.run(main())
    
    # ------------------- modules/submodule.py -------------------
    import asyncio
    import concurrent.futures
    import time
    import sys
    import os
    
    class Module:
            def __init__(self, x):
            self.x = x
    
        def __repr__(self) -> str:
            return f'{self.__class__.__name__}(x={self.x!r}) {hex(id(self)).upper()} @ {os.getpid()}'
    
        async def run_tasks(self, data):
            self.x = len(items)
            print(f'Using instance {self!r}')
            loop = asyncio.get_running_loop()
            pool = concurrent.futures.ProcessPoolExecutor(4)  # где будем исполнять таск
            # готовим таски к исполнению
            futures = [loop.run_in_executor(pool, self.worker_func, item) for item in data]
            # ждём их завершения
            return await asyncio.gather(*futures, return_exceptions=True)
    
        def worker_func(self, item):
            # инстансы разные, разумеется, но их состояние, похоже, клонируется в новые процессы...
            print(f'Using instance {self!r}. urllib3', 'is' if 'urllib3' in sys.modules else 'is not', 'present', flush=True)
            time.sleep(0.1)  # имитируем напряжённую работу
            return repr(item)
    Ответ написан
    Комментировать
  • Как правильно спроектировать эту функцию?

    Vindicar
    @Vindicar
    RTFM!
    Ты неверно понимаешь, как работает overload. Это тупо подсказка для IDE - "эту функцию можно вызывать вот так, и она вернёт вот это, или же вот этак, и она вернёт вон то". Тело функции, завёрнутой в overload, игнорируется. А последнее объявление функции не должно иметь overload, и оно как раз и будет реализовывать все варианты. Так что итоговая реализация всё равно будет иметь if внутри. Да и в твоём случае overload никчему, так как другие-то параметры не отличаются.

    Ты мог бы, конечно, упороться, и сделать functools.singledispatch - при условии, что ты message переставишь первым параметром. Но, имхо, в этом нет практической необходимости, всё это делается в разы проще.

    async def show_cpanel(
        state: FSMContext,
        message_source: typing.Union[Message, CallbackQuery]  # значение одного из двух типов, но обязательное!
    ) -> None:
        '''
        Send control panel to current user
        '''
        this_user = message_source.from_user.id  # и Message, и CallbackQuery имеют from_user
        message = message if isinstance(message_source, Message) else message_source.message
    
        profile_is_visible = await req.check_profile_visible(this_user)
        message_id = (await message.answer(
            '<b>' + _('Панель управления') + '</b>',
            parse_mode='HTML',
            reply_markup=kb.control_panel(profile_visible=profile_is_visible)
        )).message_id
        await state.update_data(cpanel_message_id=message_id)
        logger.info(f'Admin #{this_user} opened control panel')

    Вот и всё. Твой вариант
    callback_query: Optional[CallbackQuery] = None,
        message: Optional[Message] = None,

    плох тем, что из него совершенно не очевидно, что хотя бы один из параметров не должен быть None.

    EDIT: Хотя я не вполне понял идею насчёт callback.message. Если я верно помню, этот атрибут хранит ссылку на сообщение от бота, содержащее кнопку, для которой был вызван callback. Поэтому его from_user по идее будет заведомо указывать на бота. Так что да, лучше последуй совету Everything_is_bad и переделай функцию так, чтобы она принимала пользователя и сообщение отдельными параметрами. А их значения определяй там, где ты функцию вызываешь.
    Ответ написан
    5 комментариев
  • Не копятся ли строки в памяти при работе с python?

    Vindicar
    @Vindicar
    RTFM!
    Попробовал накидать скрипт для проверки:
    import tracemalloc
    
    tracemalloc.start()
    
    for i in range(4):
        snapshotA = tracemalloc.take_snapshot()
        s = f'Строка №{i+1}'
        snapshotB = tracemalloc.take_snapshot()
        s = s.replace('Строка', 'Другая строка')
        snapshotC = tracemalloc.take_snapshot()
        print('>', s)
        statab = snapshotB.compare_to(snapshotA, 'lineno')
        print("[A->B]")
        for stat in statab[:10]:
            if stat.traceback[0].filename == __file__:
                print(stat)
        statbc = snapshotC.compare_to(snapshotB, 'lineno')
        print("[B->C]")
        for stat in statbc[:10]:
            if stat.traceback[0].filename == __file__:
                print(stat)

    Если я верно интерпретирую вывод, неиспользуемые строки "умирают" на каждой итерации.
    Но ситуация усложняется, если на них есть ссылки в других местах, особенно если имеют место циклические ссылки.
    Ответ написан
  • Извлечение таблиц со спецификациями из PDF чертежей металлоконструкций - решаемо? Или я встрял?

    Vindicar
    @Vindicar
    RTFM!
    Имхо без комбинации подходов ловить вообще нечего. Найди инструмент (и набор предобработок), который найдёт тебе разметку таблиц, выдерни содержимое отдельных ячеек, и подбирай предобработки+инструмент, который будет распознавать ячейки.
    Ответ написан
    Комментировать
  • Как ловить exceptions в библиотеке которую я не использую напрямую?

    Vindicar
    @Vindicar
    RTFM!
    где я его должен ловить учитывая что саму urllib3 я через import не подключаю

    Ну так подключай её в свой код и лови в своём коде. from urllib3.exceptions import LocationParseError
    Раз requests её уже использует, затраты на import в твоём скрипте будут околонулевые.
    requests поступает вполне логично, не изобретает велосипед (т.е. своё исключение с тем же смыслом), а задействует тип исключения из используемой стандартной библиотеки языка.
    Ответ написан
    Комментировать
  • Какую библиотеку лучше использовать для discord бота на python?

    Vindicar
    @Vindicar
    RTFM!
    discord.py уже давно начал деятельность обратно. На момент написания этого коммента на гитхабе последний коммит 3 месяца назад, issue закрыт считанные дни назад.
    Ответ написан
    Комментировать
  • Почему возникают ошибки несовместимости версий Python и как решить проблему с установкой PySimpleGUI?

    Vindicar
    @Vindicar
    RTFM!
    Разобраться, какие версии библиотек всё-таки нужны. Ты не написал, для каких пакетов возникает ошибка.
    Возможно, стоит смягчить требования - например, не требовать определённую версию пакета, а любую, или ограничится только первой цифрой. Скажем, вместо somepackage==1.2.3 указать somepackage==1.* или вообще somepackage без версии.
    Ответ написан
    Комментировать
  • Можно ли улучшить приложенный AutoEnum (см. код ниже) в python?

    Vindicar
    @Vindicar
    RTFM!
    Очевидно, что декоратор должен находить и заменять объявленные константы сам, так как всё остальное уже отработало.
    Если хочешь полагаться на метакласс, ему можно передавать аргументы при наследовании. ЕМНИП:
    class MyMeta(type):
        def __new__(metacls, name, bases, namespace, **kwargs):
            print(f"__new__(): {name}({kwargs})")
            return super().__new__(metacls, name, bases, namespace)
    
    class MyBase(metaclass=MyMeta):  # __new__(): MyBase({})
        pass
    
    class MyClass(MyBase, foo='bar'):  # __new__(): MyClass({'foo': 'bar'})
        pass

    А вообще я не вижу выигрыша в твоём классе. Статическая типизация, к слову, барахлить не начнёт? Среда разработки будет правильно опознавать TestAutoEnum.FIRST как экземпляр TestAutoEnum?
    Ответ написан
    Комментировать
  • Почему api золотого яблока блокирует все запросы?

    Vindicar
    @Vindicar
    RTFM!
    Вполне может быть, что банят за запросы не из диапазонов домашних и мобильных провайдеров.
    Ответ написан
    Комментировать
  • Как можно ускорить выполнение этого кода?

    Vindicar
    @Vindicar
    RTFM!
    Я верно понимаю, что нужно найти слова не короче заданной длины, которые можно составить из букв данного слова?
    Во-первых, может иметь смысл сразу отбросить слова, содержащие буквы не из данного слова, чтобы в дальнейшем анализировать только сравнительно допустимые слова. Также может иметь смысл использовать для подсчёта количества букв collections.Counter.
    Например, так
    from collections import Counter
    
    main_word = 'АКСИЛИРОВАНИЕ' + '\n'  # основное слово + перенос строки, чтобы не вызывать str.rstrip()
    main_set = frozenset(main_word)  # множество букв слова без учёта повторов
    main_len = len(main_word)
    min_length = 8
    with open('D:\\Program Files\\Text\\слова\\1.txt', 'r') as f:
        candidates = [  # слова-кандидаты, состоящие из тех же букв и подходящие по длине
            (word, Counter(word))  # само слово и его состав по буквам
            for word in f  # для всех слов в файле
            # проверяем длину слова и соответствие набора букв без учёта их количества
            if min_length<=len(word)<=main_len and main_set.issuperset(word)  
        ]
    main_counter = Counter(main_word)  # подсчёт числа букв в основном слове
    results = [  # итоговый результат
        word  # те слова
        for word, counter in candidates  # из числа слов-кандидатов
        if all(counter[key] <= main_counter[key] for key in counter)  # у которых нет превышения ни по одной букве
    ]


    Во-вторых, важна структура файла. Например, если строки в файле расположены по возрастанию длины, мы можем попробовать быстренько пропустить короткие слова в начале файла, обработать то, что идёт после, и остановиться, когда дойдём до слов длиннее заданного.
    В-третьих, если вместо текстового файла использовать, скажем, pickle-файл, и хранить в нём предрассчитанные количества букв для каждого слова, то это потенциально может ускорить процесс.
    Но чтобы проверить скорость работы кода, нужна ссылка на пример файла со словами. И уточни, как обрабатывается буква Ё.
    Ответ написан
    Комментировать
  • Как создать task в __init__ asyncio?

    Vindicar
    @Vindicar
    RTFM!
    Тебе довольно прямо сказано: "AttributeError: loop attribute cannot be accessed in non-async contexts."
    Иными словами, атрибут Bot.loop (ссылку на рабочий цикл asyncio) можно читать, только если ты находишься внутри async def функции, прямо или косвенно.
    Причина простая - asyncio.run() или эквивалентная функция как раз создаёт и запускает рабочий цикл asyncio (обычно называемый event loop или просто loop). Если рабочий цикл ещё не создан, то что должен вернуть атрибут Bot.loop? На этот вопрос просто нет правильного ответа. Поэтому доступ к атрибуту блокируется.

    И насчёт решения тоже подсказано: "Consider using either an asynchronous main function and passing it to asyncio.run or using asynchronous initialisation hooks such as Client.setup_hook". Иными словами, тебе нужно сделать так, чтобы твой код, обращающийся к Bot.loop, выполнялся в асинхронной (async def) функции. Это можно сделать двумя способами.

    Первый: вынести твой код из конструктора (который не может быть асинхронным) в отдельный метод. Например, Bot (и его предок Client) позволяют вызвать асинхронный метод on_ready() при запуске бота. Тут есть много оговорок - в частности, on_ready() может быть вызван неоднократно, если есть проблемы с соединением. Это нужно иметь ввиду.

    Второй: завернуть вызов всего твоего конструктора в асинхронный метод. Упрощённо, вместо
    import discord
    from discord.ext import commands
    
    intents = discord.Intents.default()
    intents.members = True
    intents.message_content = True
    
    bot = commands.Bot(command_prefix='!', intents=intents)
    # ...
    bot.run('token')

    можешь попробовать что-то вроде
    import discord
    from discord.ext import commands
    
    
    async def main():
        # асинхронная функция может быть выполнена ТОЛЬКО внутри рабочего цикла
        # значит, рабочий цикл уже точно существует и выполняется
        intents = discord.Intents.default()
        intents.members = True
        intents.message_content = True
        # конструктор сам по себе не асинхронный, но он выполняется в асинхронном контексте
        bot = commands.Bot(command_prefix='!', intents=intents)  
        # ...
        # мы уже в асинхронной функции, поэтому используем await start() вместо run()
        await bot.start('token')  # main() не завершит работу, пока бот не завершит работу
    
    
    if __name__ == '__main__':
        asyncio.run(main())  # создаём рабочий цикл. он будет работать, пока main() не завершит работу


    Но я соглашусь с Everything_is_bad - сначала разберись, как работает asyncio. Строить сложные конструкции с несколькими долгоживущими корутинами методом проб и ошибок - это слишком муторно.

    Ну и очень большой вопрос от меня: ты, я вижу, мастеришь систему плагинов. Чем тебя не устроили коги?
    Ответ написан
    3 комментария
  • Как исправить ошибку?

    Vindicar
    @Vindicar
    RTFM!
    Объясняю на пальцах:
    UPDATE trial SET trialactive = 0 WHERE trialkey = 'vless:-test'
    - обновить запись, где столбец trialkey равен строке "vless:-test"
    UPDATE trial SET trialactive = 0 WHERE trialkey = vless:-test
    - обновить запись, где столбец trialkey равен столбцу vless:-test, а такого столбца у тебя нет.

    А причина - потому что ты не озаботился как следует посмотреть примеры работы с БД в питоне, и сразу побежал херачить текст запроса с помощью f-строк, хотя каждый первый туториал предупреждает что так делать нельзя, а нужно использовать placeholder'ы.

    Ссылку на https://docs.python.org/3/library/sqlite3.html#sql... тебе выше дали, разобрать её несложно.
    Первый пример кода (который помечен # Never do this -- insecure!) допускает ту же самую ошибку, что и твоё
    cursor.execute(f'UPDATE trial SET trialactive = 0 WHERE trialkey = {results}')
    и другие запросы.
    А второй пример кода показывает, как правильно.
    # This is the qmark style used in a SELECT query:
    params = (1972,)
    cur.execute("SELECT * FROM lang WHERE first_appeared = ?", params)

    Т.е. ставишь знак вопроса там, где нужно вставить значение, а потом вторым параметром передаёшь кортеж вставляемых значений - столько, сколько у тебя знаков вопроса в запросе.
    Ответ написан
    Комментировать
  • Как устроен вызов классов в Python?

    Vindicar
    @Vindicar
    RTFM!
    Насколько я это понимаю:
    1. Вызов класса транслируется в обращение к метаклассу, т.е. klass.__class__.__call__()
    2. По умолчанию метакласс обращается к klass.__new__(). Если класс не определяет этот метод, он ищется по предкам. Задача __new__() - вернуть экземпляр класса, который был "сконструирован". Это не обязательно новый экземпляр, у нас может быть синглтон или ещё что-то.
    3. Получив экземпляр instance, метакласс обращается к instance.__class__.__init__(), чтобы проинициализировать возвращённый экземпляр. Вроде где-то упоминалось, что если __new__() возвращает экземпляр другого класса, то и __init__() будет вызван от этого другого класса.
    4. После того, как экземпляр был проинициализирован, klass.__class__.__call__() его возвращает программе
    Это подтверждается таким тестовым кодом:
    class MetaTest(type):
        def __call__(self, *args, **kwargs):
            print('MetaTest.__call__() is being called...')
            instance = super().__call__(*args, **kwargs)
            print(f'MetaTest.__call__() returning {instance=}')
            return instance
    
    class Test(metaclass=MetaTest):
        def __new__(cls):
            print('Test.__new__() is being called...')
            instance = super().__new__(cls)
            print(f'Test.__new__() returning {instance=}')
            return instance
        
        def __init__(self):
            print(f'Test.__init__() has been called on instance = {self}')
    
    
    t = Test()

    И вот результат выполнения:
    MetaTest.__call__() is being called...
    Test.__new__() is being called...
    Test.__new__() returning instance=<__main__.Test object at 0x0000028EC8E41700>
    Test.__init__() has been called on instance = <__main__.Test object at 0x0000028EC8E41700>
    MetaTest.__call__() returning instance=<__main__.Test object at 0x0000028EC8E41700>
    Ответ написан
    7 комментариев
  • Как правильно обрабатывать ошибки при чтении файла?

    Vindicar
    @Vindicar
    RTFM!
    Тебе правильно написали про with, а я добавлю в чём проблема:
    # допустим, это наш код
        try:
            file = open("config.json", "r")  # исключение может произойти тут
            config = json.load(file)  # или тут
            print(config)
        except FileNotFoundError:
            print(">>> Файл не найден!")
        except PermissionError:
            print(">>> Доступ запрещен!")
        finally:
            print(">>> Файл закрылся!")
            file.close()

    У тебя две разные ошибки, на которые требуются разные реакции.
    Если исключение произойдёт в open(), то переменная file не будет создана, так как до присваивания дело просто не дойдёт. Если же исключение произойдёт в load(), то переменная file будет существовать, файл бдует открыт, и его нужно будет закрыть. Таким образом, тебе нужно или обработать эти ошибки отдельно, или использовать другие средства (вроде оператора with) для закрытия файла.
    Ответ написан
    Комментировать
  • Pytest. Почему декоратор не записывает в файл логи?

    Vindicar
    @Vindicar
    RTFM!
    with tempfile.NamedTemporaryFile(dir=custom_dir, delete=False, mode="w") as temp_file:

    Немедленно по выходу из with файл закроется, и будет удалён.
    Ответ написан
    2 комментария
  • Как отправить запрос в LM Studio?

    Vindicar
    @Vindicar
    RTFM!
    Примеры использую утилиту curl. В них прописаны:
    1. URL запроса, например, localhost:1234/api/v0/chat/completions
    2. Заголовки, например, Content-Type: application/json
    3. Тело запроса, например,
    {
        "model": "granite-3.0-2b-instruct",
        "messages": [
          { "role": "system", "content": "Always answer in rhymes." },
          { "role": "user", "content": "Introduce yourself." }
        ],
        "temperature": 0.7,
        "max_tokens": -1,
        "stream": false
      }


    Как уже выше написали, читаешь доки на модуль requests (ну или aiohttp, если тебе лучше работать асинхронно), они позволяют всё это делать. Просто нужно выучить, как.
    Ответ написан
    Комментировать
  • Несколько запросов к API с помощью python?

    Vindicar
    @Vindicar
    RTFM!
    Используй цикл for, самое простое решение.
    Ответ написан
    Комментировать