JavaRush /Java блог /Random UA /Регулярні вирази в Java, частина 5
Professor Hans Noodles
41 рівень

Регулярні вирази в Java, частина 5

Стаття з групи Random UA
Пропонуємо до вашої уваги переклад короткого посібника з регулярних виразів у мові Java, написаного Джеффом Фрісеном (Jeff Friesen) для сайту javaworld. Для простоти читання ми розділабо статтю кілька частин. Ця частина - остання. Регулярні вирази Java, частина 5 - 1Регулярні вирази в Java, частина 1 Регулярні вирази в Java, частина 2 Регулярні вирази в Java, частина 3 Регулярні вирази в Java, частина 4

Використання регулярних виразів для лексичного аналізу

Ще більш корисний додаток регулярних виразів – бібліотека, що допускає багаторазове використання коду для виконання лексичного аналізу, ключовий компонент будь-якого компілятора або асемблера. У цьому випадку вхідний потік символів групується у вихідний потік маркерів (token) – імен для послідовностей символів, що мають загальне значення. Наприклад, наткнувшись у вхідному потоці на послідовність символів c, o, u, n, t, e, r, лексичний аналізатор може вивести маркер ID(ідентифікатор). Відповідна маркеру послідовність символів називається лексемою (lexeme).
Ще про маркери та лексеми
Такі маркери, як ID, можуть відповідати багатьом послідовностям символів. У випадку подібних маркерів фактична лексема, що відповідає маркеру, теж необхідна компілятору, асемблеру або іншій утиліті, якій потрібен лексичний аналіз. Для маркерів, які представляють одну конкретну послідовність символів, як маркер PLUS, відповідний лише символу +, фактична лексема не потрібно, оскільки її можна [однозначно] визначити за маркером.
Регулярні вирази набагато ефективніші за лексичні аналізатори на основі [кінцевого] ​​стану, які необхідно писати вручну і зазвичай не можна використовувати повторно. Як приклад лексичного аналізатора на основі регулярних виразів можна навести JLex , лексичний генератор для мови Java, що використовує регулярні вирази для завдання правил розбиття вхідного потоку даних на маркери. Ще один приклад – Lexan.

Знайомимося з Lexan

Lexan - допускає багаторазове використання Java-бібліотека, призначена для лексичного аналізу. Вона заснована на коді із серії повідомлень із блогу Пишемо синтаксичний аналізатор на мові Java веб-сайту Cogito Learning . Бібліотека складається з наступних класів, які знаходяться в пакеті ca.javajeff.lexan, включеному в завантажуваний код цієї статті:
  • Lexan: лексичний аналізатор;
  • LexanException: виняток, що генерується в конструкторі класуLexan;
  • LexException: виняток, що генерується у разі виявлення неправильного синтаксису при лексичному аналізі;
  • Token: назва з атрибутом-регулярним виразом;
  • TokLex: пара маркер/лексема.
Конструктор Lexan(java.lang.Class tokensClass)створює новий лексичний аналізатор. Для нього потрібен один аргумент у вигляді об'єкта класу java.lang.Class, що відповідає класу констант типу static Token. За допомогою API Reflection конструктор читає всі константи Tokenв масив значень Token[]. Якщо констант Tokenнемає, генерується виняток LexanException. Регулярні вирази у Java, частина 5 - 2Клас 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. Конструктор цього об'єкта отримує рядок-назву маркера, разом із регулярним виразом, що описує всі рядки символів, що належать до цього маркеру. Для ясності бажано, щоб рядкова назва маркера збігалася з назвою константи, але це не обов'язково. Регулярні вирази у Java, частина 5 - 3Позиція константи 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 для компіляції коду. Регулярні вирази у Java, частина 5 - 4

Висновок

Регулярні вирази – корисний інструмент, який може стати у нагоді будь-якому розробнику. API Regex мови програмування Java спрощує їх використання у програмах та бібліотеках. Тепер, коли у вас вже є базове уявлення про регулярні вирази і цей API, загляньте в документацію SDK java.util.regex, щоб дізнатися ще більше про регулярні вирази і додаткові методи API Regex.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ