Как использовать API сайта, у которого нет API?

в 10:43, , рубрики: api, dom-crawler, php, Веб-разработка

У меня достаточно часто появляется задача получить данные от стороннего сайта, при этом далеко не всегда этот сайт предоставляет возможность удобно получить эти данные через API. Единственное решение в таком случае — парсить html содержимое страниц. Когда-то я писал регэкспы, потом появились библиотеки, позволяющие получить нужное содержимое по css-селектору, а сейчас и это кажется сложной задачей, которую хотелось бы упростить.

Сегодня я хочу рассказать вам о моей небольшой библиотеке, позволяющей описать в API-стиле http-запросы и парсить ответ сервера в нужный вам формат.

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

Установка

Библиотека доступна к установке через composer, поэтому все, что необходимо сделать — это добавить зависимость «sleeping-owl/apist»: «1.*» в ваш composer.json и вызвать composer update.

У данной библиотеки нет зависимостей от каких-либо фреймворков, поэтому вы можете использовать ее с любым фреймворком, либо же в чистом PHP-проекте. Для сетевых запросов используется Guzzle, для манипуляций с dom-деревом используется «symfony/dom-crawler».

Использование

После установки вы можете приступить к созданию нового класса, олицетворяющего API нужного вам сайта. Библиотека не накладывает никаких ограничений на то, как и где вы будете создавать свой класс. Нужно расширить класс SleepingOwlApistApist и указать базовый урл:

use SleepingOwlApistApist;

class HabrApi extends Apist
{
	protected $baseUrl = 'http://habrahabr.ru';
}

Это все, что нужно для базового описания. Далее вы можете добавлять в данный класс методы, которые вам нужны:

public function index()
{
	return $this->get('/', [
		'title' => Apist::filter('.page_head .title')->text()->trim(),
		'posts' => Apist::filter('.posts .post')->each([
			'title'      => Apist::filter('h1.title a')->text(),
			'link'       => Apist::filter('h1.title a')->attr('href'),
			'hubs'       => Apist::filter('.hubs a')->each(Apist::filter('*')->text()),
			'author'     => [
				'username'     => Apist::filter('.author a'),
				'profile_link' => Apist::filter('.author a')->attr('href'),
				'rating'       => Apist::filter('.author .rating')->text()
			]
		])
	]);
}

Здесь метод «get» — это тип используемого http-запроса, также доступны остальные методы (post, put, patch, delete и т.д.).
Первый параметр — урл данного метода, он может быть как относительным, так и абсолютным.
Второй параметр — это и есть та основа, из-за которой я создал эту библиотеку. Он описывает структуру, которую необходимо получить в результате вызова данного метода. Это может быть как массив, так и одиночное значение. То есть для описанного выше метода результат будет такого вида:

$api = new HabrApi;
$result = $api->index();

Примечание: результат будет типа array, json-формат здесь использован для удобства.

{
    "title": "Публикации",
    "posts": [
        {
            "title": "Проверьте своего хостера на уязвимость Shellshock (часть 2)",
            "link": "http://habrahabr.ru/company/host-tracker/blog/240389/",
            "hubs": [
                "Блог компании ХостТрекер",
                "Серверное администрирование",
                "Информационная безопасность"
            ],
            "author": {
                "username": "smiHT",
                "profile_link": "http://habrahabr.ru/users/smiHT/",
                "rating": "26,9"
            }
        },
        {
            "title": "Курсы этичного хакинга и тестирования на проникновение от PentestIT",
            "link": "http://habrahabr.ru/company/pentestit/blog/240995/",
            "hubs": [
                "Блог компании PentestIT",
                "Учебный процесс в IT",
                "Информационная безопасность"
            ],
            "author": {
                "username": "pentestit-team",
                "profile_link": "http://habrahabr.ru/users/pentestit-team/",
                "rating": "36,4"
            }
        },
        ...
    ]
}

Третьим опциональным параметром могут идти любые дополнительные параметры запроса, get или post переменные, загружаемые файлы, заголовки запроса и т.п. С полным списком можно ознакомиться в документации Guzzle.

Создание фильтров

Пара слов о том, как это работает: каждый объект, созданный через Apist::filter($cssSelector) после загрузки данных заменяется на нужное значение, он сохраняет не только сам селектор, по которому он будет искать данные, но и всю вереницу вызовов, которые к нему были применены. После загрузки данных он пытается применить эти методы к найденным элементам.

Вот некоторые типы методов, которые могут быть применены (вы можете комбинировать их в нужной вам последовательности):

  • Методы класса SymfonyComponentDomCrawlerCrawler для перемещения по dom-дереву и получению данных:
    Apist::filter('.navbar li')->eq(3)->filter('a.active')->text();
    Apist::filter('input')->first()->attr('value');
    Apist::filter('.content')->html();
    

  • Созданные мной методы:
    Apist::filter('body')->element();
    // Вернет объект класса SymfonyComponentDomCrawlerCrawler, отвечающий за элемент body
    
    Apist::filter('.post')->each(...);
    // Этот объект будет заменен на массив, каждый элемент которого будет создан согласно схеме, которая была передана параметром. Все внутренние css-селекторы будут применены относительно текущего элемента.
    
    Apist::filter('.errors')->exists()->then(...)->else(...);
    // Описывает условие, если элемент с классом "errors" был найден, то используется значение из блока "then", иначе из блока "else"
    

  • PHP-функции или ваши функции, описанные в корневом namespace. При этом текущий элемент будет передан в качестве первого параметра, а остальными параметрами будут те, что вы указали при инициализации.
    Apist::filter('.title')->text()->mb_strtoupper()->trim()->substr(5);
    
    function myFunc($string, $find, $replace)
    {
    	return str_replace($find, $replace, $string);
    }
    Apist::filter('.title')->text()->myFunc('My', 'Your');
    // Если убрать ->text(), то в функцию будет передан объект, а не строка. Это можно использовать в своих целях при необходимости.
    

Исходники демо-класса HabrApi.php, используемого в примерах на сайте проекта можно посмотреть здесь.

Исходники на GitHub | Документация и примеры

Автор: sleeping-owl

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js