Еще один кейс на PHP. Скачиваем базу данных из открытого API

Решать задачу будем на примере каталога исторических экспонатов Музейного фонда РФ. API каталога является общедоступным сервисом. Сначала нам нужно будет сформировать ссылку на скачивание архива ZIP с информацией. Сделать это нужно аккуратно, так, чтобы изменения в названиях файлов, которые могут время от времени случаться, не повлияли на работоспособность нашего скрипта.

Затем, мы создадим шакалу загрузки, или прогресс-бар, как ее еще называют, чтобы отслеживать процесс скачивания. Экспонатов много, каталог большой: «весит» он более 5 Гб. Без индикации загрузки пользователь может недоумевать, почему ничего не происходит и консоль просто висит долгое время. Наконец, скачаем файл из API, который потом можем разобрать по таблицам своей базы в том виде, в котором понадобится.

1.     «Побеждаем» html

Структура файлов нашего проекта будет типичной. Потребуется класс Downloader.php. Там пропишем основную логику скачивания архива из API, а также index.php, в котором будем запускать методы из этого и других классов. Также будет необходим config-файл, в котором мы будем аккумулировать информацию о переменных параметрах нашего приложения, которые могут поменяться.

Из Downloader.php придется периодически наведываться в «конфиг», чтобы получить тот или иной входной параметр. Обратите внимание на папку museumData, туда мы будем сохранять нашу базу данных из API. Воспроизведите, пожалуйста, структуру ниже у себя.

Скриншот №1     

2a6858f42c5f10b0d501a6fa3c17e6f8.png

API каталога Музейного фонда расположено на обычной html-странице сайта opendata.mkrf.ru. Это типично для многих сервисов с открытыми данными. В таких случаях нам нужно «распарсить» веб-страницу источника: получить страницу для обработки, проследовать по иерархии тегов и найти тот узел, где находится название базы данных для скачивания. После этого мы склеиваем url, где выложена база с названием файла, после чего у нас есть все необходимое, чтобы скачать к себе в папку базу средствами PHP.

1.1        Выбираем библиотеку для работы с DOM, то есть с разметкой документов в сети. Мы будем пользоваться библиотекой DOMXpath, потому что это универсальный и надежный инструмент. Она хорошо «дружит» с PHP. Xpath представляет собой синтаксис для работы с DOM и является встроенным модулем PHP. Библиотека DOM обычно встроена в сборки серверов, .dll файл с ней поставляется уже в php-дистрибутиве. DOMXpath, фактически, расширение для DOM. Проверьте, может быть DOMXpath у вас тоже уже подключен? Если даже нет, то установить его обычно не составляет никакого труда. Если у вас локальный сервер на основе WSL+ubuntu, например, то нужно поставить php-xml одной командой «sudo php-xml». Альтернативный DOMXpath вариант — специальные сторонние библиотеки, например «PHP Query». У всех альтернатив свои преимущества, но, на мой взгляд, воспользоваться DOMXpath проще всего.

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

/config.json

{

  "source": {

    "link" : "https://opendata.mkrf.ru/opendata/7705851331-museum-exhibits/"

  },

  "database_saving_folder" : "/museumData/"

}

1.3        Теперь в индексном файле подключаем класс Downloader.php и любые другие классы на будущее, с которыми нам понадобится работать.

/index.php

spl_autoload_register(function ($classRequired) {

    include $classRequired . '.php';

});

1.4        Прочитываем файл config.json и кладем содержимое в суперглобальную переменную, чтобы путь к API и другие данные были для нас в досягаемости по всей папке, в которой «обосновался» наш загрузчик:

/index.php

$_ENV["conf"] = json_decode(file_get_contents(__DIR__ . "/config.json"), true);

1.5        Создаем в файле Downloader.php класс Dwonloader и заводим в нем одну переменную и один публичный метод.

/Downloader.php

class Downloader

{

    private string $fileName;

    public function searchSourceFileOnWebPage()

    {

    }

Из-под этого единственного публичного метода будут запускаться все другие закрытые методы загрузчика. Мне кажется такой способ удобным и безопасным. В переменной $fileName будем хранить название файла с базой, которую получим из html.

1.6        Подготовительные действия закончились, теперь работаем с самими html страницы «https://opendata.mkrf.ru/opendata/7705851331-museum-exhibits/»:

https://opendata.mkrf.ru/opendata/7705851331-museum-exhibits/

Заводим следующий приватный метод разборки html:

/Downloader.php

private function findZipWithXPath(): void

    {

        $dom = new DOMDocument();

        $dom->loadHTML(file_get_contents($_ENV["conf"]["source"]["link"]));

        $xpath = new DOMXpath($dom);

        $nameOfFileWithDatabase = $xpath->query('//div[contains(@class,"download_btn")]/@data-name');

        $this->fileName = $nameOfFileWithDatabase["length"]->nodeValue;

print_r($nameOfFileWithDatabase["length"]->nodeValue);

    }

 Создаем объект DOMDocument в соответствии с синтаксисом библиотеки и загружаем в нее веб-страницу с базой — метод loadHTML. Как видите, с помощью созданной ранее библиотеки получаем ссылку. Если когда-нибудь ссылка изменится, нам нужно будет просто отредактировать конфиг-файл. Все остальное будет работать. Далее отправляем xpath запрос к атрибуту «data-name» класса «download_btn». Обратите внимание, какой короткий и простой Xpath запрос. Больше о синтаксисе можно узнать в документации Xpath. Исходный html, который мы разбираем, выглядит так:

Скриншот №2

abfad9b0c7b7fa2ea549c72bcd40b9c5.png

Найденное Xpath запросом передается в виде DOM-дерева, доступного по ключу «length». И тут мы подходим к вопросу о том, что значит ($nameOfFileWithDatabase[«length»]→nodeValue. DOMXpath устроена таким образом, что передает найденный узел с возможностью проследовать к дочерним элементам, к родительским, получить текст тега, его атрибут или отображаемый тегом текст — наш случай, –, а также любые другие элементы html-верстки. Если открыть элементы, доступные по [«length»], то видим:

Скриншот №3

16532cc69c5904b7ad13ed9a5c5fa1c9.png

В dom-дереве узлы называются «нодами», «nodeValue» — значение ноды, то есть того, что находится между открывающими и закрывающими тегами и того, что мы видим при просмотре веб-страницы.

1.7        Нас интересует имя базы «data-4-structure-3», которое мы успешно и получили в переменную $nameOfFileWithDatabase. Чтобы убедиться в этом, достаточно просто проверить метод на этом этапе. Так как он приватный, вызвать мы его сможем только через посредничество публичной функции searchSourceFileOnWebPage (), которую ранее создали. Делаем вызов:

/Downloader.php

class Downloader

{

    private string $fileName;

    public function searchSourceFileOnWebPage()

    {

        $this->findZipWithXPath();

    }

Теперь нам надо запустить searchSourceFileOnWebPage () в index.php, ведь стартовать наш скрипт будет через индексный файл.

/index.php

spl_autoload_register(function ($classRequired) {

    include $classRequired . '.php';

});

$_ENV["conf"] = json_decode(file_get_contents(__DIR__ . "/config.json"), true);

$fileWithData = new Downloader();

$fileWithData->searchSourceFileOnWebPage();

 Вот так он у нас теперь выглядит со всеми изменениями. Запускаем скрипт, получаем «data-4-structure-3», то есть именно то, что нужно. Последнее, что нам остается сделать на этом этапе — это сохранить результат в свойство класса private string $fileName.

2.     Качаем базу и ждем, когда наш прогресс-бар покажет, что все готово.

У нас к настоящему моменту есть url и имя файла, который нужно скачать. Это все, что нужно для функции file_put_contents (), которая загрузит в нашу папку museumData базу данных. С другой стороны, учитывая 5 Гб архива, грузиться она будет долго, поэтому нам хорошо бы отслеживать статус загрузки.

Большинство примеров создания прогресс-баров основано на том, что с помощью цикла скачивается несколько файлов. И шкала загрузки складывается из общего числа файлов и пройденных итераций. Но как быть, если скачивается один файл? Придется встроиться в поток PHP, с помощью готовой callback функции stream_notification_callback. Итак, в переменную $path кладем путь до папки museumData, куда мы хотим сохранить базу данных, а в $url — путь на скачивание файла.

Детали работы функции stream_notification_callback — отдельная тема, примерно в таком виде, как здесь, прогресс-бар можно встроить в любое скачивание. Для этого не нужно понимать логику работы функции. Прогресс-бар для консоли, показывающий загрузку прописан после STREAM_NOTIFY_PROGRESS. В переменную $bytes_max попадает общий объем скачиваемого файла, а $bytes_transferred фиксирует, сколько загрузилось байтов от начала загрузки.  

/Downloader.php

  private function getFileInOurFolder ()

    {

        $path = $_ENV[«conf»][«database_saving_folder»];

        $url = $_ENV[«conf»][«source»][«link»] . $this→fileName.».json.zip»;

        function stream_notification_callback ($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max)

        {

            switch ($notification_code) {

                case STREAM_NOTIFY_RESOLVE:

                case STREAM_NOTIFY_AUTH_REQUIRED:

                case STREAM_NOTIFY_COMPLETED:

                case STREAM_NOTIFY_FAILURE:

                case STREAM_NOTIFY_AUTH_RESULT:

                    break;

                case STREAM_NOTIFY_REDIRECTED:

                    echo «Перенаправлены на:», $message;

                    break;

                case STREAM_NOTIFY_CONNECT:

                    echo «Подсоединились…»;

                    break;

                case STREAM_NOTIFY_FILE_SIZE_IS:

                    echo «Получили размер файла:», $bytes_max;

                    break;

                case STREAM_NOTIFY_MIME_TYPE_IS:

                    echo «Получили mime-тип файла:», $message;

                    break;

                case STREAM_NOTIFY_PROGRESS:

                    if ($bytes_transferred > 0) {

                        $perc = round (($bytes_transferred * 100) / $bytes_max);

                        $bar = round ((50 * $perc) / 100);

                        echo sprintf (»\r%s%% [%s>%s] %s», $perc, str_repeat (»=», $bar), str_repeat (» », 50 — $bar), $info = 'каталог Музейного фонда РФ скачивается, подождите');

                    }

                    break;

            }

            echo »\n»;

        }

        $ctx = stream_context_create();

        stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));

        file_put_contents(__DIR__ . $path . 'database.json.zip', fopen($url, 'r', true, $ctx));

    }

}      

Чтобы заставить наш прогресс-бар «прослушивать» скачивание, достаточно просто поместить переменную $ctx параметром функции file_put_contents (). Запускаем приватный метод getFileInOurFolder () через публичный searchSourceFileOnWebPage ():

/Downloader.php

class Downloader

{

    private string $fileName;

    public function searchSourceFileOnWebPage()

    {

        $this->findZipWithXPath();

        $this->getFileInOurFolder();

    }

Теперь мы можем видеть, как происходит наша загрузка.

Скриншот №4

2ccb909854f98a034403b33e0a0f6199.png

Ждем немного и получаем результат.

Скриншот №5

abad582e6c456a9252d4bf2eea78302a.png

А вы скачивали базы данных архивами из API, как вы решили задачу? Поделитесь, пожалуйста, в комментариях!

Валентин Рахманов, руководитель отдела разработки сайта «Инглиш Форсаж»

© Habrahabr.ru