Компания делает сайт: стандартный справочник организаций. Клиенты на сайте ищут номера телефонов организаций. Номера телефонов устаревают. Из-за этого компания теряет клиентов.
Для каждой организации в базе хранится ссылка на её сайт и путь к странице контактов. Страниц контактов на одном сайте может быть несколько. Есть модуль, который умеет распознавать неактуальный номер. На вход он получает список номеров в формате 8KKKNNNNNNN.
Вам нужно написать модуль, который скачивает web-страницы, находит в тексте и выводит все распознанные номера телефонов в этом формате.
Номера по формату российские. Если для номера не указан код города — номер московский. Чем выше доля распознаваемых реальных номеров на странице и чем быстрее работает модуль — тем он лучше.
Здесь: https://hands.ru/company/about модуль должен найти номер, здесь: https://repetitors.info тоже. Страниц в базе может быть очень много!
В задании не нужно использовать тяжелые фреймворки или сохранять найденные номера в базу. Задание ориентировано буквально на несколько часов.
Решение нужно предоставить в виде отдельного репозитория на github.com.
Пример запуска программы:
cd src;
PYTHONPATH=. downloader/main.py --num_processes 3 --max_downloads_per_process 10 ../data/sample10.txt
В каталоге data
два набора данных:
sample10.txt
- файл с 10 урлами, два из которых упомянуты в заданииsample10_repeated.txt
- файл с этими же 10 урлами, повторенными несколько раз
Найденные телефоны выводятся в логах.
Теперь собственно по решению.
- для получения списка урлов используется
UrlsProvider
и его простая реализацияListUrlsProvider
для хранения урлов в памяти (и чтения из файла). Несложно сделать другую реализацию для чтения, например, из базы данных. Кроме того, класс может выдавать урлы батчами, например, с последовательными запросами к базе данных - для парсинга телефонов используется класс
PhoneParser
. Я честно начал писать регулярки на телефоны, но потом подумал, что это тысячу раз делали до меня и нашел прекрасную библиотекуphonenumbers
- для сохранения результатов использую
OutputStorage
с простым интерейсом и реализацией на основе мультипроцессной очереди. (поскольку используется несколько процессов, см. ниже). Опять же, несложно заменить на запись в БД - такое деление на классы позволяет использовать
Downloader
достаточно гибко (можно приспособить под парсинг разнообразных данных) - сам
Downloader
- класс для загрузки данных по урлам изUrlsProvider
, парсингом их с помощьюParserBase
и записью результатов вOutputStorage
. Загрузка выполняется с использованиемasyncio/aiohttp
. Есть поддержка ограничения числа одновременных загрузок на каждый Downloader. Сессия там создается каждый раз новая (но без пула соединений), чтобы избежать общих cookies между запросами. Не уверен, что это актуально для этой задачи (переделать несложно), но как-то в похожей задаче меня банили по кукам из-за этого (но там было сильно больше запросов per site). - каждый
Downloader
реализован как отдельный процесс, у него есть свой идентификаторid
, который позволяет распределять урлы между разными инстансамиDownloader
' (распределяются на основе хэша). Зачем отдельный процесс? Потому что корутины помогают только при скачивании данных, потому что там долгие IO-операции. Если парсинг становится более сложным, то он может стать "узким горлышком" и поэтому нужны честные процессы для использования нескольких ядер процессора. Кроме того, с небольшими доработками это позволяет запустить программу на разных машинах