Мой родной 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 Добавил: Андрей Ковальчук