Поиск подстрок в тексте с использованием паттернов (patterns) в LuaПодписка на Комментарии к "Поиск подстрок в тексте с использованием паттернов (patterns) в Lua"

Аватар xxblx

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

Данный туториал подразумевает, что вы уже знакомы со строками (и соответствующей стандартной библиотекой) в Lua.

Рассмотрим простейший пример паттерна.

> print(string.find('some string', 'str'))
6    8

some string - строка
str - маска
string.find находит первое совпадение с маской и возвращает индекс начала и конца маски в строке.
Как видно из примера, s - шестой символ в строке, r - восьмой. Сразу стоит напомнить, в случае с Lua, шестой символ в строке носит индекс 6, а не 5, как было бы в некоторых других языках программирования, в Lua индекс начинается с 1 (единицы), а не с 0 (ноля).

Попробуем другой поиск по другой маске

> print(string.find('some string', 'z'))
nil

Результат - nil, так как символ z в строке не найден, z в указанной строке отсутствует.

Осуществлять поиск только по буквенным символам, как минимум, неудобно, тем более, что иногда известно только начало искомой под-строки. В этом случае на помощь приходят символьные классы (characters classes).

> print(string.find('some string', 'o..'))
2    4

. (точка) означает один любой символ. По этому в данном случае, маска o.. будет эквивалентна маске ome .

Использование символьных классов бывает особенно полезным при поиске под-строки

> print(string.match('some string', 'o..'))
ome

Паттерны в Lua включают предопределенные символьные классы (character class):
%a - буквенные символы
%d - числовые символы
%g - печатаемые символы, не включая пробел
%l - символы в нижнем регистре
%u - символы в верхнем регистре
%s - "пробелы"
и т.д.

> print(string.match('a1 AA', '%a'))
a
> print(string.match('a1 AA', '%d'))
1
> print(string.match('a1 AA', '%g'))
a
> print(string.match('a1 AA', '%l'))
a
> print(string.match('a1 AA', '%u'))
A
> print(string.find('a1 AA', '%s'))
3    3

Сразу стоит отметить, что если указать название символьного класса заглавной буквой, действие будет обратное. Т.е. %a - буквенные символы, %A - не буквенные символы, %d - числа, %D - не числа соответственно и т.д.

> print(string.match('a1 AA', '%a'))
a
> print(string.match('a1 AA', '%A'))
1
> print(string.match('a1 AA', '%d'))
1
> print(string.match('a1 AA', '%D'))
a

Помимо предопределенных символьных классов, паттерны в Lua включают предопределенные элементы (pattern item):
* (звездочка) - повторение предыдущего символа 0 или более раз. Этот элемент будет соответствовать максимальной длине возможной последовательности.

> print(string.match('ssszzz', 's*'))
sss
> print(string.match('ssszzz', 'i*'))

>

+ (плюс) - повторение предыдущего символа 1 или более раз. Этот элемент будет соответствовать максимальной длине возможной последовательности.

> print(string.match('ssszzz', 's+'))
sss

> print(string.match('ssszzz', 'i+'))
nil
>

- (минус) - повторение предыдущего символа 0 или более раз. В отличии от * (звездочки) этот элемент будет соответствовать минимальной длине возможной последовательности.

> print(string.match('ssszzz', 's-'))

> print(string.match('ssszzz', 'ss-'))
s
> print(string.match('ssszzz', 'sss-'))
ss
> print(string.match('ssszzz', 'i-'))

>

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

> print(string.match('aaa', 'aaab'))
nil
> print(string.match('aaa', 'aaab?'))
aaa
> print(string.match('aaa', 'aaa?'))
aaa

И т.д.
Посмотреть больше символьных классов и элементов можно в параграфе 6.4.1 документации Lua:
http://www.lua.org/manual/5.2/manual.html#6.4.1

Элементы можно использовать не только с отдельными конкретными символами, но и с классами

> print(string.match('a1 AA', '%g')) -- находит первый печатаемый символ, не-пробел
a
> print(string.match('a1 AA', '%g.')) -- находит первый печатаемый символ, не-пробел и повторяет печать символа один раз
a1
> print(string.match('a1aa AA', '%g*')) -- находит первый печатаемый символ, не-пробел и повторяет печать символов максимально возможное количество раз, пока не дойдет до первого пробела
a1aa

Помимо предопределенных символьных классов, можно создать и использовать свои. Для обозначения своего класса нужно заключить последовательность, диапазон символов, которые будут входить в класс, в квадратные скобки.
Создадим строку, включающую в себя строчные и прописные латинские буквы буквы, цифры.

> str = 'abcDEF012345'

Определим класс, включающий в себя все строчные английские буквы
> print(string.match(str, '[a-z]+'))
abc

Определим класс, включающий в себя все заглавные английские буквы
> print(string.match(str, '[A-Z]+'))
DEF

Определим класс, включающий в себя и строчные, и заглавные английские буквы
> print(string.match(str, '[A-z]+'))
abcDEF

Определим класс, включающий в себя цифры от 0 до 9
> print(string.match(str, '[0-9]+'))
012345

Определим класс, включающий в себя цифры от 0 до 9, строчные и заглавные английские буквы
> print(string.match(str, '[0-z]+'))
abcDEF012345

Определим класс, включающий в себя цифры от 2 до 5
> print(string.match(str, '[2-5]+'))
2345

Определим два класса: включающий в себя английские заглавные буквы E и F, а так же цифры от 0 до 2
> print(string.match(str, '[E-F]+[0-2]+'))
EF012

При определении диапазона класса можно использовать другие классы. Например, определим класс, включающий в себя цифры от 0 до 3 и все буквы в верхнем регистре (заглавные). Для указания букв в верхнем регистре воспользуемся предопределенным символьным классом %u.

> print(string.match(str, '[0-3%u]+'))
DEF0123

Добавим к этому классу строчную английскую букву "c".
> print(string.match(str, '[0-3%uc]+'))
cDEF0123

Возьмем строку 'abc456 -=+ABC123XYZxyz' и составим свой класс, маска с которым будет подходить для символов начиная со знака - (минус), заканчивая английской строчной буквой y.

> str = 'abc456 -=+ABC123XYZxyz'
> print(string.match(str, '[%p%u1-3xy]+'))
-=+ABC123XYZxy

%p - все пунктуационные символы, %u - буквы в верхнем регистре, 1-3 - цифры от 1 до 3, x и y - строчные x и y соответственно.

Изменим класс так, чтобы маска подходила для символов от строчной буквы a до знака = (равно).

> print(string.match(str, '[%a%s%d-=]+'))
abc456 -=

%a - все буквы, %s - пробелы, %d - цифры, - и = означают сами себя, соответственно.

Иногда бывает необходимо указать ищем мы от начала строки и до маски, либо, наоборот, от указанной маски и до конца строки. Для этого можно использовать ^ и $.
^ (галочка) - поиск с начала строки.
$ (доллар) - поиск до конца строки.

> str = 'abcdefg'
> print(string.match(str, '^.-d'))
abcd
> print(string.match(str, 'd.-$'))
defg

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

> str = '<div id="post" class="main-content"><p>здесь какой-то текст</p></div>'
> print(string.match(str, '<div id="post".->(.-)</div>'))
<p>здесь какой-то текст</p>

Рассмотрим детальней маску <div id="post".->(.-)</div> по частям.

<div id="post".->

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

(.-)</div>

Как и в предыдущей части, . означает один любой символ, а - означает минимальное количество повторений, таким образом, эта часть обозначает поиск символов, идущих сразу за <div ... > из первой части, до ближайшего </div>. Иными словами, под совпадение попадают символы между <div ... > из первой части и ближайшим </div> (выделено синим).

Как видно из примера выше, паттерны можно использовать, например, для простенького "парсинга". Но сразу стоит отметить, что в данном случае будет найдено только содержимое первых встретившихся тегов <div id="post"> </div>, последующие теги <div> </div> даже при наличии в них id="post" будут проигнорированы.

> str = [[<div id="post" class="main-content"><p>здесь какой-то текст</p></div>
>> <div id="post" class="sidebar-content"><p>и здесь снова какой-то тест</p></div>
>> ]]
> print(string.match(str, '<div id="post".->(.-)</div>'))
<p>здесь какой-то текст</p>

Чтобы найти содержимое не первых, а последних <div> </div> с id="post" потребуется изменить маску.
> print(string.match(str, '<div id="post".+>(<.-)</div>'))
<p>и здесь снова какой-то тест</p>

Рассмотрим детальней маску <div id="post".+>(<.-)</div> по частям:

<div id="post".+>

В этой части . означает один любой символ, а + в свою очередь означает максимально возможное количество повторений, но не менее одного (т.е. от одно или более повторений, при этом берется по максимуму). Точка и плюс (именно точка и плюс, а не вся <div id="post".+>, т.к. вся эта часть маски охватит всю строку) в случае с этой маской обхватывают следующую часть строки (выделено красным)

(<.-)</div>

</div> в этой части ограничивает действие .+ из предыдущей, но так как + подразумевает брать максимум повторений, под совпадение попадает именно последний в строке закрывающийся тег </div>.

И в полученном ищется новая подстрока, идущая перед последним </div> и начинающаяся с <, таким образом под совпадение попадает (выделено синим):

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

> str = 'bla bla bla bla'
> print(string.match(str, '(bla)+'))
nil

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

Нельзя использовать "или" в маске.

> str = 'bla bla bla bla la la la'
> print(string.match(str, '(bla|la)'))
nil

Данный пример демонстрирует, что с паттернами нельзя искать или одну маску, или другую. В данном случае, или "bla" или "la", как это можно было бы сделать при использовании регулярных выражений.
Иными словами, искать можно что-то одно только по одной маске.

Для более полного ознакомления с паттернами, рекомендую прочитать:

Помимо паттернов, стоит так же ознакомиться с использованием регулярных выражений в Lua (библиотека Lrexlib):

И с использованием РВ-грамматики в Lua (библиотека LPeg):

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

Похожие материалы:

Добавить комментарий