Итак, для начала давайте поучимся получать страницы сайтов в переменную PHP. Это делается с помощью функции file_get_contents
, которая чаще всего используется для получения данных из файла, однако, может быть использована для получения страницы сайта - если передать ей параметром не путь к файлу, а url страницы сайта. Итак, давайте для примера получим главную страницу сайта и выведем ее на экран.
echo file_get_contents('http://theory.phphtml.net');
Что вы получите в результате: у себя на экране вы увидите страницу сайта, однако, скорее всего без CSS стилей и картинок. Давайте теперь выведем не страницу сайта, а ее исходный код. Запишем его в переменную $str и выведем на экран с помощью var_dump
.
$str = file_get_contents('http://theory.phphtml.net');
var_dump($str);
Учтите, что var_dump должен быть настроен корректно в конфигурации PHP. Корректно - это значит вы должны видеть теги и не должно быть ограничения на длину строки (код страницы сайта может быть очень большим и желательно видеть его весь). Итак, если все сделано хорошо, и вы видите исходный код страницы сайта - самое время приступить к его парсингу с помощью регулярных выражений.
Первая неожиданность, которая ожидает вас при использовании preg_match
и preg_match_all
- это то, что они работают только для тегов, целиком расположенных на одной строке (то есть, в них нету нажатого энтера). Если попытаться спарсить многострочный тег - у вас ничего не получится, пока вы не включите однострочный режим с помощью модификатора s
.
preg_match_all('#регулярка#s', $str, $res);
var_dump($res);
Вторая неожиданность ждет вас, когда вы попробуете поработать с кириллицей - в этом случае нужно не забыть написать модификатор u
(u
маленькое, не путать с большим).
preg_match_all('#регулярка#u', $str, $res);
var_dump($res);
Таким образом рекомендуем вам всегда работать с этими двумя модификаторами, вот так:
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 код, который мы разбираем).
preg_match_all('#<title>(.+?)</title>#su', $str, $res);
var_dump($res);
Содержимое <head>
:
preg_match_all('#<head>(.+?)</head>#su', $str, $res);
var_dump($res);
Содержимое <body>
:
preg_match_all('#<body>(.+?)</body>#su', $str, $res);
var_dump($res);
В общем-то ничего сложного нет, только обратите внимание на то, что как уголки тегов, так и слеш от закрывающего тега экранировать не надо (последнее верно, если ограничителем регулярки является не слеш /
, а, например, решетка #
, как у нас сейчас). Однако, на самом деле наши регулярки не идеальны. При некоторых условиях они просто откажутся работать. Вы должны быть готовы к этому - сайты, которые вы будете парсить - разные (часто они еще и устаревшие), и то, что хорошо работает на одном сайте, вполне может перестать работать на другом. Что же у нас не так? На самом деле тег <body>
- такой же тег, как и остальные и в нем вполне могут быть атрибуты. Чаще всего это атрибут class
, но могут быть и другие (например, onload для выполнения JavaScript).
Итак, перепишем регулярку с учетом атрибутов.
preg_match_all('#<body.+>(.+?)</body>#su', $str, $res);
var_dump($res);
Но и здесь мы ошиблись, при чем ошибок несколько. Первая - следует ставить не плюс +
, а звездочку *
, так как плюс предполагает наличия хотя бы одного символа - но ведь атрибутов в теге может и не быть - и в этом случае между названием тега body
и уголком не будет никаких символов - и наша регулярка спасует (не понятно, что я тут написал - учите регулярки). Поправим эту проблему и вернемся к дальнейшему обсуждению.
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[^>]*?>
- в этом случае мы полностью застрахуем себя от проблем такого рода, так как регулярка никогда не сможет выйти за тег.
<!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 у нас тоже одинарные, вот тут:
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 (в подавляющем большинстве случаев).