Я написал небольшую программу на Haskell для скачивания видео с Летней школы по языкам программирования в штате Орегон 2012. Репозиторий GitHub можно найти здесь.

(На самом деле, видео действительно огромные, и, возможно, вам было бы лучше посмотреть их на YouTube, вот несколько ссылок.)

Получение страницы

Я использовал wreq в качестве http-клиента, он прост и удобен в использовании, хотя, возможно, его зависимость от объектива немного тяжеловата. Кажется, что он может отлично работать с небольшой библиотекой объективов, такой как микролинза.

Разбор страницы

Я использовал комбинацию tagsoup и megaparsec с полезной библиотекой tagsoup-megaparsec, выступающей в роли клея.

Раньше я не использовал мегапарсек. Люди, переходящие из аттопарсека, должны знать, что мегапарсек не возвращается автоматически, за исключением нескольких комбинаторов. Вам нужно запросить возврат с помощью try.

Одна приятная особенность мегапарсека заключается в том, что это монадный преобразователь. У меня были проблемы с отладкой ошибки синтаксического анализа, и я поместил монаду Writer в Parser, чтобы иметь возможность проверять частичные результаты. Размещение Writer ниже Parser означает, что вы можете получать отладочные сообщения даже для веток, которые возвращаются.

Создание структуры папок

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

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

Элементарное решение, но оно работает.

Data.Tree используется для представления структуры папок, каждый узел которой содержит имя папки и список файлов для загрузки.

В качестве учебного упражнения я решил манипулировать деревьями без явной рекурсии, используя только катаморфизмы (последняя версия контейнеров включает полезный катаморфизм foldTree).

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

В итоге я использовал эту вспомогательную функцию:

inherit :: Tree a -> Tree (NonEmpty a)
inherit tree = foldTree algebra tree [] where
    algebra :: a -> [[a] -> Tree (NonEmpty a)] -> [a] -> Tree (NonEmpty a)    
    algebra a fs as = Node (a:|as) (fs <*> [a:as])

Что украшает каждый узел (непустым) списком всех его предков. Функция использует распространенный «трюк» для распространения информации от корня: заставить катаморфизм возвращать функцию, которая строит окончательный результат, а не сам окончательный результат.

Более простой вариант этого трюка — выражение foldl через foldr (если немного прищуриться, первый элемент списка подобен корню линейного дерева). Усовершенствованная версия этого трюка позволяет вычислять унаследованные атрибуты в грамматиках атрибутов.

Загрузка файлов

Чтобы разрешить несколько одновременных загрузок, я прибегнул к всегда полезному Concurrently Applicative из пакета async.

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

traverseThrottled :: Traversable t => Int -> (a -> IO b) -> t a -> IO (t b)
traverseThrottled concLevel action taskContainer = do 
    sem <- newQSem concLevel 
    let throttledAction = bracket_ (waitQSem sem) (signalQSem sem) 
                        . action 
    runConcurrently (traverse (Concurrently . throttledAction) taskContainer)

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