Салимóненко Дмитрий Александрович

Операционные системы

Задание 2: Пример использования регулярных выражений

(C++, Linux)

Введение


Регулярное выражение – это технология поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов. Для поиска используется строка-образец (англ. pattern, по-русски её часто называют «шаблоном», «маской»), состоящая из символов и метасимволов и задающая правило поиска. Иными словами, регулярные выражения облегчают обработку текста.

Регулярные выражения используются во многих современных языках программирования: javascript, PHP, Ruby, Pithon, C++... И даже в С. Правда, в С работа с ними несколько ограничена, возможностей – немного. Но, тем не менее. Тогда как во многих других языках их использование позволяет, зачастую, одной-двумя строчками кода выполнить трудоемкую операцию по преобразованию текста в соответствии с тем или иным шаблоном, определить число вхождений соответствующей подстроки в пределах всего текста или его части.

Отдельно стоит сказать о необходимости использования синтаксиса и возможностей регулярных выражений, например, в командной строке (bash) Linux. А также, что очень существенно – для настроек сайта и сервера в целом, например, в файлах .htaccess.

Иными словами, технология регулярных выражений хотя и видится второстепенной (допустим, по сравнению просто со «знанием языка»), но, тем не менее, хотя бы представление о них и элементарные навыки работы и с ними видятся целесообразными. Вот об этом и поговорим ниже.

Общее понятие

Технология регулярных выражений подразумевает собой выполнение поиска и/или замены части текста, соответствующей шаблону, на что-нибудь другое. Шаблон как раз и представляет собой регулярное выражение. Шаблон записывается в соответствующем формате, который строго регламентирован. Кстати, в разных языках программирования шаблоны регулярных выражений имеют некоторые отличия. Функции по использованию регулярных выражений тоже различаются.

Простейший пример. У нас есть текст:

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

Мы хотим заменить в этом тексте все вхождения, соответствующие шаблону «н… о» (где представляет собой ноль или более любых символов, необязательно одинаковых) на подстроки вида «tt3». Само регулярное выражение, например, для языка С++, при помощи которого возможно будет осуществить такую замену, может иметь следующий вид:

“н(.*?)о”

При применении указанного шаблона к вышеприведенному тексту получим:

Техtt3логия регулярtt3дразумевает собой выполtt3иска и/или замеtt3ответствующей шаблоtt3-tt3е.

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

Где об этом прочитать?

Если говорить о сетевых ресурсах, где в доступной форме, для новичка, рассказывается о регулярных выражениях, то здесь, к сожалению, ждет некое разочарование. Вроде, сайтов-то много, где об этом пишут. Да вот как-то написано так, что… Такое впечатление, что подавляющее большинство статей в интернете на эту тему написано не программистами, а журналистами или т.п. На мой взгляд, вот здесь, пожалуй, рассказано о регулярных выражениях мало-мальски понятно и наглядно. А вообще, конечно, по моему, довольно длительному, опыту, даже не стоит особо тратить время на поиски в интернете (за исключением, пожалуй, определенного, частного класса задач). Оптимальный вариант – это прочитать, например, вот эту книгу:

Гойвертс Я., Левитан С. Регулярные выражения. Сборник рецептов. – Пер. с англ. – СПб.: Символ-Плюс, 2010. – 608 с., ил.

Год издания, в принципе, неважен. Книга продается, например, в интернет-магазинах «Books.ru», «Ozon». В книге содержатся - не только детальное, обстоятельное разъяснение самой специфики регулярных выражений (что, повторюсь, практически невозможно прочитать в интернете), но и полезные практические примеры.

Основные методы, классы регулярных выражений в языке С++ приводятся, например, здесь. Следует учесть, что, если в РНР, Perl или javascript (и т.п.) регулярные выражения являются уже встроенными, то в С++ подавляющее большинство библиотек – сторонние. Говорят, что одна из лучших – это Boost. Есть также libpcre и т.д. Кроме того, ряд из них разработан относительно недавно. Но, существует и встроенная библиотека, например, в Ubuntu (Linux).

Это, в частности, библиотека regex, которая работоспособна, начиная с 2011 г. Речь идет о стандарте С++11. Кстати, этот факт обуславливает необходимость использования соответствующей опции при компиляции программы, использующей регулярные выражения (см. ниже).

Кроме того, есть еще кроссплатформенная библиотека pcre.h. Это библиотека используется, например, в ядре РНР.

Мы с Вами будем использовать библиотеку regex, благо она, с одной стороны, является встроенной в современную Ubuntu. С другой стороны, обладает достаточной функциональностью. Единственный, пожалуй, ее минус: компиляция программы с ее присутствием происходит довольно долго (хотя, возможно, другие библиотеки в этом плане - не лучше). Но, работает потом программа быстро.

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

Их синтаксис, отчасти, напоминает, скажем так, обстановку не очень-то приятного сна или еще чего подобного. Особенно, если браться за дело впервые. Однако, есть, как минимум, два момента. Первый: изучается он ОДИН раз (если, конечно, при этом использовать дельные учебные руководства, а не отрывочно-интернетно-форумные сведения). Второй: этот синтаксис применим не только при работе с текстами, но и, к примеру, в файлах настроек серверов хостингов. Так что хотя бы попробовать с ними повзаимодействовать, хоть немного изучить - стоит.
Пример: удаление фавиконки из html-кода страницы средствами регулярных выражений в С++

Для чего может это понадобиться? Не лучше ли, мол, использовать для этого тот же javascript или соответствующий серверный язык высокого уровня?

Необходимость может возникнуть, например, при разработке «низкоуровневых» программ – серверов, снифферов. Используемых, в том числе, для перехвата вебстраницы, модификации ее требуемым образом и только потом – передача браузеру. В ситуации, когда необходимо обработать отрывок (тем более, если он – грубо невалидный) html кода это также актуально. Тем более, что, к примеру, современный браузер не позволит работать с невалидным кодом: он, в меру своих способностей и умений, будет вначале пытаться исправить его и только потом сформирует из него DOM и начнет отрисовывать страницу.

Рассмотрим простейший программный код на С++:

#include <iostream>
#include <regex>
#include <stdio.h>
#include <stdlib.h>
// #include <string>
// #include <fcntl.h>
// #include <unistd.h>
using namespace std;
#define BS 64
char *b, buf[BS];
string str = "", ex, str_to_change;

int main()
{
ssize_t count;
FILE *fd = fopen("f.html", "r");
FILE *file = fopen("to.html", "w");
while(!feof(fd))
  {if( fgets(buf, BS, fd))
  str = str + buf;
  }
if (str == "") cout<< "Not to read from file";
fclose(fd);

  regex fav ("<link(.*?)favicon\\.png(.*?)>");
  str_to_change = "<!-- favicon deleted -->";
  ex = regex_replace(str, fav, str_to_change);  // Cut favicons
  b = (char*)ex.c_str();
  cout<<b<<endl; // Вывод на экран

   if (file) // если есть доступ к файлу,
   {
       if ((bool)fputs(b, file)) // если запись произошла успешно
           cout << "File is written successfully." << endl;
   }
   else
   cout << "Cannot access to write the file!" << endl;

fclose(file);
   return 0;
}


Как видим, используется библиотека regex, а также ряд стандартных, проверенных временем, библиотек.

Исходный файл f.html содержит в себе отрывок кода html примерно такого вида:

HTTP/1.1 200 OK
Server: nginx
Date: Thu, 14 Dec 2017 01:07:13 GMT
Content-Type: text/html
Content-Length: 21517
Connection: closeVary: Accept-Encoding
Accept-Ranges: bytes

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html  xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="author" content="Sea" />
<title>Салимоненко Д.А.</title><meta name="keywords" content="Салимоненко Дмитрий Александрович, сайт персональный"/>
<meta name="description" content="Персональный сайт Салимоненко Дмитрия Александровича"/>
<meta name='yandex-verification' content='62a873910feb59b4' />
  <link rel="icon" href="http://www.4846d.ru/favicon.png" type="image/png" />
  <link rel="shortcut icon" href="http://www.4846d.ru/favicon.png" type="image/png" />

<link href="dropdown.css" media="all" rel="stylesheet" type="text/css" />
<link rel="stylesheet" type="text/css" media="all" href="style.css" />
<meta http-equiv="Content-Type" content="text/html;
charset=windows-1251"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<style type="text/css">
</style>

Этот код взят с моего сайта (заголовки протоколов ответа сервера взяты из консоли браузера).


Компиляция программы

Компиляция осуществляется следующим образом:

g++ reg.cpp -o reg -std=c++11

  • reg.cpp – имя исходного файла (для С++),
  • reg – имя выходного (исполняемого) файла,
  • -std=c++11 – опция, указывающая на необходимость использования новой версии С++11 (в отличие от ранее использовавшейся обычной С++).

Надо сказать, что именно благодаря этой опции компиляция даже этой, очень простой, программы выполняется довольно долго. Скажем, у меня на компьютере – до 1 минуты. Тогда как без нее аналогичные программы компилируются в течение, наверное, не более 0,1…0,5 с. И, кстати, файл с исполняемым кодом вместо положенных нескольких килобайт занимает, при статической компиляции,... полмегабайта! Однако, эта опция необходима в силу наличия библиотеки regex. Без этой опции программа компилироваться не будет.


Что делает приведенный программный код на С++?

Да, в общем-то, ничего особенного. Вначале – открывает два файла: исходный – для чтения, и результативный – для записи. После чего осуществляется поблочное чтение из исходного файла, в итоге – все его содержимое формируется в одну строку.

А дальше – применяется регулярное выражение (шаблон) вида:

"<link(.*?)favicon\\.png(.*?)>"

После работы регулярного выражения, строка (представляющая собой все содержимое файла f.html, соответствующим образом измененное) записывается в файл to.html.

Обсуждение

Обратите внимание, на то, что указанный шаблон регулярного, отчасти, совпадает с двумя строчками в html-коде, а именно:

<link rel="icon" href="http://www.4846d.ru/favicon.png" type="image/png" />
<link rel="shortcut icon" href="http://www.4846d.ru/favicon.png" type="image/png" />

Жирным цветом выделены совпадающие части шаблона регулярного выражения и текста (html-кода).

Вроде как, никаких сложностей нет. Единственное, в чем может возникнуть вопрос – это два обратных слеша после слова «favicon» в шаблоне регулярного выражения.

Дело в том, что точка (впрочем, не только она, а и многие другие аналогичные символы, например, ^, |, [, ], {, }, *, (, ), $), находящаяся перед расширением файла с фавиконкой (т.е. перед символами «png») является в технологии регулярных выражений специальным символом, имеющим специальное (технологические) значение. В частности, она может обозначать любой символ. Если же есть необходимость, чтобы в пределах регулярного выражения присутствовали точка – именно как обычная точка, безо всякой функциональной нагрузки, тогда ее необходимо экранировать – вот для этого-то и применены два рядом стоящих слеша.

Отметим, что, к примеру, в javascript или РНР допускается экранировать специальные символы при помощи лишь ОДНОГО слеша (а не двух).

Также отметим, что в шаблоне регулярного выражения НЕДОПУСТИМО делать пробелы без необходимости. В противоположном случае, они будут интерпретироваться (тогда как если сделать несколько пробелов подряд, скажем, в html-коде, браузер, если не принять специальных мер, превратит их в ОДИН пробел). Этим, кстати, технология регулярных выражений отличается от многих языков программирования. Например, в строчках кода на С++, за исключением регулярных выражений и, быть может, текстовых строк – констант, пробелы никак не влияют на работу программы.

Таким образом, в силу того, что шаблон регулярного выражения совпадает с двумя имеющими подстроками, содержащимися в файле f.html, последние (и, в данном случае, только они) будут подвергаться обработке технологией регулярных выражений – при помощи функции regex_replace. Которая осуществляет замену указанных подстрок на то, чему равен ее третий параметр - str_to_change. А он равен, как видим, следующему строковому значению:

"<!-- favicon deleted -->"

Это – не что иное, как комментарии кода html, содержащие внутри себя слова  favicon deleted.

Таким образом, после того, как программа отработает, в файл to.html будет записан примерно такой текст:

HTTP/1.1 200 OK
Server: nginx
Date: Thu, 14 Dec 2017 01:07:13 GMT
Content-Type: text/html
Content-Length: 21517
Connection: close
Vary: Accept-Encoding
Accept-Ranges: bytes

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html  xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="author" content="Sea" /><title>Ñàëèìîíåíêî Ä.À.</title>
<meta name="keywords" content="Ñàëèìîíåíêî Äìèòðèé Àëåêñàíäðîâè÷, ñàéò ïåðñîíàëüíûé"/>
<meta name="description" content="Ïåðñîíàëüíûé ñàéò Ñàëèìîíåíêî Äìèòðèÿ Àëåêñàíäðîâè÷à"/>
<meta name='yandex-verification' content='62a873910feb59b4' />  
<!-- favicon deleted -->
<!-- favicon deleted -->

<link href="dropdown.css" media="all" rel="stylesheet" type="text/css" />
<link rel="stylesheet" type="text/css" media="all" href="style.css" />
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<style type="text/css">
</style>

Как видим, на месте подстрок, задававших фавиконки, теперь присутствуют комментарии.

Отдельно стоит сказать, что кириллический (русский) текст записался в файл в нечитаемых символах. Причина тому состоит в том, что у меня на сайте установлена кодировка windows-1251, а в Linux, по умолчанию, utf-8. Текст кириллицы, записанный в одной кодировке, выглядит нечитаемо при отображении в другой кодировке. Как вариант, можно в Linux сменить utf-8 – отображаемую кодировку этого файла – на windows-1251, тогда его содержимое станет читаемым.

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

Или, если файл to.html, скопировав из Linux, вставить в Windows (в какой-нибудь каталог) и открыть, например, в Notepad++, то он будет также полностью читаемым и корректным. Ибо в операционной системе Windows (в России!) установлена как раз кодировка windows-1251.

Комбинация (.*?) означает следующее:

  • Скобки (   ) – это группа,
  • . – любой символ,
  • * означает, что символ (или группа) непосредственно идущий перед *, может повторяться в строке текста, как минимум, ноль раз,
  • ? – это, так называемый, «жадный» квантификатор, задающий наименее возможную область поиска по строке текста (при конкретной замене; для последующей замены, быть может, будет уже иная, тоже минимальная, область поиска).

Еще вопрос может возникнуть, пожалуй, по поводу функций fgets/fputs. О них наиболее понятно, на мой взгляд, описано здесь.

Кстати, что актуально особенно тем, кто только начинает изучать программирование: сайт по приведенной ссылке - один из немногих, где, с одной стороны, кратко, а, с другой стороны - обстоятельно и с примерами описываются функции языков С/С++. К сожалению, не все. Но, для тех, что есть - лучшего описания в интернете я не встречал.

Последние замечания

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

Строго рекомендуется использовать для открытия/закрытия файла, для считывания из него и записи – функции ОДНОГО И ТОГО ЖЕ ВИДА!

Иными словами, если Вы используете для открытия файла, скажем, fopen, то следует для чтения/записи/закрытия файла использовать аналогичные (начинающиеся на “f”) функции, например, fgets, fputs, fread, fwrite, fclose.

Ну, а если Вы открыли файл при помощи системного вызова open, так тогда уж и использовать надо read, write, close – соответственно.

Если используется объектный подход в С++ (fstream), то, однозначно, уже не присутствовать функций типа fread, например.

Почему так? Дело в том, что, с одной стороны, вроде как, ПРЯМОГО запрета на использование разных подходов (системных вызовов) в С/С++ - нет. Языки эти, вроде как, универсальные. Однако, по моему (небольшому, но, тем не менее) опыту – если, скажем, открыть файл функцией одного вида, а читать из него, записывать в него – функциями другого вида…, то, программа, конечно, как-то там работает. Однако, ей ничего не стоит, скажем, добавить в файл N-е количество нулевых символов… а то и неких странных символов – вперемежку с тем, что должно там быть. И делать она может это совершенно свободно.

Теоретически, вроде бы, что там – ну, открыли файл, прочитали из него сколько-то символов, объединили их в строку. Ну, и что, мол, строка  в С++ - она и есть строка.

Ан, нет. Видимо, все же, разные команды (read и fread, к примеру) читают файл немного по-разному. Соответственно, и то, что потом формируется как «строка», тоже имеет немного разные (на низком уровне) вид и характеристики. Возможно, отчасти, дело – в разных типах данных, возвращаемых этими функциями (делающими, казалось бы, одно и то же). Хотя, преобразование данных не всегда помогает. Компилятор, естественно, при этом ошибок не замечает. Предупреждений тоже не выдает. Так что не все столь уж универсально, как может показаться.

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

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

Внимательный читатель тут же заметит: а что же, мол, Вы сам-то, автор статьи, используете в программных кодах смешанные технологии - как С, так и С++. Даже библиотеки для "разных" языков в ОДНОМ коде. Что ответить - даже не знаю. Наверное, привычка такая. Что на ум придет первое - то и использую. НО: при этом четко отдавая себе отчет, ЧТО же я делаю. Хотя, конечно, согласен, это - не совсем корректный стиль программирования. Да и, честно говоря, мне С как-то ближе к душе (ибо - быстрее работает), чем С++. Поэтому, если есть возможность, стараюсь отдать приоритет первому. Наверное, поэтому и получаются такие вот смешанные коды.

Конечно, что касается языков программирования высокого уровня, типа С#, не говоря уже о РНР, Питоне – там все гораздо проще. В том же РНР, скажем, даже необязательно определять тип данных – интерпретатор делает это самостоятельно. Если строка – так она и есть СТРОКА (как минимум), неважно, при помощи каких действий, команд – полученная. А, если возможно, эта строка может быть еще и числом, участвовать в арифметических операциях.

Правда, даже в РНР мне, иной раз, все-таки, приходилось принудительно задавать тип данных – строковый, в частности (хотя, вроде как, и так уж строка – куда более «строчно»-то). Иначе некорректно работали некоторые функции, например, обрабатывающие дерево XML, аргументами которых являлись строки.

Впрочем, вот что. В универсальном РНР все хорошо до тех пор, пока. А когда Юникод там (особенно, некорректный) или еще что, – то могут наблюдаться интересные вещи. В этом смысле язык С++ (и, особенно, С), конечно, выигрывает.

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

Решением здесь может быть (исключительная) блокировка файла (только) на момент записи, затем – его закрытие и, тут же, вновь, повторное открытие для последующей работы. Похожий подход, кстати, используется в ряде случаев в Web-технологиях при централизованном управлении клиентами в модели удаленного доступа (о чем я рассказываю в рамках лекций по предмету "Вычислительные системы, сети и телекоммуникации").

Задание для самостоятельной работы

Необходимо:

  1. Детально разобраться в предложенном коде, дополнив его, по крайней мере, обработкой ошибок, которые могут возникнуть при открытии файла, на предмет максимального количества байтов в файле, при неудаче чтения из исходного файла, неудачи записи в результативный файл.
  2. Запустить код, опробовав и изучив его работу.
  3. Поэкспериментировать с другими регулярными выражениями, чтобы можно было производить какие-то другие виды обработки текста, в данном случае – кода html. Например, это могло бы быть добавление сообщения в начало страницы, окраска границы тега <body> в черный цвет. Последнее пригодится для выполнения Задания 4 по ВССТ.

С уважением, Салимоненко Д.А.