Пользователь
0,0
рейтинг
23 октября 2014 в 14:43

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

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

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

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

Установка


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

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

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


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

use SleepingOwl\Apist\Apist;

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) после загрузки данных заменяется на нужное значение, он сохраняет не только сам селектор, по которому он будет искать данные, но и всю вереницу вызовов, которые к нему были применены. После загрузки данных он пытается применить эти методы к найденным элементам.

Вот некоторые типы методов, которые могут быть применены (вы можете комбинировать их в нужной вам последовательности):
  • Методы класса Symfony\Component\DomCrawler\Crawler для перемещения по 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();
    // Вернет объект класса Symfony\Component\DomCrawler\Crawler, отвечающий за элемент 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 | Документация и примеры

Upd: в версии 1.2.0 добавилась возможность инициализировать апи из yaml файла, подробнее можно посмотреть в документации.
Sleeping Owl @sleeping-owl
карма
26,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (40)

  • –43
    У хабра есть свой удобный API c оберткой на php
    • +13
      Речь не о создании API хабра, а о библиотеке.
    • 0
      не в ту ветку написал! habrahabr.ru/post/241335/#comment_8085921
    • +13
      В данном случае хабр — всего лишь пример. Я использовал его как всем вам известный ресурс. В большинстве случаев библиотеку я использую для доступа к данным на маленьких локальных сайтах, которые были бы плохим примером.
    • +1
      Понял, был не прав.
  • 0
    ну хабр. тут для примера, и насколько я помню то у хабра. закрытое API и ключ выдается по запросу.

    У «Хабрахабра» есть непубличный API, доступ к которому предоставляется только по запросу через форму обратной связи.

    В обращении расскажите о себе и подробно опишите, для каких целей вы собираетесь использовать API.


    Извиняюсь промахнулся, это ответ на habrahabr.ru/post/241335/#comment_8085891
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      Для выбора элемента из dom-дерева используется «symfony/dom-crawler» и трансляцией css-селектора в xpath и выборкой элементов занимается эта библиотека. Моя же библиотека с dom-документом ни одной строкой кода не связана, основной целью имеет объединение этих селекторов в структуру, которую нужно получить на выходе.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Тут используется еще один уровень абстракции над dom-документом, который при этом не запрещает вам получить DomNode объект. Но для меня использование стандартных объектов для парсинга html выливается в десятки строк кода и сложную поддержку. Dom-crawler же позиционирует себя как некий аналог jQuery на php и намного проще позволяет модифицировать документ, а также получить полный html обратно. Я не пропагандирую, что это единственно верное решение, оно превносит ограничения в работу, но для большинства задач, с которыми я сталкивался, серьезно упрощает жизнь.
        • +7
          Например — готовую структуру для складывания данных в БД.
  • –6
    На всякий случай, вдруг кому-то пригодится, была статья на Хабре о сравнении скорости парсеров. И мои две копейки PHP+tidy+SimpleXML, в чем принципиальное отличие решения, предлагаемого автором, от этой связки не совсем понятно.
  • +4
    Отличная библиотека, буду пользоваться

    P.S.: Спасибо за Guzzle, не знал, использовал раньше свой велосипед
    • 0
      согласен. Особенно нормальная работа с куками. Курл так и не смог заставить нормально работать с сохранёнными сессиями, а тут сходу через GuzzleHttp\Cookie\CookieJar.
  • +2
    По мне, так лучше jsoup пока лучше ничего нет
  • +2
    В свое время активно пользовался phpquery.
  • +5
    Отличная библиотека.
    Комментаторы выше видимо не сталкивались с написанием сложным парсеров для 10 и более ресурсов, с условиями различными настройками зависящими от того что указал пользователь.

    Если кто-то ещё не въехал поясню — это не аналог и не замена xpath phpquery или каким-нибудь другим средствам работы с dom, автор да, добавил слой абстракции над парсерами для возможности чуть ли не динамической генерации api по работе с различными сайтами на основе каких-нибудь настроек из БД. Мне в нескольких проектах это очень поможет, сам хотел что-то подобное написать но на грамотную реализацию мозгов не хватило.
    • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    по сути просто обертка над селекторами, которые используются в PHP Simple HTML DOM Parser, который довольно удобен
    а что скажете о производительноти по сравнению с другими библиотеками?
    • 0
      habrahabr.ru/post/241335/#comment_8086091 — нет никаких других библитотек для сравнения.
      • 0
        к сожалению (или к счастью), я не вижу особой проблемы самостоятельно завернуть некий парсер в удобную для проекта обертку. вспоминается ситуация с «нужно создать еще один универсальный стандарт»
        я писал парсеры на php, node.js видел другие парсеры — все упиралось либо в использование библиотеки либо в написание регулярного выражения или xpath
        я не утверждаю, что библиотека не имеет права на существование, а говорю о том, что мы пишем те же селекторы (query, xpath, regexp), но в другом месте
        я сужу сугубо по статье
    • 0
      Запросы к апи в большинстве случаев не стоит делать в основном потоке, а выполнять в фоне и кэшировать. Поэтому для меня производительность не так важна, как удобство использования. А производительность у symfony/dom-crawler не хуже других php-парсеров, посколько все они основаны на трансляции css-селектора в xpath и в конечном итоге используют одни и те же методы.
  • +3
    Еще б допилить немного исполнение в более удобный и расширяемый вид

    Выделить getBaseUrl, чтобы не трогать протектед переменную, для построения фильтров вынести фабричный метод вместо статика, each сделать менее декларативным — дать возможность засунуть не только массив правил, но и коллбэк (в который передается каждая нода) — аналогично я бы все расширил коллбэками для постобработки того же text(). Т.е. надо манипулировать ситуациями, когда вложенность json и исходного html может совпадать, а когда нет (например, дату не запихнули в .post, а положили рядом дивом).

    Пр опостобработку еще вот что — существует много постоянно необходимых обработок (вроде убрать лишние пробелы внутри, вырезать теги, сделав br2nl) и прочее, что было бы хорошо иметь в ООП-стиле, а лучше и расширять собственными правилами.

    Плюс конструкция exist -> then -> else не особо нравится (и не хватает аналога для условного выражения).

    Плюс не заметил обработки хттп-ошибок — т.к. порой надо парсить и по 404 считать данные по странице отсутствующими.
    • 0
      Теперь я и сам вижу недостаток в расширяемости, буду развивать проект и постараюсь учесть комментарии. Коллбэки были в первоначальном варианте, но показались мне слишком громоздкими в описании и метод начинал превращаться в нечитаемую кашу. Попробую поискать достойное решение, чтобы и функционал не страдал, и удобство в использовании и поддержке тоже. Хттп-ошибки возвращают результат всегда одного вида (в документации есть пример) с урлом и статусом ошибки.
      • +1
        Посмотрите как это реализовано в Yii (в 2.0 не видел, в Yii 1 точно есть). Функция CController::evaluateExpression

        Позволяет передавать в качестве параметра строку с PHP кодом или замыкание
  • +2
    а бывает, чтобы получить нужный контент со страницы, надо там с нее определенный js выполнить
    А если не хочешь, то надо js парсить… Ну это, наверное, особо грустный случай.
    • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    • 0
      Скорее developer.yahoo.com/yql/
      Access Data Across the Web
      Select, filter, sort and join data across web services. You can even insert, update and delete from YQL.
      Easily Scrape HTML
      Scrape HTML from the web and turn it into data to reuse. Create an API where one doesn’t exist.
      и все это из любого языка и платформы
      • 0
        У них ограничение на кол-во запрсов
  • +7
    Недавно делал доклад по теме «Парсинг на PHP», где рассматриваю 5 разных способов достать информацию с сайта.

    Вот видео: www.youtube.com/watch?v=KZnrb6U2uw8
    Вот слайды: www.slideshare.net/pavelpolyakov58/think-php-10-parsing-with-php
    Вот код: github.com/PavelPolyakov/parsing-with-php

    Надеюсь кому-то понадобится :)
  • НЛО прилетело и опубликовало эту надпись здесь
  • –1
    вывести rest через sql-injection
  • 0
  • –1
    Спасибо. Просто спасибо.
    Кстати, есть ли поддержка регулярных выражений в селекторах?
    • 0
      Не совсем понимаю что для вас означают регулярные выражения в селекторах. Насколько мне известно в стандарте такого нет.
      • 0
        Имел ввиду некоторую пост обработку значений, из того, что было выбрано по селектору. Извините, поторопился и не дочитал статью про фильтры. То что нужно.
  • 0
    Подскажите, а как загрузить картинку?
    Речь идёт о капче, т.е. с теми же куками, что и обычный запрос.

    $this->get("/captha.png",[]);
    


    так не работает (и не должно похоже).
    • +2
      Обновил код. С версии 1.3.2 если не указана схема для парсинга, то вы получите исходный результат http-запроса:
      $this->get('/captcha.png');
      
      • 0
        Спасибо большое!

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