Салимóненко Дмитрий Александрович
Задание 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
, тогда его содержимое станет читаемым.
Или, если файл 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-технологиях при централизованном управлении клиентами в модели удаленного доступа (о чем я рассказываю в рамках лекций по предмету "Вычислительные системы, сети и телекоммуникации").
Задание для самостоятельной работы
Необходимо:
- Детально разобраться в предложенном коде, дополнив его, по крайней мере, обработкой ошибок, которые могут возникнуть при открытии файла, на предмет максимального количества байтов в файле, при неудаче чтения из исходного файла, неудачи записи в результативный файл.
- Запустить код, опробовав и изучив его работу.
- Поэкспериментировать с другими регулярными выражениями, чтобы можно было производить какие-то другие виды обработки текста, в данном случае – кода html. Например, это могло бы быть добавление сообщения в начало страницы, окраска границы тега <body> в черный цвет. Последнее пригодится для выполнения Задания 4 по ВССТ.
С уважением, Салимоненко Д.А.