Пишем асинхронный парсер и скрапер картинок на Python с графическим интерфейсом
  • Тема была создана
  • 46
  • html
  • python
  • ооп
  • программирование
sitekeys
  • 8
  • 13
В этой статье мы создадим desktop-приложение, которое по нашему запросу будет сохранять на нашем диске заданное количество картинок. Так как картинок будет много, мы воспользуемся асинхронностью Python для конкурентной реализации операций ввода-вывода. Посмотрим, чем отличаются библиотеки requests и aiohttp. Также создадим два дополнительных потока приложения, чтобы обойти глобальную блокировку интерпретатора Python.

Вместо тысячи слов…​

Чтобы вы лучше поняли, о чем я говорю, я вам просто покажу, что в итоге должно получиться и как это будет работать:
0e1317589844e31d517d4d0ebbbf9ccc


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

Классы графического интерфейса​

У нас будет отдельный класс графического интерфейса. Назовем его UI – это главное окно программы. В этом окне есть две различные рамки (frames). Давайте эти рамки также представим различными классами:

  • Класс SearchFrame будет отвечать за ввод поискового запроса, по которому будет осуществляться поиск картинок.
  • Класс ScraperFrame будет отвечать за вывод количества доступных для скачивания картинок, задания пути сохранения этих картинок, выбора их размера и количества. Также в этом классе реализована шкала прогресса выполнения скачивания и сохранения, и информационное поле.
Реализуем эти классы с помощью стандартной библиотеки Python – tkinter.

Мы определились с классами графического интерфейса. Теперь необходимо разобраться каким именно образом будет осуществляться поиск, скачивание и сохранение картинок.

Класс для парсинга PictureLinksParser​

Выбор фотохостинга​

Вначале нам надо выбрать фотохостинг, где хранятся картинки. Здесь надо проанализировать, каким именно образом размещены фотографии на хостинге. Для начала с помощью инструментов разработчика мы отключим JavaScript и посмотрим на поведение сайта.


8b68510215f9b8814113453eb5035a93

Если у нас при обновлении страницы пропало все содержимое – скорее всего мы имеем дело с одностраничным приложением SPA (англ. Single Page Applications). А для парсинга таких сайтов требуются «продвинутые» библиотеки Python. Например, Scrapy с инструментом Splash, или еще хуже – Selenium. Scrapy – прекрасный инструмент, но не для нашего случая. Помните принцип KISS? Поэтому ищем сайт, где незначительное влияние JavaScript на контент.

Мой выбор остановился на сайте flickr.com. Единственная
проблема данного сайта в том, что при выводе картинок здесь отсутствует
пагинация страниц, а новые картинки появляются при прокрутке ленты. Тем не
менее 25 картинок без прокрутки мы имеем. Чуть позже в статье я расскажу, как
очень хитро обойти это ограничение.

Выбор библиотеки для парсинга​

Самым простым парсером для Python является Beautiful Soup. Это библиотеки вполне будет достаточно для решения нашей задачи.

Выбор библиотеки для веб-запросов​

Все знают, что существует библиотека requests. Проблема этой библиотеки в том, что она является блокирующей – на время выполнения запроса и получения данных у нас происходит глобальная блокировка интерпретатора Python (GIL). Это блокировка, которая не дает Python-процессу исполнять более одной команды байт-кода в каждый момент времени. У вас должен возникнуть вопрос – а зачем ей тогда пользуются? Для одного веб-запроса GIL будет незаметна. А представьте, что у нас 1000 таких запросов. Пока у нас вся 1000 запросов не выполнится, остальная программа будет заблокирована. Для решения этой проблемы создали неблокирующие библиотеки. Примером неблокирующей библиотеки является aiohttp, которая также умеет отправлять веб-запросы.

И здесь я до вас должен донести одну важную мысль: для одного единственного веб-запроса с aiohttp мы ничего не выигрываем у requests. Выигрыш у aiohttp будет только, если мы выполняем несколько веб-запросов конкурентно.

В классе PictureLinksParser будет выполняться только один веб-запрос для получения HTML-документа. Но так как в другом классе мы будем выполнять конкурентно несколько веб-запросов – мы установим только aiohttp. Нам не нужна дополнительная библиотека requests для одно веб-запроса – здесь её заменить aiohttp. Алгоритм парсинга следующий:
Код:
def parse_html_to_get_links(self, html: str) -> None:

        """Parses HTML and adds links to the array."""

        soup = BeautifulSoup(html, 'lxml')

        box = soup.find_all(PHOTO_CONTAINER, class_=PHOTO_CLASS)

        for tag in box:

            img_tag = tag.find('img')

            src_value = img_tag.get('src')

            self.add_links('https:' + src_value)



    async def get_html(self) -> None:

        """Downloads HTML with pictures links."""

        async with aiohttp.ClientSession() as session:

            async with session.get(self.url) as response:

                html = await response.text()

        self.parse_html_to_get_links(html)
И вот здесь всплывает первый недостаток aiohttp по сравнению с requests. Requests имеет простейший интерфейс – написал метод get(адрес веб-страницы) и получил страницу. В aiohttp мы создаём клиентскую сессию, которая является средой исполнения для выполнения HTTP-запросов и управления соединениями. Также мы используем асинхронный менеджер контекста, который позволяет корректно начинать и закрывать HTTP-сеансы.

Выводы:

  • если в программе всего один запрос (получение токена, получение одной веб-страницы) – то мы применяем библиотеку requests.
  • если в программе необходимо выполнить одновременно множество запросов – то мы используем aiohttp (или другую неблокирующую библиотеку).

Класс для скрапинга картинок PictureScraperSaver​

Когда после работы класса PictureLinksParser у нас было сформировано множество (set) ссылок картинок, мы должны перейти по этим адресам и сохранить картинки на наш диск.

Множества set() в Python​

Это стандартный тип данных, о котором все знают. Это очень быстрая коллекция, которая построена на основе хеш-таблиц и содержит только уникальные элементы. Особенность нашего фотохостинга состоит в том, что при повторном запросе в HTML документе иногда появляются абсолютно новые ссылки, которых не было в предыдущем запросе. Используя множество set, мы при повторном запросе добавляем эти ссылки в наше множество ссылок и происходит следующее: число элементов множества автоматически увеличивается на число новых уникальных ссылок с новым запросом с тем же ключевым словом.

Выполняем веб-запросы конкурентно​

Итак, множество ссылок у нас есть. Теперь по ним надо перейти и сохранить картинки на диске. Я реализовал это следующим образом (Для лучшей читаемости кода я не стал использовать list comprehension):

Код:
async def _save_image(self, session: ClientSession, url: str) -> None:
        """Asynchronously downloads the image and saves it on disk."""
        try:
            response = await session.get(url)
            if response.status == HTTPStatus.OK:
                image_data = await response.read()
                pic_file = f'{self.picture_name}{self.completed_requests}'
                with open(f'{self.save_path}/{pic_file}.jpg', 'wb') as file:
                    file.write(image_data)
                logging.info(f'Успешное сохранение картинки {url}')
            else:
                logging.error(
                    f'Ошибка при работе с картинкой {response.status}'
                    )
        except Exception as e:
            logging.exception(
                f"Ошибка при загрузке {url}: {response.status} {e}"
                )
        self.completed_requests += 1
        if self.completed_requests % self.refresh_rate == 0 or \
                self.completed_requests == self.total_requests:
            self.callback(self.completed_requests, self.total_requests)

    async def _make_requests(self) -> None:
        """Concurrently sends URL links to perform."""
        async with ClientSession() as session:
            reqs = []
            for _ in range(self.total_requests):
                current_link = self.links_array.pop()
                reqs.append(self._save_image(session, current_link))
            await asyncio.gather(*reqs)

Начнем с корутины _make_requests. Мы отдаем на конкурентное выполнение только то количество картинок, которое указали в графическом интерфейсе – атрибут self.total_requests. Методом pop() в множестве мы удаляем случайный элемент и отправляем его на скачивание и сохранение. И далее мы применяем метод asyncio.gather для конкурентного скачивания картинок по соответствующим URL адресам.

Что касается корутины _save_image – здесь все ещё проще. Мы проходим по ссылке картинки, получаем подтверждение, что все статус = 200. И далее сохраняем этот прочитанный контент с помощью стандартной функции open и бинарного режима записи по указанному адресу. На всех этапах логируем события.

Перекладываем парсер и скрапер на дополнительные потоки​

Проблема в том, что у нас на этапе парсинга и скрапинга могут возникнуть длительные выполнения операций, которые у нас заблокируют графический интерфейс. На практике, это будет выглядеть так: пока выполняется 1000 запросов, у нас заблокирован графический интерфейс, он перестанет отвечать. И операционная система предложит нам снять этот процесс, посчитав его «зависшим».

Чтобы такого не было для операций ввода-вывода используют многопоточность. Мы создадим 2 дополнительных потока и передадим туда асинхронные циклы событий.

Что мы имеем:

  • главный поток: графический интерфейс
  • дополнительный поток №1: парсер
  • дополнительный поток №2: скрапер
Остается только реализовать потокобезопасность в классах парсера и скрапера. Это достигается двумя методами asyncio:

  1. Метод call_soon_threadsafe принимает функцию Python (не корутину) и потокобезопасным образом планирует её выполнение на следующей итерации цикла событий.
  2. Метод run_coroutine_threadsafe принимает корутину, потокобезопасным образом подает её для выполнения и сразу же возвращает будущий объект, который позволит получить доступ к результату сопрограммы.

Посмотреть полный код приложения​

Код на на GitHub.

Там же в описании к репозиторию вы найдете ссылку на exe-версию программы и можете с ней немного поиграться.
В этой теме нет ответов
В данный момент, эту тему никто не просматривает

Посмотрели тему 6

Назад
Сверху