Практика 14

В этой лабе вы напишите простое многопоточное приложение.

Scraper

Web scraping wiki - это процесс скачивания и последующего разбора веб страниц. Ваша задача - написать свой web-scraper, который полностью скачает все гиперссылки (<a href='ADDRESS'> элементы) в рамках этого сайта (во вне выходить нельзя, иначе мы скачаем весь интернет). То есть вам не надо качать картинки и страницы, а только сохранять сами адреса. По своей сути мы сделаем ограниченную версию wget. Примеры использования: тут и тут.

Экспериментировать можете с сайтом ru.wikipedia.org/wiki/Заглавная_страница.

Инструкции

Рекомендуется использовать библиотеку BeautifulSoup4. Русскоязычный туториал, официальная документация.
Поставить пакет можно командой pip install beautifulsoup4.

Как должно всё работать

  1. Консольная утилита принимает как аргумент адрес сайта.
  2. Программа скачивает к себе в память html страницу по этому адресу.
  3. Программа разбирает html и ищет все <a> теги и извлекает href параметр.
  4. По каждому href идем в 1 пункт, так как внутри href хранится ссылка на страницу сайта.
  5. Когда-нибудь процесс остановится и результат - список уникальных страниц разделенных новой строкой, должен лежать одном файле.

Пример файла links.csv:

https://en.wikipedia.org/wiki/Main_Page
https://en.wikipedia.org/wiki/Portal:Arts
https://en.wikipedia.org/wiki/Portal:Contents/Portals
.... и так далее ....

Однопоточная версия

Напишите в лоб решение задачи. Можете сделать рекурсивно, можете обход в ширину или глубину. Как угодно. Ограничьте работу программы 1000-ей ссылок. Это понадобится нам для замера скорости работы.

Многопоточная версия с очередями

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

Рассмотрим обычный подход для решения подобных задач - использование очереди задач и набора(pool) работников-потоков (thread workers).

Наша задача относится к классу задач, параллельних по данным. Нам нет смысла ждать, когда будет произведен разбор предыдущей ссылки, если у нас есть еще N штук таких же. Мы могли бы нашим работникам раздать задания - работник номер 1 возьми ссылки с 1 по 10, работник 2 - ссылки с 11 по 20 и так далее. Работники в процессе своей работы получат еще N других ссылок, которые они положат опять в некоторое хранилище ссылок на обработку.

Реализовывать это можно следующим образом. У нас есть некоторое хранилище задач на обработку и есть конечное число работников. Каждый из них берет задачу из хранилища, что-то с ней делает и может, например, положить в это же хранилище следующие задачи. Если работник пришел к хранилищу, а в нем нет задач, то он должен заблокироваться и ждать пока не появится новая задача.
Так как в наше хранилище кто-то складывает задачи и кто-то их обрабатывает, то это очень похоже на Очередь. А условие с ожиданием новых задач - это Очередь с блокировкой.

В Python есть класс Queue и модуль threading для решение подобных задач.

Обратите внимание, что в многопоточность сама по себе очень сложная наука. В описании мы обошли понятия синхронизации потоков, гонок, дедлоков и прочего, полагая, что вы еще изучите это дальше. А в данной лабораторной мы просто показали вам паттерн Workers Pool.

Теперь сравните скорость выполнения задачи в однопоточном режиме и многопоточном.

Как вы думаете, что был “узким горлышком” в данной программе и почему распаралеливание помогло. Кстати, работа асинхронная однопоточная версия будет не медленнее многопоточной, как вы думаете - почему?

Не Python

Если вы делаете это задание на Java, то это даже интереснее. Вы скорее всего будете использовать ExecutorService и синхронизированную очередь. Попробуйте для интереса использовать потоко НЕ безопасную очередь и посмотрите, что случится.

Прочитайте статью на русском про Java Concurency.

Дополнительные материалы