Пропонуємо до вашої уваги переклад короткого посібника з регулярних виразів у мові Java, написаного Джеффом Фрісеном (Jeff Friesen) для сайту javaworld. Для простоти читання ми розділабо статтю кілька частин. Ця частина - остання. Регулярні вирази в Java, частина 1 Регулярні вирази в Java, частина 2 Регулярні вирази в Java, частина 3 Регулярні вирази в Java, частина 4
Регулярні вирази набагато ефективніші за лексичні аналізатори на основі [кінцевого] стану, які необхідно писати вручну і зазвичай не можна використовувати повторно. Як приклад лексичного аналізатора на основі регулярних виразів можна навести JLex , лексичний генератор для мови Java, що використовує регулярні вирази для завдання правил розбиття вхідного потоку даних на маркери. Ще один приклад – Lexan.
Використання регулярних виразів для лексичного аналізу
Ще більш корисний додаток регулярних виразів – бібліотека, що допускає багаторазове використання коду для виконання лексичного аналізу, ключовий компонент будь-якого компілятора або асемблера. У цьому випадку вхідний потік символів групується у вихідний потік маркерів (token) – імен для послідовностей символів, що мають загальне значення. Наприклад, наткнувшись у вхідному потоці на послідовність символівc
, o
, u
, n
, t
, e
, r
, лексичний аналізатор може вивести маркер ID
(ідентифікатор). Відповідна маркеру послідовність символів називається лексемою (lexeme).
Ще про маркери та лексеми |
---|
Такі маркери, як ID, можуть відповідати багатьом послідовностям символів. У випадку подібних маркерів фактична лексема, що відповідає маркеру, теж необхідна компілятору, асемблеру або іншій утиліті, якій потрібен лексичний аналіз. Для маркерів, які представляють одну конкретну послідовність символів, як маркер PLUS , відповідний лише символу + , фактична лексема не потрібно, оскільки її можна [однозначно] визначити за маркером. |
Знайомимося з Lexan
Lexan - допускає багаторазове використання Java-бібліотека, призначена для лексичного аналізу. Вона заснована на коді із серії повідомлень із блогу Пишемо синтаксичний аналізатор на мові Java веб-сайту Cogito Learning . Бібліотека складається з наступних класів, які знаходяться в пакетіca.javajeff.lexan
, включеному в завантажуваний код цієї статті:
Lexan
: лексичний аналізатор;LexException
: виняток, що генерується у разі виявлення неправильного синтаксису при лексичному аналізі;Token
: назва з атрибутом-регулярним виразом;TokLex
: пара маркер/лексема.
LexanException
: виняток, що генерується в конструкторі класуLexan;
Lexan(java.lang.Class tokensClass)
створює новий лексичний аналізатор. Для нього потрібен один аргумент у вигляді об'єкта класу java.lang.Class
, що відповідає класу констант типу static Token
. За допомогою API Reflection конструктор читає всі константи Token
в масив значень Token[]
. Якщо констант Token
немає, генерується виняток LexanException
. Клас Lexan
також надає такі два методи:
- Метод повертає перелік цього лексичного аналізатора;
List
getTokLexes() Token
Метод void lex(String str)
виконує лексичний аналіз вхідного рядка [з поміщенням результату] до списку значень типуTokLex
. У разі виявлення символу, що не відповідає жодному з шаблонів масивуToken[]
, генерується винятокLexException
.
LexanException
методів немає, він використовує для повернення повідомлення виключення успадкований метод getMessage()
. На відміну від нього, клас LexException
надає такі методи:
- Метод
int getBadCharIndex()
повертає позицію символу, що не відповідає жодному шаблону маркерів. - Метод
String getText()
повертає аналізований при генерації виключення текст.
Token
перевизначає метод toString()
повернення назви маркера. Він також надає метод String getPattern()
, що повертає атрибут регулярного вираження маркера. Клас TokLex
надає метод Token getToken()
, який повертає його маркер. Він також надає метод String getLexeme()
, який повертає його лексему.
Демонстрація роботи бібліотеки Lexan
Для демонстрації роботи бібліотекиLexan
я написав додаток LexanDemo
. Воно складається з класів LexanDemo
, BinTokens
, MathTokens
та NoTokens
. Вихідний код програми LexanDemo
наведено у лістингу 2. Лістинг 2. Демонстрація роботи бібліотеки Lexan
import ca.javajeff.lexan.Lexan;
import ca.javajeff.lexan.LexanException;
import ca.javajeff.lexan.LexException;
import ca.javajeff.lexan.TokLex;
public final class LexanDemo
{
public static void main(String[] args)
{
lex(MathTokens.class, " sin(x) * (1 + var_12) ");
lex(BinTokens.class, " 1 0 1 0 1");
lex(BinTokens.class, "110");
lex(BinTokens.class, "1 20");
lex(NoTokens.class, "");
}
private static void lex(Class tokensClass, String text)
{
try
{
Lexan lexan = new Lexan(tokensClass);
lexan.lex(text);
for (TokLex tokLex: lexan.getTokLexes())
System.out.printf("%s: %s%n", tokLex.getToken(),
tokLex.getLexeme());
}
catch (LexanException le)
{
System.err.println(le.getMessage());
}
catch (LexException le)
{
System.err.println(le.getText());
for (int i = 0; i < le.getBadCharIndex(); i++)
System.err.print("-");
System.err.println("^");
System.err.println(le.getMessage());
}
System.out.println();
}
}
Метод main()
з лістингу 2 викликає утиліту lex()
для демонстрації лексичного аналізу за допомогою Lexan. При кожному виклику цьому методу передається клас маркерів в об'єкті Class
та рядок, аналіз якого необхідно виконати. Метод lex()
спочатку створює об'єкт класу Lexan
, передаючи об'єкт Class
конструктору класу Lexan
. А потім він викликає метод lex()
класу Lexan
для цього рядка. У разі успішного виконання лексичного аналізу з метою повернення списку об'єктів TokLex
викликається метод getTokLexes()
класу Lexan
. Для кожного з цих об'єктів викликається його метод getToken()
класу TokLex
для повернення маркера та його методgetLexeme()
для повернення лексеми. Обидва значення виводяться у стандартний потік виведення. У разі невдачі лексичного аналізу генерується і обробляється відповідним чином один із винятків LexanException
або LexException
. Для стислості, розглянемо, зі складових цю програму, тільки клас MathTokens
. У лістингу 3 показаний вихідний код. Лістинг 3. Опис набору маркерів для невеликої математичної мови
import ca.javajeff.lexan.Token;
public final class MathTokens
{
public final static Token FUNC = new Token("FUNC", "sin|cos|exp|ln|sqrt");
public final static Token LPAREN = new Token("LPAREN", "\\(");
public final static Token RPAREN = new Token("RPAREN", "\\)");
public final static Token PLUSMIN = new Token("PLUSMIN", "[+-]");
public final static Token TIMESDIV = new Token("TIMESDIV", "[*/]");
public final static Token CARET = new Token("CARET", "\\^");
public final static Token INTEGER = new Token("INTEGER", "[0-9]+");
public final static Token ID = new Token("ID", "[a-zA-Z][a-zA-Z0-9_]*");
}
З лістингу 3 видно, що клас MathTokens
визначає послідовність констант типу Token
. Кожній із них присвоюється значення об'єкта Token
. Конструктор цього об'єкта отримує рядок-назву маркера, разом із регулярним виразом, що описує всі рядки символів, що належать до цього маркеру. Для ясності бажано, щоб рядкова назва маркера збігалася з назвою константи, але це не обов'язково. Позиція константи Token
у списку маркерів є важливою. Розташовані вище списку константи Token
мають пріоритет перед розташованими нижче. Наприклад, зустрівши sin
, Lexan вибирає маркер FUNC
замість ID
. Якби маркер ID
передував маркеру FUNC
, то був би обраний він.
Компіляція та запуск програми LexanDemo
Завантажуваний код цієї статті включає архівlexan.zip
, що містить всі файли дистрибутива Lexan. Розпакуйте цей архів і перейдіть в підкаталог demos
кореневого каталогу lexan
. Якщо ви використовуєте Windows, виконайте наступну команду для компіляції файлів вихідного коду демо-програми:
javac -cp ..\library\lexan.jar *.java
У разі успішної компіляції виконайте наступну команду для запуску демо-додатків:
java -cp ..\library\lexan.jar;. LexanDemo
Ви повинні побачити такі результати:
FUNC: sin
LPAREN: (
ID: x
RPAREN: )
TIMESDIV: *
LPAREN: (
INTEGER: 1
PLUSMIN: +
ID: var_12
RPAREN: )
ONE: 1
ZERO: 0
ONE: 1
ZERO: 0
ONE: 1
ONE: 1
ONE: 1
ZERO: 0
1 20
--^
Неожиданный символ во входном тексте: 20
Повідомлення Неожиданный символ во входном тексте: 20
виникає в результаті генерації виключення LexanException
, викликаного тим, що в класі BinTokens
не описана константа Token
зі значенням 2
як регулярний вираз. Зверніть увагу, що обробник винятків вивів отриману в результаті лексичного аналізу тексту позицію невідповідного символу. Повідомлення маркери відсутні виходить у результаті генерації винятку LexException
, викликаного тим, що в класі NoTokens
не описано жодних констант Token
.
За лаштунками
Lexan
використовує як свій "движок" клас Lexan. Погляньте на реалізацію цього класу в лістингу 4 і відзначте внесок регулярних виразів у можливість повторного використання "движка". Лістинг 4. Створення архітектури лексичного аналізатора на основі регулярних виразів
package ca.javajeff.lexan;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
/**
* Лексический анализатор. Этот класс можно использовать для
* преобразования входного потока символов в выходной поток маркеров.
*
* @Автор Джефф Фризен
*/
public final class Lexan
{
private List tokLexes;
private Token[] values;
/**
* Инициализируем лексический анализатор набором об'єктов Token.
*
* @параметры tokensClass – об'єкт Class класса, содержащего
* набор об'єктов Token
*
* @генерирует исключение LexanException в случае невозможности
* формирования об'єкта Lexan, возможно, из-за отсутствия об'єктов
* Token в классе
*/
public Lexan(Class tokensClass) throws LexanException
{
try
{
tokLexes = new ArrayList<>();
List _values = new ArrayList<>();
Field[] fields = tokensClass.getDeclaredFields();
for (Field field: fields)
if (field.getType().getName().equals("ca.javajeff.lexan.Token"))
_values.add((Token) field.get(null));
values = _values.toArray(new Token[0]);
if (values.length == 0)
throw new LexanException("маркеры отсутствуют");
}
catch (IllegalAccessException iae)
{
throw new LexanException(iae.getMessage());
}
/**
* Получаем список TokLex'ов этого лексического анализатора.
*
* @возвращает список TokLex'ов
*/
public List getTokLexes()
{
return tokLexes;
}
/** * Выполняет лексический анализ входной строки [с помещением * результата] в список TokLex'ов. * * @параметры str – строка, подвергаемая лексическому анализу * * @генерирует исключение LexException: во входных данных обнаружен * неожиданный символ */
public void lex(String str) throws LexException
{
String s = new String(str).trim(); // удалить ведущие пробелы
int index = (str.length() - s.length());
tokLexes.clear();
while (!s.equals(""))
{
boolean match = false;
for (int i = 0; i < values.length; i++)
{
Token token = values[i];
Matcher m = token.getPattern().matcher(s);
if (m.find())
{
match = true;
tokLexes.add(new TokLex(token, m.group().trim()));
String t = s;
s = m.replaceFirst("").trim(); // удалить ведущие пробелы
index += (t.length() - s.length());
break;
}
}
if (!match)
throw new LexException("Неожиданный символ во входном тексте: "
+ s, str, index);
}
}
}
Код методу lex()
базується на коді, наведеному у повідомленні блогу "Пишемо синтаксичний аналізатор на мові Java: Генератор маркерів" веб-сайту Cogito Learning. Прочитайте це повідомлення, щоб дізнатися більше про те, як Lexan використовує API Regex для компіляції коду.
Висновок
Регулярні вирази – корисний інструмент, який може стати у нагоді будь-якому розробнику. API Regex мови програмування Java спрощує їх використання у програмах та бібліотеках. Тепер, коли у вас вже є базове уявлення про регулярні вирази і цей API, загляньте в документацію SDKjava.util.regex
, щоб дізнатися ще більше про регулярні вирази і додаткові методи API Regex.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ