- PVSM.RU - https://www.pvsm.ru -

PHP парсинг от А до Я. Грабли и возможные решения

PHP парсинг от А до Я. Грабли и возможные решения - 1

Поскольку на Хабре после лавины публикаций «N в 30 строк» в 2013-м г. почти не публикуются материалы по программированию, то скажу, что этот материал для домохозяек, желающих научиться программированию, тестировщиков и просто неравнодушных людей.

В современном мире микросервисов господствует API. Но сервисов с каждым днем все больше, а API предоставляют далеко не все. Между тем данные сервисов могут быть весьма важны для анализа, бизнеса, копирования и т.д.

Посмотрим, как можно распарсить один известный сайт объявлений о продаже/аредне недвижимости максимально эффективно (и быстро), научимся обходить качпу и парсить мобильные приложения.

Внимание. Данный материал не является призывом к действию, носит информационный характер с целью повышения квалификации и увеличения качества работы тестировщиков.

Теория

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

PHP парсинг от А до Я. Грабли и возможные решения - 2

Всё это сферический конь в вакууме, и может быть не очень интересно, пока вдруг Вы не захотите прилично сэкономить, купив квартиру по методу линейной регрессии [1] и Вам понадобятся данные не в няшном виде на сайте, а в конкретной SQL-таблице. А для этого нужно собрать данные.

Парсим на PHP

Итак, мы на коне. У нас есть: страницы сайта с HTML-версткой, в которую примешаны искомые данные. Чтобы вычленить наши данные, можно использовать разные методы, которые ограничиваются только вашей фантазией и функциями PHP для работы со строками, т.к. полученная HTML-страница — всего лишь строка (string):

  • Связка mb_strpos и mb_substr будет работать быстрее всего, но съест у Вас большое количество нервов и и далеко не всегда подходит. Я использую этот метод, когда стоимость высокой скорости очень высока, а другие методы не дают нужного результата.
  • Функция preg_match и регулярные выражения — мощный инструмент, я всегда начинаю с него. Но потребляемые ресурсы линейно зависят от сложности регулярного выражения. А если представить, что вы пробегаетесь по строке размером 2Мб, становится понятно, одно объявление вытягивается со страницы за 2-3 секунды. В качестве костыля к этому и предыдущему методу можно предварительно уменьшить искомую область. Например, удалить head и footer. С помощью, конечно, mb_strpos.
  • А не проще просто оперировать с деревом HTML-документа, при помощи simplexml (или другого extension)? Да, конечно! Или просто пробежаться по HTML как по массиву:
    $xml = simplexml_load_string($xmlstring);
    $json = json_encode($xml);
    $array = json_decode($json,TRUE);
    

Хм. Я уже умею кодить на jQuery, можно мне селектировать дивы контейнеры простыми селекторами, а не писать $document['body']['div']['div']['div']? Да, можно.

phpQuery [2] — для тех кто хочет кодить на jQuery в PHP. И когда при помощи phpQuery вы получите одну строчку в вашей таблице, соответствующую одному объявлению, Вас закружит эйфория, Вы поставите Ваш скрипт парсить весь сайт, а тем временем пойдете в ближайшую пятёрочку покупать шампанское и будете мечтать о маленькой яхточке. Так пройдет ночь.

Суровая реальность

Наутро, придя к монитору, Вы увидите, что в таблице у Вас всего 2 тыс. строк, а скрипт отвалился с ошибкой Allowed Memory Size of… Bytes Exhausted…

Да, phpQuery, хоть и удобная штука, но память утекает. А скорость — самая низкая. Тогда Вам придется отказаться от удобной библиотеки и перейти на preg_match ради стабильности и скорости.

Ещё одни грабли — динамические селекторы. Запустив скрипт на следующий день, Вы поймете, что все классы изменились.

PHP парсинг от А до Я. Грабли и возможные решения - 3

Решение: вообще не привязываться к классам. Ниже пример, как можно вычленить из циановского объявления все некоторые параметры, не привязываясь к CSS-классам.


$paramsNames = '["Общая","Жилая","Кухня","Этаж","Год постройки","Материалы стен","Этажность","Подъездов","Квартир","Средняя цена за м<sup>2</sup>","Средний возраст домов","Средняя цена за м<sup>2</sup>","Население","Название","Налог на недвижимость"]';
if( preg_match_all( '#<div[^>]*>[^<]*<div[^>]*>[^<]*(' . implode( '|', $paramsNames ) . ')</div>[^<]*<div[^>]*>(.*?)</div>#ius', $html, $matches ) ){
     /* ...... */
}

Как видите, структура верстки всегда одна и та же. Поэтому нет смысла вообще привязваться к классам, зацепимся к названиям параметров. И так, Вы опять ставите скрипт на ночь.

Капча

Наутро Вы увидите, что скрипт доработал до конца, но на сайт показывает 50 тыс. объявлений, а у Вас примерно те же 2 тыс.

PHP парсинг от А до Я. Грабли и возможные решения - 4
Так выглядит капча ЦИАН

С капчей можно было бы посоревноваться при помощи ruCaptcha [3]. Но только не в случае с reCaptcha. Бросьте эту идею, она провальна. Весь наш метод прямых HTTP-запросов из PHP уперся в тупик.

Есть выход

Выход заключается в том, что мы не будем делать GET-запросы прямо из PHP, а запустим браузер, и через PHP заставим его открывать нужные страницы и читать их содержимое. Когда вылетит капча, мы просто её пройдем, и будем парситься дальше. Рассмотрим технологии, которые нам понадобятся.

Selenium Webdriver [4] — позволяет запускать браузер и управлять им по API (ссылка). Это инструмент тестировщиков, но мы ведь и тестируем, не правда ли? Обычно тестировщики используют Java. Но нам зачем эти танцы, если есть
php-webdriver [5] — инструмент от Facebook (спасибо, Цукерберг!), предоставляющий SDK на PHP для Selenium webdriver.

Мне это интересно

Selenium Webdriver дружит не со всеми браузерами. Лично я поставил Windows XP на Oracle VirtualBox. На неё накатил Selenium и Хром. Когда нужно — запускаю виртуальную машину, и связка всегда работает. Так выглядит работающая консоль Selenium, где выводится служебная информация.

PHP парсинг от А до Я. Грабли и возможные решения - 5

Вот как мы откроем главную страницу ЦИАН и нажмем на кнопку «Найти»


# используем хром как рабочую лошадку
$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);

$driver = RemoteWebDriver::create('192.168.1...:4444/wd/hub', $capabilities, 5000);
$driver->manage()->timeouts()->implicitlyWait(10);

# открываем сессию и сразу открываем страницу
$remote = $driver->get('https://cian.ru/');

# ищем по CSS-селектору. Да, это почти что jQuery
$remote->findElement(WebdriverBy::cssSelector('.c-filters-field-button___1EBB-'))->click();

В примере я просто вставил динамический CSS-селектор. Но так делать нельзя, нужно сначала узнать класс у кнопки «Найти», предварительно получив все содержимое в HTML, потом программно подставить. Получить содержимое страницы из Selenium можно так:

$content = $remote->findElement(WebdriverBy::cssSelector('body'))->getAttribute('innerHTML');

И вот, Вы один раз ввели капчу, наслаждаетесь потоком приятных строк, наполняющих Вашу таблицу, параллельно тестируя ЦИАН. Вам нравится процесс, хотя он идет и не очень быстро, но главное — идет! И Вы уходите спать.

Грабли

А наутро приходите, и обнаруживаете, что парсинг закончился, а в таблице стало чуть больше объявлений. Всего 3 тыс. Скрипт не может вызвать Selenium (не достучаться). Что происходит?!

А вот что.

Открывая страницу через $driver->get, Вы заполняете location history у браузера. Открывая следующую в текущей сессии, предыдущая из памяти не стирается. И браузер просто пухнет в оперативной памяти, пока не произойдет коллапс. Выход — прерывать сессию через $driver->close. Но тогда можно опять нарваться на капчу…

А что если...

И так до бесконечности. Скорость очень низкая, Selenium чересчур, постоянные срывы парсинга… Нужно искать что-то другое. Да, у сервиса бесплатных объявлений, который мы тестируем, есть ещё мобильное приложение. А значит, есть и публичное API. Нужно только добраться до URL-ов API, и успех у нас в кармане! Делать это будем, конечно, через Android.

Прослушивать трафик, чтобы протестировать мобильное приложение, можно, но сложно мало эффективно. Инструкция, как это сделать [6], уже есть. Гораздо более школьный метод просто установить SSL Capture [7].

Мне это интересно

PHP парсинг от А до Я. Грабли и возможные решения - 6
Так выглядит интерфейс приложения одного из бесплатных сервисов объявлений недвижимости
PHP парсинг от А до Я. Грабли и возможные решения - 7
Так выглядит запрос на получение общего количества объявлений. Да-да, это на Android.

Для того, чтобы распарсить весь тестируемый сайт (сервис), нужен всего-лишь один метод API. Вот он: api.cian.ru/search-offers/v1/search-offers-for-mobile-apps [8]

И в завершение прикреплю функцию (код), при помощи которой Вы сможете уже через час получить весь массив данных. Никаких (!!!) ограничений на запросы к мобильному API нет.

Вот она


	$request = [
		'query' => [
			'engine_version' => [
				'type' => 'term',
				'value' => '2'
			],
			'limit' => [
				'type' => 'term',
				'value' => 50
			],
			'object_type' => [
				'type' => 'terms',
				'value' => [0]
			],
			'page' => [
				'type' => 'term',
				'value' => $page
			],
			'_type' => 'flatsale'
		]
	];

	$body = json_encode( $request );

	$ch = curl_init();

	curl_setopt($ch, CURLOPT_URL, "https://api.cian.ru/search-offers/v1/search-offers-for-mobile-apps/?new_schema=1&multioffer_version=3" );
	curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 );
	curl_setopt($ch, CURLOPT_POST,           1 );
	curl_setopt($ch, CURLOPT_HEADER,           0 );
	curl_setopt($ch,CURLOPT_ENCODING , "gzip");
	curl_setopt($ch, CURLOPT_HTTPHEADER, [
	'Authorization: simple ....',
	'Os: android',
	'BuildNumber: ',
	'VersionCode: 20630300',
	'Device: Phone',
	'ApplicationID: ....',
	'User-Agent: Cian/ (Android; ....; Phone; ....)',
	'Content-Type: application/json; charset=UTF-8',
	'Content-Length: ' . mb_strlen($body),
	// 'Host: api.cian.ru',
	'Connection: Keep-Alive',
	'Accept-Encoding: gzip',
		]);

	curl_setopt($ch, CURLOPT_POSTFIELDS,     $body ); 

	$result=json_decode( curl_exec ($ch), true );

Вместо точек нужно подставить свои данные авторизации. После этого в $result['data']['offers'] будет абсолютно вся информация по каждому объявлению в списке.

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

Используемые инструменты:

phpQuery [2]
Selenium Webdriver [4]
php-webdriver [5]
Oracle VirtualBox [9]

Автор: qravits

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/php-2/303710

Ссылки в тексте:

[1] купив квартиру по методу линейной регрессии: https://habr.com/post/148782/

[2] phpQuery: https://code.google.com/archive/p/phpquery/

[3] ruCaptcha: https://rucaptcha.com

[4] Selenium Webdriver: https://www.seleniumhq.org/projects/webdriver/

[5] php-webdriver: https://github.com/facebook/php-webdriver

[6] как это сделать: https://habr.com/company/infopulse/blog/156711/

[7] SSL Capture: https://play.google.com/store/apps/details?id=com.minhui.networkcapture

[8] api.cian.ru/search-offers/v1/search-offers-for-mobile-apps: https://api.cian.ru/search-offers/v1/search-offers-for-mobile-apps/

[9] Oracle VirtualBox: https://www.virtualbox.org

[10] Источник: https://habr.com/post/434754/?utm_campaign=434754