Для того, чтобы спарсить страницу сайта (то есть разобрать ее HTML код), ее для начала следует получить. А затем уже полученный код можно разобрать с помощью регулярных выражений и, либо каким-то образом его проанализировать, либо сохранить в базу данных, либо и то, и другое.

Получение страниц сайтов с помощью file_get_contents

Итак, для начала давайте поучимся получать страницы сайтов в переменную PHP. Это делается с помощью функции file_get_contents, которая чаще всего используется для получения данных из файла, однако, может быть использована для получения страницы сайта - если передать ей параметром не путь к файлу, а url страницы сайта.

Учтите, что эта функция не идеальна и существует более мощный аналог - библиотека CURL, которая позволяет работать с куками, с заголовками, позволяет отправлять формы и переходить по редиректам. Все это file_get_contents делать не умеет, однако для начала нам сойдет и она, а работу с CURL мы разберем в следующем уроке.

Итак, давайте для примера получим главную страницу моего сайта и выведем ее на экран (сделайте это):

<?php
	echo file_get_contents('http://theory.phphtml.net');
?>

Что вы получите в результате: у себя на экране вы увидите страницу моего сайта, однако, скорее всего без CSS стилей и картинок (будут ли работать CSS и картинки - зависит от сайта, почему так - разберем попозже).

Давайте теперь выведем не страницу сайта, а ее исходный код. Запишем его в переменную $str и выведем на экран с помощью var_dump:

<?php
	$str = file_get_contents('http://theory.phphtml.net');
	var_dump($str);
?>

Учтите, что var_dump должен быть настроен корректно в конфигурации PHP (см. предыдущий урок для этого). Корректно - это значит вы должны видеть теги и не должно быть ограничения на длину строки (код страницы сайта может быть очень большим и желательно видеть его весь).

Итак, если все сделано хорошо, и вы видите исходный код страницы сайта - самое время приступить к его парсингу с помощью регулярных выражений.

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

Должна быть включена директива allow_url_fopen http://php.net/manual/ru/filesystem.configuration.php#ini.allow-url-fopen

Парсинг с помощью регулярных выражений

При попытке разобрать HTML код с помощью регулярных выражений вас будут ждать некоторые подводные камни. Их наличие чаще всего связано с тем, что регулярные выражения не предназначены для разбора тегов - для этого есть более продвинутые инструменты, например библиотека phpQuery, которую мы будем разбирать в следующих уроках.

Однако, уметь использовать регулярные выражения для парсинга тоже важно - во-первых, регулярки это простой (если вы их уже знаете - то простой) и популярный инструмент для парсинга, во-вторых, регулярки работают на порядок быстрее, чем любые библиотеки (часто это критично), ну, и в-третьих, даже при использовании специальных библиотек нужда в регулярках все равно есть.

Подводные камни

Первая неожиданность, которая ожидает вас при использовании preg_match и preg_match_all - это то, что они работают только для тегов, целиком расположенных на одной строке (то есть, в них нету нажатого энтера). Если попытаться спарсить многострочный тег - у вас ничего не получится, пока вы не включите однострочный режим с помощью модификатора s. Вот таким образом:

<?php
	preg_match_all('#регулярка#s', $str, $res);
	var_dump($res);
?>

Вторая неожиданность ждет вас, когда вы попробуете поработать с кириллицей - в этом случае нужно не забыть написать модификатор u (u маленькое, не путать с большим), вот так:

<?php
	preg_match_all('#регулярка#u', $str, $res);
	var_dump($res);
?>

Таким образом я рекомендую вам всегда работать с этими двумя модификаторами, вот так:

<?php
	preg_match_all('#регулярка#su', $str, $res);
	var_dump($res);
?>

Какие еще подводные камни вас ждут - будем разбирать постепенно в течении данного урока.

Попробуем разобрать теги

Пусть мы каким-то образом (например, через file_get_contents) получили HTML код сайта. Вот он:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Это заголовок тайтл</title>
	</head>
	<body>
		Это основное содержимое страницы.
	</body>
</html>

Давайте займемся его разбором. Для начала давайте получим содержимое тега <title>, тега <head>, и тега <body>.

Итак, получим содержимое тега <title> (в переменной $str хранится HTML код, который мы разбираем):

<?php
	preg_match_all('#<title>(.+?)</title>#su', $str, $res);
	var_dump($res);
?>

Содержимое <head>:

<?php
	preg_match_all('#<head>(.+?)</head>#su', $str, $res);
	var_dump($res);
?>

Содержимое <body>:

<?php
	preg_match_all('#<body>(.+?)</body>#su', $str, $res);
	var_dump($res);
?>

В общем-то ничего сложного нет, только обратите внимание на то, что как уголки тегов, так и слеш от закрывающего тега экранировать не надо (последнее верно, если ограничителем регулярки является не слеш /, а, например, решетка #, как у нас сейчас).

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

Что же у нас не так? На самом деле тег <body> - такой же тег, как и остальные и в нем вполне могут быть атрибуты. Чаще всего это атрибут class, но могут быть и другие (например, onload для выполнения JavaScript).

Итак, перепишем регулярку с учетом атрибутов:

<?php
	preg_match_all('#<body.+>(.+?)</body>#su', $str, $res);
	var_dump($res);
?>

Но и здесь мы ошиблись, при чем ошибок несколько. Первая - следует ставить не плюс +, а звездочку *, так как плюс предполагает наличия хотя бы одного символа - но ведь атрибутов в теге может и не быть - и в этом случае между названием тега body и уголком не будет никаких символов - и наша регулярка спасует (не понятно, что я тут написал - учите регулярки).

Поправим эту проблему и вернемся к дальнейшему обсуждению:

<?php
	preg_match_all('#<body.*>(.+?)</body>#su', $str, $res);
	var_dump($res);
?>

Вторая проблема следующая: если внутри <body> будут другие теги (а так оно и будет в реальной жизни) - то наша регулярка <body.*> зацепит лишнего. Например, рассмотрим такой код:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Это заголовок тайтл</title>
	</head>
	<body class="www">
		<p>Абзац</p>
	</body>
</html>

Регулярка <body.*> найдет не <body class="www">, как ожидалось, а <body class="www"><p>Абзац{

} - потому что мы не ограничили ей жадность. Сделаем это: место <body.*> напишем <body.*?> - в этом случае будет все хорошо.

Но более хорошим вариантом будет написать вместо точки конструкцию [^>] (не закрывающий уголок), вот так - <body[^>]*?> - в этом случае мы полностью застрахуем себя от проблем такого рода, так как регулярка никогда не сможет выйти за тег.

Получение блока по id

Давайте рассмотрим следующий код:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Это заголовок тайтл</title>
	</head>
	<body class="www">
		<div id="content">Контент</div>
		<div>Еще див</div>
	</body>
</html>

Напишем регулярку, которая получит содержимое блока с id, равным content.

Итак, попытка номер один (не совсем корректная):

#<div\sid="content">(.+?)</div>#su

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

Все проблемы такого рода существенны - даже если ваша регулярка разбирает одну страницу сайта - это не значит, что она разберет другую подобную страницу: на ней вполне вокруг равно в атрибуте id могли поставить пробелы - и тут ваша регулярка спасует.

Поэтому, регулярки парсера нужно строить так, чтобы они обходили как можно больше проблем - в этом случае ваш парсер будет работать максимально корректно на всех страницах сайта, а не только на тех, которые вы проверили.

Давайте поправим нашу регулярку:

#<div\s+?id\s*?=\s*?"content">(.+?)</div>#su

Обратите внимание на то, что вокруг равно пробелы могут быть, а могут и не быть, поэтому там стоит оператор повторения звездочка *.

Кроме того, перед закрывающем уголком тега тоже могут быть пробелы (а могут и не быть) - учтем и это:

#<div\s+?id\s*?=\s*?"content"\s*?>(.+?)</div>#su

Итак, уже лучше, но еще далеко не идеал - ведь вокруг атрибута id могут быть и другие атрибуты, например так: <div class="www" id="content" onclick="">. В этом случае наша регулярка спасует. Давайте укажем, что могут быть еще и другие атрибуты:

#<div.+?id\s*?=\s*?"content".*?>(.+?)</div>#su

Обратите внимание, что после <div стоит регулярка .+?, а перед > стоит регулярка .*? - это не ошибка, так и задумано, ведь после <div обязательно должен идти пробел (то есть хотя бы один символ точно будет), а перед > может вообще не быть других атрибутов (кроме нашего id) и пробела тоже может не быть.

Регулярка стала еще более хорошей, но есть проблема: лучше не использовать точку в блоках типа .*? - мы вполне можем хватануть лишнего выйдя за наш тег (помните пример выше с body?). Лучше все-таки использовать [^>] - это гарантия безопасности:

#<div[^>]+?id\s*?=\s*?"content"[^>]*?>(.+?)</div>#su

Следующая проблема: кавычки-то в атрибутах могут быть как одинарными, так и двойными (их даже может вообще не быть, если значение атрибута - одно слово, но этот случай редкий - не будем его учитывать, если вам встретится такой сайт - проще написать регулярку специально для него). Итак, учтем это:

#<div[^>]+?id\s*?=\s*?["\']content["\'][^>]*?>(.+?)</div>#su

Обратите внимание на то, что одинарная кавычка заэкранирована - мы это делаем, так как внешние кавычки от строки PHP у нас тоже одинарные, вот тут:

<?php
	preg_match_all('#кавычка внутри экранируется \' - вот так#su', $str, $res);
?>

В общем-то регулярка достаточно хороша, но иногда идут дальше и делают так, чтобы первая кавычка от тега совпадала со второй (исключаем вариант id="content'). В этом случае делают так - первая кавычка ложится в карман, а вторая кавычка указывается карманом, чтобы совпадала с первой:

#<div[^>]+?id\s*?=\s*?(["\'])content\1[^>]*?>(.+?)</div>#su

Для нашей задачи это особо не нужно (можно быть точно уверенным, что такое id="content' - врядли где-то будет), но есть атрибуты, где это существенно. Например, в таком случае: <div title="Рассказ о д'Артаньяне"> - в атрибуте title вполне может затесаться одинарная кавычка и регулярка title\s*?=\s*?["\'](.+?)["\'] вытянет текст "Рассказ о д" - потому что поиск ведется до первой кавычки.

А вот регулярка title\s*?=\s*?(["\'])(.+?)\1 будет корректно обрабатывать <div title="Рассказ о д'Артаньяне"> и даже <div title='Рассказ о д"Артаньяне'>.

Проблема вложенных блоков

В нашей регулярке есть еще одна проблема - она не может работать с вложенными блоками. Например, если внутри дива #content есть еще один див - регулярка найдет текст до первого закрывающего </div>, а не для закрывающего дива для #content. Пример проблемного кода:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Это заголовок тайтл</title>
	</head>
	<body class="www">
		<div id="content">
			<div>Див внутри контента</div>
			Контент
		</div>
		<!-- end of content -->
		<div id="footer">Футер</div>
	</body>
</html>

Наша регулярка вытянет только <div id="content"><div>Див внутри контента</div> - остановится на первом же </div>. Что делать в этом случае?

Что делать в этом случае? Во-первых, к этому случаю всегда нужно быть готовым - даже если на исследуемых страницах сайта нет вложенных блоков - они вполне могут быть и на других страницах или появиться потом (если сайт парсится не один раз, а периодически).

Ну, а что делать - нужно просто привязываться не к </div>, а к тому, что стоит под нашим блоком (в нашем случае под контентом). В приведенном ниже коде под ним стоит <div id="footer"> - можно привязаться к нему или к <!-- end of content --> - и так, и так будет хорошо.

В HTML5 появились новые теги - header, footer, main (для контента) - с ними работать гораздо удобнее, ведь в них исключена вложенность. И очень часто вместо <div id="content"> можно увидеть просто <main> - и парсинг становится проще.

Вытягиваем заданные блоки

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Это заголовок тайтл</title>
	</head>
	<body>
		<p>Абзац 1</p>
		<p>Абзац 2</p>
		<p>Абзац 3</p>
		<p>Абзац 4</p>
		<p>Абзац 5</p>
	</body>
</html>

Получение href ссылок

Ссылки из блока

Получение элементов по классу

Кодировка документа

Иногда вам придется парсить не современные сайты, а достаточные старые. На таких сайтах кодировка чаще всего установлена в windows-1251. Поэтому, если вы попытаетесь получить русскоязычные тесты с этого сайта, вы вместо русских букв увидите вопросики - это первый признак сбившейся кодировки.

В этом случае следует воспользоваться функцией iconv, которая перекодирует текст из устаревшего windows-1251 в современный utf-8:

<?php
	$str = getPageByUrl('http://theory.phphtml.net');
	$str = iconv('windows-1251', 'utf-8', $str);
?>

Как понять по HTML коду сайта, что в нем не та кодировка? Посмотрите на тег meta charset. Он может выглядеть так <meta http-equiv="content-type" content="text/html; charset=utf-8"> или так <meta http-equiv="content-type" content="text/html; charset=windows-1251">. Во втором случае кодировка не та.

Кстати, в HTML5 кодировка устанавливается так , а варианты кодировки с http-equiv="content-type" устарели. Однако, на сайтах сейчас можно встретить и тот, и другой вариант.

Кстати, на сайте может вообще не быть тега meta charset - в этом случае кодировка файла windows-1251 (в подавляющем большинстве случаев).