Учимся парсить сайты с библиотекой PHP Simple HTML DOM Parser

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

Возьмем HTML код:

<div><a href="http://xdan.ru"><div>Сайт по программированию парсеров</div><div> и многое другое</div></a></div>

К примеру, из него нам нужно получить описание и url сайта. Если брать исключительно этот кусок кода, то все решается достаточно просто:

$html = '<div><a href="http://yadro.top"><div>Сайт по программированию парсеров</div><div> и многое другое</div></a></div>';
preg_match('#<div><a href="([^"]+)"><div>([^<]+)</div><div>([^<]+)</div></a></div>#U',$html,$list);
echo 'url:'.$list[1].',title:'.$list[2].$list[3]; // выведет 
url:http://xdan.ru,title:Сайт по программированию парсеров и многое другое
Проблемы начинаются тогда, когда описание сайта заполняют пользователи, и оно не имеет определенного шаблона.

<div><a href=”http://yadro.top”><div>Сайт по <b>программированию</b> парсеров</div><div> и многое <div> многое </div> другое </div></a></div>

Такой код регулярному выражению не по зубам.

Обычно, в вузах на этот случай учат писать конечный автомат. Суть его в том, что мы перебираем, посимвольно, весь html текст, находим начало тега, и строим дерево документа. Так называемое DOM (Document Object Model)

Сейчас, писать такое самому нет необходимости.

В php, начиная с версии 5, есть встроенные методы работы с деревом документа (класс DOMDocument), но основан он на XML парсере.

А HTML и XML это хоть и очень похожие, но в тоже время абсолютно разные технологии.

К примеру, непременное требование к XML это закрытые теги и отсутствие ошибок.

Отсюда вытекает условие: ошибок в html, который мы парсим с помощью нативных средств php, быть не должно.

К сожалению, на сайтах донорах, ошибки не редки, а значит этот метод отпадает.

Для корректного разбора таких сайтов, на помощь придут php библиотеки PHPQuery, Simple HTML DOM, Zend DOM Query, Nokogiri .

Некоторые из них, после небольших манипуляций скармливают html тому же DOMDocument. Мы не будем их рассматривать.

В этой статье я расскажу про SimpleHTMLDOM. Этой библиотекой я пользуюсь уже несколько лет, и она меня еще ни разу не подводила.

Скачиваем последнюю версию здесь.

Пусть Вас не смущает то, что она не обновлялась с 2008 года, то, что она умеет, полностью покроет Ваши нужды в разборе html текстов.

В архиве, который вы скачали, две папки (примеры работы и документация) и файл simple_html_dom.php.

simple_html_dom.php это и есть вся библиотека, больше ничего для работы не потребуется. Кидаем этот файл в папку с проектом и в своем скрипте просто подгружаем его.

include 'simple_html_dom.php';

Кроме документации, которую вы скачали с архивом, доступна еще online версия, ее вы найдете здесь

Файл подключен и готов к работе.

Для того, чтобы начать разбирать HTML, его сперва нужно получить. Обычно, я делаю это при помощи библиотеки CURL.

В simplehtmldom есть методы для удаленной загрузки страниц. После подключения файла библиотеки, нам доступны 2 функции для обработки HTML строк.

str_get_html(str) и file_get_html(url)


Они делают одно и тоже, преобразуют HTML текст в DOM дерево, различаются лишь источники.

str_get_htm – на вход получает обычную строку, т.е. если вы получили HTML прибегнув к curl, или file_get_contents то вы просто передаете полученный текст этой функции.

$html = str_get_html('<html><body>Привет!</body></html>');

file_get_html – сама умеет загружать данные с удаленного URL или из локального файла

$html = file_get_html('http://www.yandex.ru/');

или

$html = file_get_html('data/test.htm');

К сожалению, file_get_html загружает страницы обычным file_get_contents. Это значит если хостер, выставил в php.ini allow_url_fopen = false (т.е. запретил удаленно открывать файлы), то загрузить что-то удаленно, не получится. Да и серьезные веб сайты таким способом парсить не стоит, лучше использовать CURL с поддержкой proxy и ssl. Однако для наших опытов, вполне хватит и file_get_html.

$html = file_get_html('http://www.yandex.ru/');

в результате, в переменной $html будет объект типа simple_html_dom.

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

Делает это метод clear.

К примеру грузим 5 раз сайт www.yandex.ru с разными поисковыми запросами

$k = 5;
while($k>0){
 $html = file_get_html('http://yandex.ru/yandsearch?text=hi'.$k.'&lr=11114'); // загружаем данные
 // как-то их обрабатываем
 $html->clear(); // подчищаем за собой
 unset($html);
 $k--;
}

Эти две строчки $html->clear(); и unset($html); лучше писать сразу же после того, как Вы создали объект. Иначе забудете, и скрипт отвалится, забив всю память.

После того, как html текст упакован в объект, можно приступать непосредственно к поиску нужных элементов.

Большинство поисковых функций выполняет метод find(selector,
).  Если второй аргумент не задан, метод возвращает массив элементов. Если же задан то элемент этого массива с индексом index.

Пример: скачаем главную страницу моего блога, и выведем все ссылки, которые встретим на своем пути.

[PHP]require_once 'simple_html_dom.php';
$data = file_get_html('http://xdan.ru');
if($data->innertext!='' and count($data->find('a'))){
  foreach($data->find('a') as $a){
    echo '<a href="http://xdan.ru/'.$a->href.'">'.$a->plaintext.'</a></br>';
  }
}

В примере, в качестве селектора я воспользовался названием тега <a>. Но можно использовать и другие CSS селекторы. Элемент на странице можно найти по его атрибутам. В первую очередь, это название тега, id и class. Также могут быть использованы и второстепенные атрибуты, к примеру, href ссылки или width картинки. Если и этих атрибутов нет, то не грех воспользоваться и регулярными выражениями.

Поиск по названию тега вы уже видели

$html->find('div')

поиск по id

$html->find('#preview')

поиск по классу

$html->find('.myclass')

или комбинированный вариант

$html->find('#preview div.myclass')

в данном случае, сначала найдется элемент с id= preview затем в нем найдутся все теги div, и уже среди них фильтруются те у которых class=”myclass”

Если метод find ничего не нашел и index не задан, то он возвращает пустой массив. Если же index задан, то метод возвращает null.

Поэтому верным решением будет проверить

if(count($html->find('#preview div.myclass')))
  foreach($html->find('#preview div.myclass') as $div)
    echo $div->innertext;

Поиск по наличию атрибута

$html->find(' img [width]'); // найдет нам все изображения у которых задан атрибут ширина

или более конкретный поиск по значению атрибута

$ret = $html->find('img[width=400px]');// найдет все изображения, у которых задана ширина равная 400px

Такая нотация позволяет искать по двум и более смежным классам

$ret = $html->find('img[class=active myclass]');//<img class="active myclass"/>

Поиск нескольких тегов

$html->find('a, img, br,span');

Поиск вложенных тегов

$es = $html->find('ul.myclass li');// найдет все li который является потомком ul(возможно и не прямым)
$es = $html->find('div.myclass li');// найдет все li в  div.myclass

У каждого найденного элемента также есть метод find

$html->find('div.myclass li');//найдет все div.myclass а потом все li лежащие в них

если нам нужно найти все li только первого div’а то мы можем написать так

$html->find('div.myclass',0)->find('li');

Поиск по значению атрибута не ограничивается только равенством. Вот доступные условия

[атрибут] – проверяет есть ли у элемента данный атрибут

[атрибут=величина] - проверяет, есть ли у элемента данный атрибут и равно ли его значение величине.( div[class=myclass] – найдет все div’ы у которых class равен myclass)

[атрибут!=величина] - проверяет, есть ли у элемента данный атрибут и не равно ли его значение величине.( div[class!=myclassok] – найдет все div’ы у которых class не равен myclassok)

[атрибут^=величина] - проверяет, есть ли у элемента данный атрибут и начинается ли его значение с величины ( div[class^=my] – найдет все div’ы у которых class начинается с my, к примеру myclass и myclassok)

[атрибут$=величина] - проверяет, есть ли у элемента данный атрибут и заканчивается ли его значение величиной( div[class$=ok] – найдет все div’ы у которых class заканчивается на ok, к примеру myclassok, yok, okно не oki)

[атрибут*=величина] - проверяет, есть ли у элемента данный атрибут и содержит ли его значение в себе величину, в любом месте(div[class*=sok] – найдет все div’ы у которых class содержит sok, к примеру myclassok, ysoki, sok)

Обычный текст можно искать как тег text
$es = $html->find('text'); // найдет все текстовые блоки в html

Комментарии находим по тегу comment
$es = $html->find('comment');

Каждый найденный элемент и сам $html имеют 5 полей

$html = str_get_html("<div>foo <b>bar</b></div>");
echo $html; // выведет <div>foo <b>bar</b></div>;
$e = $html->find("div", 0);
echo $e->tag; // Вернет: "div"
echo $e->outertext; // Вернет: <div>foo <b>bar</b></div>
echo $e->innertext; // Вернет: foo <b>bar</b>
echo $e->plaintext; // Вернет: foo bar

$e->tag Читает или записывает имя тега элемента.

$e->outertext Читает или записывает весь HTML элемента, включая его самого.

$e->innertext Читает или записывает внутренний HTML элемента

$e->plaintext Читает или записывает простой текст элемента, это эквивалентно функции strip_tags($e->innertext). Хотя поле доступно для записи, запись в него ничего не даст, и исходный html не изменит

$html = str_get_html("<div>foo <b>bar</b></div");
$div = $html->find('div',0);
$div->plaintext = 'gooo';
echo $div->innertext; // вернет <div>foo <b>bar</b></div>

Как Вы могли догадаться, для удаления ненужного элемента из HTML можно затереть его поле outertext

$html = str_get_html("<div>foo <b>bar</b></div");
$b = $html->find('b',0);
$b->outertext = '';
echo $html->innertext; // вернет <div>foo</div>

Тут следует помнить, что хоть элемент и не виден в html, из дерева DOM он никуда не делся

$html = str_get_html("<div>foo <b>bar</b></div");
$b = $html->find('b',0);
$b->outertext = '';
echo $html->innertext; // вернет <div>foo</div>, элемент удален из HTML
// но
echo count($html->find('b')); // вернет 1, в дерево элемент присутствует
при желании  мы даже можем вернуть элемент на место

$b->outertext = '<span>bar</span>';
echo $html->innertext;// вернет <div>foo<span>bar</span></div>

Для более эффективной навигации по дереву документа доступны методы

$e->children ( [int $index] ) Возвращает объект N-го прямого потомка, если индекс установлен, в противном случае возвращает массив всех дочерних элементов

$e->parent() Возвращает родительский элемент.

$e->first_child() Возвращает первый дочерний элемент, или null, если ничего не найдено

$e->last_child() Возвращает последний дочерний элемент, или null, если ничего не найдено

$e->next_sibling() Возвращает следующий родственный элемент, или null, если ничего не найдено

$e->prev_sibling() Возвращает предыдущий родственный элемент, или null, если ничего не найдено

пример

$html ="<div>
  <b>bar</b>
  <b>foo</b>
  <span>arg</span>
  <div>
    <b>tor</b>
  </div>
</div>";

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

$html  = str_get_html($html);
$div = $html->find('div',0);
$i = 0;
while($item = $div->children($i++)){
  echo $item->innertext;
}

либо так

$item = $div->children(0);
echo $item->innertext;
while($item = $item -> next_sibling()){
   echo $item->innertext;
}

Данные методы полезны при разборе таблиц, элементы которых, как правило, структурированы, но не имеют идентифицирующих атрибутов.

Ну и последняя фишка это вызов callback функции на найденный элемент

function my_callback($element) {
       if ($element->tag=='span')
                $element->outertext = '<b>'.$element->innertext. '</b>';// заменим все span элементы на b
}
$html  = str_get_html('<span>bar</span><span>pole</span><span>sushi</span><a>okno</a>');
// Регистрация функции обратного вызова с ее именем
$html->set_callback('my_callback');// вызов функции произойдет при конвертации объекта в строку
echo $html; // на самом деле, при этом вызывается магический метод __toString, он и запускает наши калбяки

На экране мы увидим


<b>bar</b><b>pole</b><b>sushi</b><a>okno</a>



Доступ к атрибутам элементов осуществляется напрямую
foreach($html->find('img') as $img) echo $img->src;
//или
echo $html->find('img',0)->src;

Хватит теории, перейдем к практике

Загрузим n фотографий из поисковой выдачи Yandex Картинок. http://images.yandex.ru/

require_once 'simple_html_dom.php';
// поисковый URL
$url = 'http://images.yandex.ru/yandsearch?text='.urlencode('Джессика Альба').'&rpt=image';
$n = 2;
// загружаем данный URL
$data = file_get_html($url);
// очищаем страницу от лишних данных, это не обязательно, но когда HTML сильно захламлен бывает удобно почистить его, для дальнейшего анализа
foreach($data->find('script,link,comment') as $tmp)$tmp->outertext = '';
// находим все изображения на странице
if(count($data->find('div.b-image img'))){
  $i = 1;
  foreach($data->find('div.b-image img') as $img){
    // выводим на экран изображение
    echo '<img src="'.$img->src.'"/>';
    // и скачиваем его в файл
    file_put_contents('data/'.($i++).'.jpg',file_get_contents($img->src));
    if($i>$n)break; // выходим из цикла если скачали достаточно фотографий
  }
}
$data->clear();// подчищаем за собой
unset($data);

Как быть если нам нужно больше фото, чем лежит на одной странице?

Ответ прост: Код, приведенный выше, заключается в функцию, в html помимо фото находим еще и URLвсех страниц, и рекурсивно вызываем данную функцию для этих страниц.


Все хорошо, 200 картинок лежат в папке data. Но их размер слишком мал.

Поэтому завершающим аккордом нашей практики будет загрузка увеличенной фотографии.

Для этого определим еще одну функцию



function getBigImage($url){
  $data = @file_get_contents($url);
  if(trim($data)=='')return false; // бывает что сайт недоступен, его фото мы не грузим
  $data = str_get_html($data);
  // находим фото
  if( count($data->find('#i-main-pic')) ){
    $dataimg = @file_get_contents($data->find('#i-main-pic',0)->src); // собачка нужна в если сервер нам вернул 404, это выозвет Warning:, поэтому экранируем ошибки
    if(trim($dataimg)=='')return false; // фото не доступно, его не грузим
    file_put_contents( 'data/'.md5($url).'.jpg', $dataimg ); // сохрпаняем в файл
  }
  $data->clear();// подчищаем за собой
  unset($data);
}



и слегка поправим getYandexImages

function getYandexImages($url,$findpages = true){
  global $i,$n;
  // загружаем данный URL
  $data = @file_get_contents($url);
  $data = str_get_html($data);
  // очищаем страницу от лишних данных, это не обязательно, но когда HTML сильно захламлен бывает удобно почистить его, для дальнейшего анализа
  foreach($data->find('script,link,comment') as $tmp)$tmp->outertext = '';
  // находим URL страниц только для первого вызова функции
  if( $findpages and count($data->find('div.b-pager__pages a'))){
    foreach($data->find('div.b-pager__pages a') as $a){ 
      // довольно распространенный случай - локальный URL. Поэтому иногда url надо дополнять до полного
      if( !preg_match('#^http://#',$a->href) )$a->href = 'http://images.yandex.ru'.$a->href;
      // и еще дна тонкость, & надо заменять на &
      $a->href = str_replace('&','&',$a->href);
      // вызываем функцию для каждой страницы
      getYandexImages($a->href,false);
    }
  }
  // находим все изображения на странице
  if(count($data->find('div.b-image img'))){
    foreach($data->find('div.b-image a') as $a){
      if( !preg_match('#^http://#',$a->href) )$a->href = 'http://images.yandex.ru'.$a->href;
      $a->href = str_replace('&','&',$a->href);
      getBigImage($a->href);
      if($i++>=$n)exit; // завершаем работу если скачали достаточно фотографий
      echo '<script>document.getElementById("counter").innerHTML = "Загружено: '.$i.' из '.$n.' фото";</script>';
      flush();
    }
  }
  $data->clear();// подчищаем за собой
  unset($data);
}
// поисковый URL
$i = 1;
$n = 20; // будем грабить 20 картинок
$url = 'http://images.yandex.ru/yandsearch?text='.urlencode('Джессика Альба').'&rpt=image';
getYandexImages($url);



Вот и все, наслаждаемся фото великолепной Джессики Альбы. Надеюсь меня простит Яндекс, ведь по сути фото грабится не с их серверов, а с прямиком с сайтов, где они лежат.

Кроме того это всего лишь демонстрация работы. Думаю никому в здравом уме, не придет в голову парсить Яндекс с помощью file_get_content. Данную библиотеку можно применять и в мирном программировании. К примеру в качестве шаблонизатора для CMS. Почему нет, с хорошим кешированием будет очень удобная штука.

При больших объемах сайтов доноров, неплохо бы разбить все на потоки.

А используя описанный мой скрипт сортировки изображений по цвету, можно собрать неплохую отсортированную базу, фотографий знаменитостей.

И, как всегда, выкладываю все исходники

UPD: Благодаря пользователю Диме, выяснилось, что библиотека обновилась 2011-07-14 и имеет версию 1.5. Нововведений не много, однако самый главный баг, с которым сталкивались все, кто пользовался библиотекой, а именно утечка памяти, устранен. На то она и версия 1.5, а не 2.0. Радует, что проект все же поддерживается и развивается. Ждем новых релизов.

Добавлено: 19 Апреля 2021 13:55:35 Добавил: Андрей Ковальчук

Мой родной PHP шаблонизатор

Целью этой статьи является разработка простого шаблонизатора, который смог бы удовлетворить основные потребности разработчика в моём лице, да и не только в моём. Принцип работы его будет основан на native методе, который вместе с другим детальнее описан тут для тех, кто еще не в курсе.

Итак, какой функционал я хочу? Для начала, это должно быть довольно простое решение без всяких рюшиков типа "куча хелперов". Конечно, кому-то это нужно, но у меня обычно всё проще, поэтому обойдёмся. То есть, должно быть независимое решение в виде одного класса. Сразу скажу, что кеширование результатов тоже упустим, дабы не отвлекаться от сути, да и задача эта в большинстве случаев принадлежит другим. Кому надо, легко может его прикрутить как внутри так и снаружи, используя этот материал. Далее я также хочу спокойно выполнять такие вещи как:

Удобное извлечение переменных в шаблоне
Вставка шаблона в шаблон
Корректная работа с HTML кодом
Режим отладки
Этим списком, думаю, я буду полностью удовлетворен.

Начнем с переменных. Тут, как и везде далее все просто - магический метод __get нам поможет обращаться к переменной по ее имени. Всё начинает выглядеть примерно так:


Listing №1 (PHP)

class Template {
  /**
   * @var array
   */
  private $_Vars;
  /**
   * @var string
   */
  private $_Name;
  /**
   * @var string
   */
  private $_Path;
 
  public function __construct()
  {
    $this->_Name = '';
    $this->_Vars = array();
    $this->_Path = '';
  }
  /**
   * @param string $VarName имя переменной
   * @param string $VarValue значение переменной
   * @return Template
   */
  public function AddVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
    return $this;
  }
  /**
   * @param string $Varname имя переменной
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      if ($this->_Vars[$VarName] instanceof Template) {
        return $this->_Vars[$VarName]->Prepare();
      }
      else{
        return $this->_Vars[$VarName];
      }
    }
    return NULL;
  }
  /**
   * @var string $Name имя шаблона
   */
  public function SetName($Name)
  {
    $this->_Name = $Name;
  }
  /**
   * @var string $Path путь к шаблону(ам)
   */
  public function SetPath($Path)
  {
    $this->_Path = $Path;
  }
}


Теперь двумя следующими методами обеспечим корректность выводимых данных. Первый заботиться об спецсимволах HTML, а второй о валидности ccылок:


Listing №2 (PHP)
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeHtml($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeHtml($VarItem);
      }
    }
    else {
      $Var = htmlspecialchars($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeUrl($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeUrl($Var);
      }
    }
    else {
      $Var = htmlentities($Var, ENT_QUOTES);
    }
    return $Var;
  }


Идем дальше.

Что я подразумеваю под словами "режим отладки"? Это, скорее всего, больше похоже на режим удобного чтения. В таком режиме на выходе мы получим код шаблона в том же виде, что и до его парсинга, то есть со всеми табуляциями, переводами строк и прочей нечистью. Это довольно удобно, потому что native шаблоны могут сложно восприниматься и то и дело приходится всё табулировать и переносить. В противоположность этому режиму будет обычный рабочий режим, где ненужные символы будут удалены за ненадобностью. За это будет отвечать метод _Zip, а за переключение между режимами статический метод SetDebug:


Listing №3 (PHP)
  /**
   * Свойство хранящее текущий режим
   * @static
   * @var bool
   */
  private static $_Debug = FALSE;
  /**
   * Вкл/выкл дебаг режим
   * @static
   * @param bool $TrueOrFalse TRUE
   */
  public static function SetDebug($TrueOrFalse = TRUE)
  {
    self::$_Debug = $TrueOrFalse;
  }
  /**
   * Возвращает $Text с удаленными "\t", "\n" и "\r"
   * @param  string $Text
   * @return string
   */
  private function _Zip($Text)
  {
    return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  }


Теперь нам нужен метод Prepare, который выполнит всю основную работу, но не выведет данные, а возвратит их строкой. Это нам понадобиться для реализации вставки шаблона в шаблон, которая и происходит при извлечении переменной в шаблоне (метод __get). Думаю, без буферизации вывода тут не обойтись:


Listing №4 (PHP)
  /**
   * @return string
   */
  public function Prepare()
  {
    if (file_exists($this->_Path . $this->_Name)) {
      ob_start();
      ${__CLASS__} = $this;
      include $this->_Path . $this->_Name;
      unset(${__CLASS__});
      return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
    }
    else {
      throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
    }
  }


Сначала проверяем наличие шаблона. Затем "по родному" парсим шаблон и возвращаем контент из буфера, очистив ненужные символы, если это необходимо. Также, меня очень напрягает писать в шаблонах $this, поэтому я решил писать там $Template. Мне так очень нравится.

Наконец, добавив метод непосредственного вывода, получаем нужный "полный фарш":


Listing №5 (PHP)
class Template {
  /**
   * @var string
   */
  private $_Name;
  /**
   * @var array
   */
  private $_Vars;
  /**
   * @var string
   */
  private $_Path;
  /**
   * @static
   * @var bool
   */
  private static $_Debug = FALSE;
  /**
   * @static
   * @param bool $TrueOrFalse TRUE
   */
  public static function SetDebug($TrueOrFalse = TRUE)
  {
    self::$_Debug = $TrueOrFalse;
  }
 
  public function __construct()
  {
    $this->_Name = '';
    $this->_Vars = array();
    $this->_Path = '';
  }
  /**
   * @param string $VarName
   * @param string $VarValue
   * @return Template
   */
  public function AddVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
    return $this;
  }
  /**
   * @param string $Varname
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      if ($this->_Vars[$VarName] instanceof Template) {
        return $this->_Vars[$VarName]->Prepare();
      }
      else{
        return $this->_Vars[$VarName];
      }
    }
    return NULL;
  }
  /**
   * @var string $Name
   */
  public function SetName($Name)
  {
    $this->_Name = $Name;
  }
  /**
   * @var string $Path
   */
  public function SetPath($Path)
  {
    $this->_Path = $Path;
  }
  /**
   * @return string
   */
  public function Prepare()
  {
    if (file_exists($this->_Path . $this->_Name)) {
      ob_start();
      ${__CLASS__} = $this;
      include $this->_Path . $this->_Name;
      unset(${__CLASS__});
      return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
    }
    else {
      throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
    }
  }
 
  public function Display()
  {
    print ($this->Prepare());
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeHtml($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeHtml($VarItem);
      }
    }
    else {
      $Var = htmlspecialchars($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeUrl($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeUrl($Var);
      }
    }
    else {
      $Var = htmlentities($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param  string $Text
   * @return string
   */
  private function _Zip($Text)
  {
    return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  }
}


Ну и напоследок, небольшой пример использования.

Допустим, у нас есть два шаблона - главная страница page.php и список пользователей userslist.php. Нужно отобразить этот список вместе с информацией на главной странице. Поскупившись на контент, рисуем главную:


Listing №6 (PHP)
<div style="border: 1px solid #E0E3F3;">
  <?php echo $Template->UserList;?>
</div>

<a href="<?php echo $Template->EscapeUrl($Template->Link)?>">About</a>

А также список пользователей:


Listing №7 (PHP)
<div>
  <?php foreach($Template->EscapeHtml($Template->UserNames) as $UserName) { ?>
  <span style="color: #FF0000;"><?php echo $UserName;?></span><br>
  <?php } ?>
</div>


За логику отвечает некая модель:


Listing №8 (PHP)
//Подключаем шаблонизатор
require 'Template.php';
//Устанавливаем режим отладки включенным для всех шаблонов
Template::SetDebug(TRUE);
//Инициализируем подшаблон списка пользователей
$UsersListView = new Template();
$UsersListView->SetName('userslist.html');
//Список пользователей
$UserNames = array('Den Rights©', 'Dasha Nasha', 'Samuil Kakojto');
$UsersListView->AddVar('UserNames', $UserNames);
//Инициализируем главный шаблон страницы
$PageView = new Template();
$PageView->SetName('page.html');
$PageView->AddVar('UserList', $UsersListView);
$PageView->AddVar('Link', 'http://www.itdumka.com.ua/index.php?cmd=shownode&node=1');
$PageView->Display();



В результате работы с включенным режимом отладки, в отличие от обычного режима, где все будет в одну строку, мы увидим такую картину:


Listing №9 (HTML)
<div style="border: 1px solid #E0E3F3;">
  <div>
    <span style="color: #FF0000;">Den Rights©</span><br>
    <span style="color: #FF0000;">Dasha Nasha</span><br>
    <span style="color: #FF0000;">Samuil Kakojto</span><br>
  </div>
</div>
<a href="http://www.itdumka.com.ua/index.php?cmd=shownode&node=1">About</a>


Вот так.

Добавлено: 08 Августа 2018 07:45:41 Добавил: Андрей Ковальчук

Пример выводит все IMG ссылки на PHP

<?php
$file_content = file_get_contents("http://htmlweb.ru/index.php");

preg_match_all("/(<img )(.+?)( \/)?(>)/",$file_content,$images);
foreach ($images[2] as $val)
{
    if (preg_match("/(src=)('|\")(.+?)('|\")/",$val,$matches) == 1)
        echo $matches[3] . "<br />";
}
?>

Добавлено: 29 Мая 2018 18:58:09 Добавил: Андрей Ковальчук

Найти img src (PHP)

Найти все изображения (атрибут src у тега <img>) в тексте, можно при помощи регулярного выражения и функции int preg_match_all ( string $pattern , string $subject [, array &$matches [, int $flags = PREG_PATTERN_ORDER [, int $offset = 0 ]]] ).

Пример
Например у нас есть файл test.html и нужно найти все атрибуты src у изображений.

test.html

<!DOCTYPE HTML><html><head><meta charset="utf-8"><title>Вырезаем изображения</title></head><body>

<img src="http://expange.ru/images/1/1/207-130px.jpg" alt="Литр свежевыжатого апельсинового сока">
<img src="http://expange.ru/images/1/1/204-130px.jpg" alt="Соковыжималка">
<img src="http://expange.ru/images/1/1/206-130px.jpg" alt="Выжимаем сок">
<img src="http://expange.ru/images/1/1/208-130px.jpg" alt="Апельсиновый сок">
<img src="http://expange.ru/images/1/1/196-130px.gif" alt="Закрашенный круг">
<img src='http://expange.ru/images/1/1/198-130px.gif' alt="Векторный круг в выделение">
<img src='http://expange.ru/images/1/1/200-130px.gif' alt="Параметры наложения (Blending options)">
<img src='http://expange.ru/images/1/1/202-130px.gif' alt="Круг с рамкой (PhotoShop)">
<img src='http://expange.ru/images/1/1/195-130px.gif' alt="Фотошоп. Как нарисовать круг.">
<img src="http://expange.ru/images/1/1/197-130px.gif" alt="Векторный круг (PhotoShop)">
<img src="http://expange.ru/images/1/1/199-130px.gif" alt="Закрашиваем выделенную область">
<img src="http://expange.ru/images/1/1/201-130px.gif" alt="Свойства слоя (PhotoShop)">
<img src="http://expange.ru/images/1/1/193-130px.gif" alt="Нарисовать линию в фотошопе">
<img src="http://expange.ru/images/1/1/194-130px.gif" alt="Как нарисовать линию (PhotoShop)">
<img src="http://expange.ru/images/1/1/189-130px.gif" alt="Многоуровневое выпадающее меню (JavaScript)">
<img src='http://expange.ru/images/1/1/188-130px.gif' alt="Выпадающее меню (JavaScript)">
<img src='http://expange.ru/images/1/1/190-130px.gif' alt="Drop Down Menu (JavaScript)">
<img src='http://expange.ru/images/1/1/185-130px.png' alt="Гаечный ключ → Параметры (Google Chrome)">
<img src='http://expange.ru/images/1/1/178-130px.png' alt="Параметры безопасности. Включение JavaScript.">
<img src='http://expange.ru/images/1/1/187-130px.png' alt="Настройки содержания">

</body></html>

preg_image.php
Скрипт preg_image.php будет брать текст файла test.html и искать изображения.
<?php

$content = file_get_contents('test.html');

preg_match_all('/<img[^>]+src="?\'?([^"\']+)"?\'?[^>]*>/i', $content, $images, PREG_SET_ORDER);

foreach ($images as $image) {
    echo $image[1] . '<br>';
}

Результат
В результате выполнения скрипта preg_images.php, на экран будет выдан следующий результат:
http://expange.ru/images/1/1/207-130px.jpg
http://expange.ru/images/1/1/204-130px.jpg
http://expange.ru/images/1/1/206-130px.jpg
http://expange.ru/images/1/1/208-130px.jpg
http://expange.ru/images/1/1/196-130px.gif
http://expange.ru/images/1/1/198-130px.gif
http://expange.ru/images/1/1/200-130px.gif
http://expange.ru/images/1/1/202-130px.gif
http://expange.ru/images/1/1/195-130px.gif
http://expange.ru/images/1/1/197-130px.gif
http://expange.ru/images/1/1/199-130px.gif
http://expange.ru/images/1/1/201-130px.gif
http://expange.ru/images/1/1/193-130px.gif
http://expange.ru/images/1/1/194-130px.gif
http://expange.ru/images/1/1/189-130px.gif
http://expange.ru/images/1/1/188-130px.gif
http://expange.ru/images/1/1/190-130px.gif
http://expange.ru/images/1/1/185-130px.png
http://expange.ru/images/1/1/178-130px.png
http://expange.ru/images/1/1/187-130px.png

Добавлено: 24 Мая 2018 20:34:43 Добавил: Андрей Ковальчук

Как обчистить чужой сайт - Нет проблем

В этой статье реч пойдет о том, что называют граберами или парсерами на PHP. На самом деле я всего лишь раскрою технологию, которая очень проста, а превращать ее в парсер или грабер ваше дело. Как я уже сказал, все очень просто. Давай ка разберем что вообще мы хотим сделать, тогда проще будет понять все остальные телодвижения. Чаще всего нам надо получить текст с другого сайта. Для этого нам нужен исходный код страниц, который можно получить с помощью PHP. Самый простой способ это когда мы знаем адрес нужной страницы и просто получаем ее код, например так

<?php $data = file_get_contents('http://target.com');?>


Это очень простой способ. Можно скормить скрипту файл с адресами и он их обшарит. В самом скрипте можно вести обработку полученного кода, например вырезать новости или заголовки. Но что делать если сайт требует авторизации или особых заголовков. Сейчас большинство сайтов имеют такую защиту(нормальных сайтов:)). Тогда на на помощь приходит curl. Че за хрень, спросят некоторые, я отвечу. Это библиотека на PHP для работы с HTTP. Она умеет выставлять заголовки, куки и еще много чего. Самый просто запрос на curle будет выглядеть примерно так
<?php //инициализируем сеанс 
$ch = curl_init('http://target.com'); 
//устанавливаем параметры 
curl_setopt($ch, CURLOPT_HEADER, 1); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
//делаем запрос 
$responseData = curl_exec($ch); 
//закрываем сеанс 
curl_close($ch); ?>

Согласись, не так сложно, если учесть, что с помощью этого мы можем управлять куками и заголовками, а так же еще кучей приколов. Взять к примеру возможность не следовать редиректу:) или транслировать пост данные. К стати как передавать POST я и расскажу. Для этого наши данные должны иметь вид

Code:
переменная=значение&переменная2=значение2...

пример передачи POST данных через CURL
<?php  
//устанавливаем что будем посылать 
$reguestParams = 'var=value&var2=value2'; 
//инициализируем сеанс 
$ch = curl_init('http://target.com'); 
//устанавливаем параметры 
curl_setopt($ch, CURLOPT_HEADER, 1); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
//говорим что мы собираемся передавать данные и что именно передавать
curl_setopt($ch, CURLOPT_POST, 1); 
curl_setopt($ch, CURLOPT_POSTFIELDS, $reguestParams); 
//делаем запрос 
$responseData = curl_exec($ch); 
//закрываем сеанс 
curl_close($ch); ?>

Думаю все уже догадались что после всех этих манипуляций, исходный код целевой страницы находится в переменной $responseData. Ну вот мы и получили HTML код странички и теперь можем его обрабатывать.

В предыдущей статье мы остановились на том что научились получать доступ к страницам удаленных сайтов посредством CURL. Для повторения лишь приведу последний пример.
<?php  
//устанавливаем что будем посылать 
$reguestParams = 'var=value&var2=value2'; 
//инициализируем сеанс 
$ch = curl_init('http://target.com'); 
//устанавливаем параметры 
curl_setopt($ch, CURLOPT_HEADER, 1); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
//говорим что мы собираемся передавать данные и что именно передавать
curl_setopt($ch, CURLOPT_POST, 1); 
curl_setopt($ch, CURLOPT_POSTFIELDS, $reguestParams); 
//делаем запрос 
$responseData = curl_exec($ch); 
//закрываем сеанс 
curl_close($ch); ?>

Теперь давайте разберемся как нам получить доступ к странице с авторизацией. Для этого мы должны получить ID сессии и в дальнейшем передавать его при каждом запросе. Это может выглядеть сложно, но на самом деле все намного проще. Сперва мы отправляем POST запрос с логином и паролем, а за тем вытягиваем ID сессии и в дальнейшем путешествуем по сайту и получаем его странички, пользуясь этим ID.
Делается это так
<?php  
if ( isset($_SESSION['user_sess_id']) ) { 
    curl_setopt($ch, CURLOPT_COOKIE, 'PHPSESSID=' . $_SESSION['user_sess_id']);
} 
?>

А теперь все вместе, что бы было понятнее как CURL с этим всем справляется
<?php  
//устанавливаем что будем посылать 
$reguestParams = 'var=value&var2=value2'; 
//инициализируем сеанс 
$ch = curl_init('http://target.com'); 
//устанавливаем параметры 
curl_setopt($ch, CURLOPT_HEADER, 1); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
//говорим что мы собираемся передавать данные и что именно передавать
curl_setopt($ch, CURLOPT_POST, 1); 
//устанавливаем куки с ID сессии 
if ( isset($_SESSION['user_sess_id']) ) { 
    curl_setopt($ch, CURLOPT_COOKIE, 'PHPSESSID=' . $_SESSION['user_sess_id']);
} 
curl_setopt($ch, CURLOPT_POSTFIELDS, $reguestParams); 
//делаем запрос 
$responseData = curl_exec($ch); 
//закрываем сеанс 
curl_close($ch); 
//тут мы проверяем пришел ли нам в ответе от сервера ID сессии и если он есть устанавливаем его
if (preg_match('#Set-Cookie:\s+PHPSESSID=(.*);#Ui', $responseData, $match)) $_SESSION['user_sess_id'] = $match[1];
 ?>

как видишь, все очень просто. Мы просто, получая ответ проверяем есть ли там то что нам надо и пихаем его в нашу сессию, но перед запросом устанавливаем куки. Но некоторые сайты так же проверяют заголовки, что бы еще немного себя обезопасить. Наш друг КУРЛ и с этим справляется на 5 баллов. Например заголовки в curl можно установить так
<?php  
curl_setopt($ch, CURLOPT_USERAGENT, 'Браузер'); 
curl_setopt($ch, CURLOPT_REFERER , 'Реферер'); 
?>

Cо всем списком опций курла можно ознакомиться в доке по PHP. Использование этой библиотеки открывает очень большие перспективы, я лично писал на ее основе скрипты, способные выдерать из сайта контент и картинки, сохраняя это все в нужном мне формате, полностью автоматически, единственным недостатком являеться лишь то, что под каждый сайт такие скрипты надо писать индивидуально, но это уже не бока курла а методов обработки в целом, которые на данный момент не являются совершенством.

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

И так, из предыдущих статей мы уже знаем как посылать запросы, получать данные, формировать заголовки и даже проходить авторизацию при помощи CURL. Но один вопрос все еще открыт. Как автоматизировать процесс? Вот об этом я и хотел написать в этой части статьи. За пример возьмем обычный запрос с использованием библиотеки CURL
<?php  
//устанавливаем что будем посылать 
$reguestParams = 'var=value&var2=value2'; 
//инициализируем сеанс 
$ch = curl_init('http://target.com'); 
//устанавливаем параметры 
curl_setopt($ch, CURLOPT_HEADER, 1); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
//говорим что мы собираемся передавать данные и что именно передавать
curl_setopt($ch, CURLOPT_POST, 1); 
curl_setopt($ch, CURLOPT_POSTFIELDS, $reguestParams); 
//делаем запрос 
$responseData = curl_exec($ch); 
//закрываем сеанс 
curl_close($ch); ?>

Если запрос будет удачным и страница существует, мы получим ее код HTML. Теперь для автоматизации процесса парсинга нам необходимо определиться с тем что мы вообще делаем. Давай разберем пример когда нам нужно получить список страниц с текущей страницы, например в каком нибудь каталоге статей. Тогда мы должны в ручную посмотреть код сайта, который будем парсить и определить в каком виде у него эти самые ссылки. Обычно это что-то вроде
Code:
http://target.net/stat?page=.....

тогда для нахождения этих ссылок мы можем воспользоваться регулярным выражением вроде этого
<?php  
//пытаемся найти ссылки на другие страницы 
if (preg_match_all('#href="(http://target.net/stat?page=[\d]+)"#i', $responseData, $matches)) { 
     //если есть перебираем все совпадения 
     foreach ($matches as $match) { 
          //тут посылаем запрос на все найденные страницы по очереди 
     } 
} 
?>

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

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

Добавлено: 24 Мая 2018 20:22:59 Добавил: Андрей Ковальчук

Парсер на РНР - это возможно!

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

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

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

Что же такое автомат?

Представьте себе дискретную функцию от двух аргументов Ft(d, Ft-1). В качестве первого аргумента мы используем конечное счетное множество (массив данных), которое поступает извне. На каждом шаге в функцию поступает только одно число из данного массива. Вторым аргументом функции является значение функции на предыдущем шаге. Добавлю еще одно условие. Область значений данной функции представляет собой конечное счетное множество.

В чем прелесть такой функции? Вся прелесть заключается в том, то мы можем представить ее в виде матрицы, где номера строк будут задавать поступающие данные, а номера столбцов будут представлять область значений функции. Тогда, записав в ячейку (строка, столбец) число из множества значений функции, мы получим матрицу, которая описывает зависимость функции от входных данных и всего спектра значений. Будем называть число из множества значений СОСТОЯНИЕМ, а функцию АВТОМАТОМ.

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

Начнем со сканера. Словами являются знаки операций +, -, *, / и последовательности символов, если они не содержат разделителей, такие как перевод строки, пробел и символ табуляции. Разделители мы будем просто игнорировать. Автомат для сканера, в этом случае, будет следующим.

    // состояния   0,  1,  2
     "0" => array( 0, -1, -1),//разделитель
     "1" => array( 2, -1, -1),//слово из одного символа
     "2" => array( 1,  1, -1),//символ

Номера строк задают тип символа, поскольку нам надо выделить знаки операций в отдельные слова. Состояния (номера столбцов) будут означать следующее.

-1 слово готово, пора возвращать
0 начало сканирования
1 получили символ, надо копить пока это символ
2 получили предопределенное слово из одного символа
в состоянии 1 мы будем копить символы, чтобы вернуть их как слово в состоянии -1. Наш сканер будет вызываться из парсера и завершать свою работу, когда он распознает хотя бы одно "слово", поэтому нет смысла вводить состояние -1 в таблицу автомата. Для парсера автомат будет такой.
     // состояния  0,  1,  2,  3,  4,  5
     "0" => array( 1, -1,  1,  1,  1,  1), // оператор
     "1" => array( 2,  4, -1,  2, -1, -1), // операнд
     "2" => array( 3,  3, -1,  3, -1, -1), // левая скобка
     "3" => array(-1, -1,  5, -1,  5,  5), // правая скобка

а состояния соответственно
-1 Ошибка

0 Начало разбора

1 Получили оператор, ожидаем правый операнд
или левую скобку

2 Получили левый операнд (надо проверить число ли это),
ждем оператор или правую скобку

3 Получили левую скобку,
ожидаем оператор или левую скобку

4 Получили правый операнд (надо проверить число ли это),
ожидаем оператор или правую скобку

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

Далее привожу код программы с подробными комментариями, которые заменят дальнейшие объяснения. Я не строю дерева операций в примере данного парсера. Вы можете сделать это сами, ведь в соответствующих состояниях автомата вы получите оператор и операнды.
<?php 
class ExpressionParser { 
  var $pos,    // Позиция в буфере для разбора 
      $length,    // Длина буфера 
      $line,      // Текущий номер строки 
      $column,    // Текущий номер колонки в строке 
      $data,      // Буфер данных 
      $brackets,  // Количество открытых скобок 
      $state,     // Текущее состояние парсера 
      $errorstr,  // Строка диагностики ошибки 
      $instates,  // Код слова подаваемый на вход автомата 
      $prevstate, // Предыдущее состояние парсера 
      $automat;   // Таблица автомата парсера 

 /********************************************************************** 
  *  Конструктор                      * 
  **********************************************************************/ 
  function ExpressionParser($str) { 
    $this->data=$str; 
    $this->length=strlen($str); 
    $this->pos=0; 
    $this->line=1; 
    $this->column=0; 
    $this->brackets=0; 
     
    // Коды слов, выданных сканером, подаваемых на вход парсера 
    // Остальные слова имеют код 1 
    $this->instates = array("+" => 0, "*" => 0, "-" => 0, "/" => 0, "(" => 2, ")" => 3); 
     
    // Автомат парсера 
    $this->automat=array( 
    /* 
    -1 Ошибка 
     0 Начало разбора 
     1 Получили оператор, ожидаем правый операнд или левую скобку 
     2 Получили левый операнд (надо проверить число ли это), ждем оператор или правую скобку 
     3 Получили левую скобку, ожидаем оператор или левую скобку 
     4 Получили правый операнд (надо проверить на число), ожидаем оператор или правую скобку 
     5 Получили правую скобку, ожидаем оператор 
    */ 
     //состояния 0,  1,  2,  3,  4,  5 
     "0"=>array( 1, -1,  1,  1,  1,  1),//оператор 
     "1"=>array( 2,  4, -1,  2, -1, -1),//операнд 
     "2"=>array( 3,  3, -1,  3, -1, -1),//левая скобка 
     "3"=>array(-1, -1,  5, -1,  5,  5),//правая скобка 
    ); 
    $this->state=$this->prevstate=0; 
  } 

 /********************************************************************** 
  *  Сканер                     * 
  **********************************************************************/ 
  function Scan() { 
    // Разделители, которые игнорируем 
    $delimiters=array(" ","\t","\r","\n"); 

    // Слова из одного символа 
    $words=array("+","-","*","/","(",")"); 

    // автомат сканнера 
    $automat=array( 
    /* 
    -1 слово готово, пора возвращать 
     0 начало сканирования 
     1 получили символ, надо копить пока это символ 
     2 получили предопределенное слово из одного символа 
    */ 
    //состояния  0,  1,  2 
     "0"=>array( 0, -1, -1),//разделитель 
     "1"=>array( 2, -1, -1),//слово из одного символа 
     "2"=>array( 1,  1, -1),//символ 
    ); 
    $state=0; 
    $word=""; 
     
    // Цикл сканирования 
    while ($this->pos<$this->length) { 

      // Устанавливаем код подаваемого на вход автомата символа. 
      if (in_array($this->data[$this->pos],$delimiters))  
     $instate=0; 
      elseif (in_array($this->data[$this->pos],$words))  
     $instate=1; 
      else 
     $instate=2; 
       
      // Получаем состояние автомата 
      $state=$automat[$instate][$state]; 
       
      // Наши действия по состояниям автомата 
      switch($state) { 
     case 0: // начало сканирования 
    if ($this->data[$this->pos]=="\n") { 
      $this->line++; 
      $this->column=0; 
    } 
    $word=""; 
    break; 
     case -1: // слово готово, пора возвращать 
    if (strlen($word)) return $word; 
    break; 
     case 1: // получили символ, надо копить пока это символ 
    $word.=$this->data[$this->pos]; 
    break; 
     case 2: // получили предопределенное слово из одного символа 
    $word=$this->data[$this->pos]; 
    break; 
      } 
      $this->pos++; 
      $this->column++; 
      if ($this->pos==$this->length && strlen($word)) return $word; 
    } 
    return false; 
  } 

 /********************************************************************** 
  *  Парсер                     * 
  **********************************************************************/ 
  function Parse() { 
    // Переменная $first равна нулю, если функция разбора была вызвана первый раз 
    $first=$this->pos; 

    // Цикл состояний 
    while(1) { 
       
      // Получаем слово от сканнера 
      $word=$this->Scan(); 
       
      // Если слов больше нет, то прерываем цикл 
      if ($word===false) break; 
       
      // Устанавливаем код, подаваемого на вход автомата, слова 
      $instate=isset($this->instates[$word]) ? $this->instates[$word] : 1; 
       
      // Получаем состояние автомата парсера 
      $this->state=$this->automat[$instate][$this->state]; 
       
      // Если ошибочное состояние, то прерываем цикл 
      if ($this->state==-1) { 
     $this->errorstr="Ошибка в строке: $this->line, колонка: $this->column<br>"; 
     break; 
      } 
       
      // Наши действия по состояниям автомата парсера 
      switch($this->state) { 

     case 1: // Получили оператор, ожидаем правый операнд или левую скобку 
      
    // Если первое слово оператор, то это может быть только "+" или "-" 
    if (($this->prevstate==3 || $this->prevstate==0) && $word!="-" && $word!="+") { 
      $this->errorstr="Ошибка в строке: $this->line, колонка: $this->column<br>"; 
      return false; 
    } 
    break; 

     case 2: // Получили левый операнд (надо проверить число ли это), ждем оператор  
             //или правую скобку 

    // Проверяем число ли это? 
    if (!preg_match("/^[0-9]+(\.[0-9]+)?$/",$word)) { 
      $this->errorstr="Ошибка в строке: $this->line, колонка: $this->column<br>"; 
      return false; 
    } 
    break; 

     case 3: // Получили левую скобку, ожидаем оператор или левую скобку 

    // Увеличиваем кол-во открытых скобок на 1; 
    $this->brackets++; 
     
    // Удобно использовать рекурсию, т.к. данные в скобках 
    // можно рассматривать как самоcтоятельные выражения. 
    // Мы вернемся из функции в случае ошибки, конца данных или 
    // после получения закрытой скобки 
    if (!$this->Parse()) return false; 
    break; 

     case 4: // Получили правый операнд (надо проверить число ли это), ожидаем оператор  
             //или правую скобку 

    // Проверяем число ли это? 
    if (!preg_match("/^[0-9]+(\.[0-9]+)?$/",$word)) { 
      $this->errorstr="Ошибка в строке: $this->line, колонка: $this->column<br>"; 
      return false; 
    } 
    break; 

     case 5: // Получили правую скобку, ожидаем оператор 
      
    // Уменьшаем кол-во открытых скобок на 1 
    $this->brackets--; 
    return true; 

      } // end switch 
       
      // Запоминаем текущее состояние для следующего шага цикла 
      $this->prevstate=$this->state; 

    } // end while 

    // Так как у нас отсутствует состояние конца разбора, то надо 
    // Проверить в каком состоянии мы завершили разбор 
    // Это надо делать только один раз в самом первом вызове 
    // функции разбора. Это первый вызов, если $first==0 
    // Итак, мы должны вернуть ошибку, если у нас есть лишние скобки, 
    // или если мы не получили правого операнда или правой скобки, 
    // т.е. разбор завершился "на середине". 
     
    if (!$first && ($this->brackets || $this->state!=4 && $this->state!=5)) return false; 
     
    return true; 
  } 
   
} 

$p=new ExpressionParser("-4.25*((2+3)*4+1)/5"); 
print $p->data."<br>"; 
if ($p->Parse()) 
  print "Выражение корректно.<br>"; 
else 
  print $p->errorstr; 
?>

Добавлено: 24 Мая 2018 18:24:03 Добавил: Андрей Ковальчук

Делаем простой парсер новостей на php.

Итак, есть новостной ресурс (для примера, возьмем новости с Яндекса) и есть задача получать на автомате с этого сайта несколько последних новостей. Полученные новости мы должны в нужном нам формате выводить на странице нашего сайта.

1. Находим нужную rss ленту нашего новостника. В нашем случае – это http://news.yandex.ru/index.rss (у Яндекса множество лент, разбитых по категориям. Берем одну из них – «Главные новости»). Новости в ленте состоят из заголовка (с ссылкой на саму новость), даты публикации и краткого анонса.

Сам xml документ выглядит примерно так (укорочен):

<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:yandex="http://news.yandex.ru" xmlns:str="http://exslt.org/strings" version="2.0">
  <channel>
    <title>Яндекс.Новости: Главные новости</title>
    <link xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:lego="https://lego.yandex-team.ru">http://news.yandex.ru/index.html</link>
    <description>
      Первая в России служба автоматической обработки и систематизации новостей. Сообщения ведущих российских и мировых СМИ. Обновление в режиме реального времени 24 часа в сутки.
    </description>
    <image xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:lego="https://lego.yandex-team.ru">
      <url>http://company.yandex.ru/i/50x23.gif</url>
      <link>http://news.yandex.ru</link>
      <title>Яндекс.Новости</title>
    </image>
    <lastBuildDate>Mon, 12 Nov 2012 09:20:44 +0000</lastBuildDate>
    <item>
      <title>Путин утвердил состав нового Совета по правам человека</title>
      <link>http://news.yandex.ru/yandsearch?cl4url=www.forbes.ru%2Fnews%2F203951-putin-utverdil-sostav-novogo-soveta-po-pravam-cheloveka&amp;cat=0&amp;lang=ru</link>
      <description>Президент России Владимир Путин утвердил новый состав президентского Совета по развитию гражданского общества и правам человека ( СПЧ ). Текст указа опубликован на сайте Кремля. В утвержденном списке – 39 новых фамилий.</description>
      <pubDate>Mon, 12 Nov 2012 08:36:10 +0000</pubDate>
      <pubDateUT>1352709370</pubDateUT>
      <guid>http://news.yandex.ru/yandsearch?cl4url=www.forbes.ru%2Fnews%2F203951-putin-utverdil-sostav-novogo-soveta-po-pravam-cheloveka&amp;cat=0&amp;lang=ru</guid>
    </item>
<item>
      <title>«Луркморье» могло попасть в «черный список» за статью о марихуане</title>
      <link>http://news.yandex.ru/yandsearch?cl4url=izvestia.ru%2Fnews%2F539403&amp;cat=0&amp;lang=ru</link>
      <description>IP-адрес «Луркоморья» (85.17.124.180) был добавлен в «черный список». Решение о внесении ресурса в список запрещенных сайтов принял ФСКН. Об этом сообщается на официальном сайте Роскомнадзора.</description>
      <pubDate>Mon, 12 Nov 2012 07:30:00 +0000</pubDate>
      <pubDateUT>1352705400</pubDateUT>
      <guid>http://news.yandex.ru/yandsearch?cl4url=izvestia.ru%2Fnews%2F539403&amp;cat=0&amp;lang=ru</guid>
    </item>
</channel>
</rss>

Как видно из примера, структура включает несколько строк служебной информации об источнике, а уже затем идут сами новости, обрамленные тегами item. В конце документа идут заключительные теги channel и rss.

Для нашей задачи нужны только новости в тегах item, при этом лишь определенное количество новостей (последние четыре).

2. Парсим (считываем) нужное нам количество строк из xml-документа.
    $buffer="";//здесь будем хранить весь полученный документ
    $handle = fopen("http://news.yandex.ru/index.rss", "r");
    for($i=0;$i<46;$i++):
      feof($handle);
      $line = fgets($handle);
      $buffer .=$line;
    endfor;
    fclose($handle);
    $buffer .="</channel></rss>";

Здесь мы считываем 46 строк. Почему 46? Потому что последняя новость (ее заключительный тег item) находится на 46 строчке. Чтобы спарсенный документ был цельным, мы добавляем в его конец закрывающие теги channel и rss.

3. Разбираем полученный xml-документ в массив. Для решения данной задачи используем специальные функции php.
// создаем xml-анализатор, ссылка на него помещается в $p
$p = xml_parser_create(); 
//следующая функция разбирает наш документ и создает два массива (один со значениями, другой с индексами)
 xml_parse_into_struct($p, $buffer, $vals, $index);
//Освобождаем память, занятую XML анализатором $p
 xml_parser_free($p);

4. Для вывода полученного контента нам достаточно одного массива со значениями $val. Нам нужно создать цикл, который бы выводил заголовки, дату и анонсы новостей.
<?php
//начинаем с 20-го элемента, т.к. вначале идут не новости, а служебная информация об источнике
  $e=20;
//выводим четыре новости
    for($k=0;$k<4;$k++):
//заголовок
      $yandex_title=$vals[$e]['value'];
      $e=$e+2;
//ссылка на новость
      $yandex_link=$vals[$e]['value'];
      $e=$e+2;
//анонс новости
      $yandex_desc=$vals[$e]['value'];
  ?>
      <div class="yandex_news">     
        <div class="news_text">
          <h2><a href="<?=$yandex_link?>"target="_blank"><?=$yandex_title?></a></h2>
            <?=$yandex_desc?></a>
        </div>
      </div>
<?php
//делаем переход к индексу в массиве для следующей новости
      $e=$e+11;
      endfor;
?>

Таким образом, на нашей странице будут выведены последние новости от новостной ленты Яндекса.

Добавлено: 09 Мая 2018 17:31:16 Добавил: Андрей Ковальчук

Парсер файла robots.txt, проверка url на запрет.

Класс, получающий и разбирающий файл robots.txt:

<?php
class robots {

var $content = ''; # текст файла robots.txt
var $branches = array(); # здесь храним ветки для разных User-Agent

# конструктор
# @url: адрес сайта или хост в произвольном формате
function robots($url) {
    # получаем файл robots.txt
    $url = preg_replace('#^http://#is', '', trim($url));
    $url = current(explode('/', $url));
    $this->content = trim(file_get_contents('http://' . $url . '/robots.txt'));
    
    # парсим полученные данные
    $s = preg_split('#[\n]+#is', $this->content);
    $current_user_agent = '';
    foreach($s as $line) {
        $line = trim(current(explode('#', trim($line), 2)));
        if (substr_count($line, ':')<1) continue;
        $line = explode(':', $line, 2);
        $current_directive = strtolower(trim($line[0]));
        $current_value = trim($line[1]);
        if ($current_directive == 'user-agent') {
            $current_user_agent = $current_value;
        } elseif($current_user_agent!='') {
            $this->branches[$current_user_agent][$current_directive][] = $current_value;
        }
    }
}



# получить значение заданной директивы для заданного агента
# @user_agent: агент
# @directive: имя директивы
# возвратит FALSE если директива не указана
function get_directive($user_agent, $directive) {
   $user_agent = strtolower($user_agent);
   $directive = strtolower($directive);
   $ret = array();
   foreach($this->branches as $ua_mask=>$data) {
    if (($ua_mask=='*' || $user_agent=='*' || @preg_match('#'.preg_quote($ua_mask,'#').'#is', $user_agent)) && isset($data[$directive]))
        $ret = array_merge($ret, $data[$directive]);
    }
   if (count($ret)>0) return array_unique($ret);
   else return false;
}

# проверить, запрещен ли url к индексации
# @user_agent: агент
# @url: полный url для проверки
# возвратит TRUE если url запрещен
function check_disallow($user_agent, $url) {
    $url = preg_replace('#^http://#is', '', trim($url));
    $url = explode('/', $url, 2);
    $url = (count($url)>1) ? '/' . $url[1] : '/' . $url[0];
    $url = trim($url);
    $info = $this->get_directive($user_agent, 'Disallow');
    foreach($info as $url_mask) {
        if (preg_match('#^'.preg_quote($url_mask, '#').'#', $url)) {
            return true;
        }
    }
    return false;
}
}

Добавлено: 19 Апреля 2018 06:21:01 Добавил: Андрей Ковальчук

Парсер bash.im

<?php 
$count = 40224; //количество страниц которые будем парсить

for ($i = 1; $i <= $count; $i++){
$b = file_get_contents('http://bash.im/quote/'.$i);
if (preg_match('|<div class="text">(.*?)</div>|is', $b, $quote)){
echo'<pre>';
print_r($quote);
echo'</pre>'; 
} 
}
?>

Добавлено: 17 Апреля 2018 20:12:38 Добавил: Андрей Ковальчук

file_get_contents.php

<!--
Сканируем чужие сайты

Вот уж где находка вора, с помощью функции file_get_contents() вы можете открывать на своих страницах чужие сайты.
-->

<?php
echo file_get_contents('http://delphisources.at.ua');
?>

<!--
Злоупотреблять этой функцией не нужно, иначе можно доиграться!
-->

Добавлено: 11 Апреля 2018 06:59:52 Добавил: Андрей Ковальчук

Парсинг курса валют

В данной статье хочу познакомить читателей с основами php парсинга.

Для примера будем получать официальный курс гривны к доллару с сайта НБУ.
Данный код вы сможете использовать в дальнейшем, если вам нужно будет производить какие-либо математические операции с курсом, или просто выводить где-то на сайте.

Сначала приведу весь код, а далее – более подробные пояснения.

Php парсинг курса гривны к доллару с сайта НБУ:

// урл страницы сайта НБУ с курсами валют
$url="http://www.bank.gov.ua/control/ru/curmetal/detail/currency?period=daily";
// получаем содержимое страницы-донора в переменную
$text = file_get_contents($url); 
// разбираем содержимое с помощью регулярных выражений
preg_match('#<td class="cell_c">USD</td>.*?</tr>#is', $text, $arr);
preg_match_all('#<td class="cell_c">(.*?)</td>#is', $arr[0], $arr);
// выводим результат
echo "{$arr[1][1]} {$arr[1][0]} = <b>{$arr[1][2]}</b> UAH";

Пояснения к коду.
Строки кода 3 и 5, думаю, понятны – получаем всё содержимое страницы сайта-донора.
Далее, заходим на сайт НБУ. Нажимаем Ctrl+U (просмотр кода страницы) и находим фрагмент кода html, где выводится курс доллара.
     <tr>
          <td class="cell_c">840</td>
          <td class="cell_c">USD</td>
          <td class="cell_c">100</td>
          <td class="cell"></td>
 
          <td class="cell_c">799.3000</td>
     </tr>

Анализируем данный фрагмент. Чтобы получить значение курса нам нужно написать регулярное выражение, которое получит весь код между уникальным кодом в
строке 4: <td class="cell_c">USD</td>
и
строке 9: </tr>
preg_match('#<td class="cell_c">USD</td>.*?</tr>#is', $text, $arr);

Таким образом, в элементе массива $arr[0] будет храниться следующее значение:
Array
(
[0] => <td class="cell_c">USD</td>
<td class="cell_c">100</td>
<td class="cell"></td>

<td class="cell_c">799.3000</td>
</tr>
)

Следующим регулярным выражением получим все значения между тегами
<td class="cell_c"> и </td>:
preg_match_all('#<td class="cell_c">(.*?)</td>#is', $arr[0], $arr);

Теперь содержимое массива $arr такое:
Array
(
[0] => Array
(
[0] => <td class="cell_c">USD</td>
[1] => <td class="cell_c">100</td>
[2] => <td class="cell_c">799.3000</td>
)

[1] => Array
(
[0] => USD
[1] => 100
[2] => 799.3000
)

)

Собственно, можно считать, что курс валют уже получен. Он храниться в $arr[1][2].
А дальше делайте с ним что необходимо. Можно сформировать строку и вывести её, как я сделал в своём рабочем примере (ссылка в начале статьи).
echo "{$arr[1][1]} {$arr[1][0]} = {$arr[1][2]} UAH";

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

Добавлено: 07 Апреля 2018 07:22:35 Добавил: Андрей Ковальчук