menu

Улучшаем токенизацию

Улучшаем токенизацию

 
( Бхабани Падхи )

Превращаем StringTokenizer в мощный токенизатор.

Tips 'N Tricks
 

Многие Java-программисты время от времени используют класс java.util.StringTokenizer. Это полезный класс, токенизирующий (разбивающий) строку символов на части, основываясь на разделителе, и возвращающий их по необходимости. (Токенизация является процессом разделения последовательности строки на части, понимаемые Вашей программой).

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

 Чтобы восполнить эти ограничения в Java 2 (JDK 1.2 и выше) включен класс BreakIterator, представляющий собой улучшенный токенизатор по сравнению с StringTokenizer. Однако т.к. этот класс отсутствует в JDK 1.1.X, разработчики часто проводят много времени, разрабатывая собственные классы, удовлетворяющие их требованиям. Большие проекты, так или иначе, включают обработку данных, и нет ничего необычного в том, что на рынке присутствует достаточно классов для этих целей. Эта статья преследует собой цель продемонстрировать Вам, как разрабатывать токенизаторы, базирующиеся на StringTokenizer.

Ограничения StringTokenizer

 Вы можете создать экземпляр StringTokenizer, используя один из следующих конструкторов:

  1. StringTokenizer(String sInput): Выделяет части используя в качестве разделителя следующие символы: " ", "\t", "\n".
  2. StringTokenizer(String sInput, String sDelimiter): Выделяет части используя в качестве разделителя sDelimiter.
  3. StringTokenizer(String sInput, String sDelimiter, boolean bReturnTokens): Тоже что и предыдущий, но если bReturnTokens установлен в true, разделитель также возвращается в качестве части.

 Первый конструктор не выполняет проверку наличия в строке подстроки. Поэтому строка "hello. Today \"I am \" going to my home town" разбивается на следующие части: hello., Today, "I, am, ", going, вместо: hello., Today, "I am ", going. Второй конструктор не отслеживает последовательное появление разделителя во входном потоке. Поэтому строка "book, author, publication,,,date published" делится (разделитель ",") на book, author, publication, and date published вместо шести значений book, author, publication, "", "", and date published, где "" означает строку нулевой длины. Чтобы поучить все шесть частей Вы должны установить параметр bReturnTokens в true.

 Особенность установки параметра в true, является очень важной т.к. наводит на мысль о возможном присутствии последовательного расположения разделителей. например, если данные поступают в динамическом режиме и предназначены для записи в таблицу базы данных, где введенные части строки ассоциированы с полями таблицы, то мы не можем правильно выполнить запись пока не будем уверены в том, какому полю таблицы соответствует пустая строка "". Рассмотрим пример, когда мы хотим добавить запись в таблицу с шестью полями, а входной поток содержит два последовательных разделителя. Результат, полученный из StringTokenizer, в этом случае будет состоять из пяти частей (т.к. два последовательных разделителя представляют "", которым StringTokenizer пренебрегает), а мы должны связать их с шестью полями. Т.к. мы не знаем, где встретилась последовательность разделителей, то соответственно, мы не можем принять решение, к какому полю относится пустая строка.

 Третий конструктор не будет работать также и в случае, когда информационная часть строки соответствует разделителю и находится внутри подстроки. После токенизации строки "book, author, publication,\",\",date published" мы получим book, author, publication, ", ", date published (шесть частей), вместо пяти (book, author, publication, , (the comma character), date published). Между прочим, установка bReturnTokens (третьего параметра конструктора) в true в этом случае не поможет.

Базовые требования к токенизатору

 Прежде чем начать разработку, Вы должны осознавать требования предъявляемые к хорошему токенизатору. Т.к. Java разработчики привыкли к использованию класса StringTokenizer, хороший токенизатор должен иметь полезные методы присущие его предшественнику, такие как: hasMoreTokens(), nextToken(), countTokens(). Исходный код, приведенный в этой статье, достаточно прост. За основу я взял StringTokenizer (с параметром bReturnTokens, равным true), используемый внутри и методами описанными выше. В некоторых случаях разделитель может выступать в качестве необходимой информационной части (очень редко), в некоторых нет, поэтому токенизатор должен будет поддерживать эту возможность при необходимости. Когда Вы создаете объект PowerfulTokenizer, передавая только строку и разделитель, он использует StringTokenizer с bReturnTokens, равным true. Чтобы токенизатор работал правильно, исходный код проверяет эту установку в нескольких местах (определяя количество частей в nextToken()).

 Как Вы могли заметить, PowerfulTokenizer воплощает интерфейс Enumeration, реализуя т.о. методы hasMoreElements() и nextElement(), которые просто делегируют вызовы к hasMoreTokens() и nextToken(), соответственно. Воплощая интерфейс Enumeration, класс становится обратно совместимым с StringTokenizer. Давайте рассмотрим пример. Возьмем, скажем, строку: "hello, Today,,, \"I, am \", going to,,, \"buy, a, book\"", а в качестве разделителя символ запятой. Результаты токенизации приведены в Таблице 1.

Тип Количество частей Части
StringTokenizer (bReturnTokens = true) 19 hello:,: Today:,:,:,: "I:,: am ":,: going to:,:,:,: "buy:,: a:,: book" (here the character : separates the tokens)
PowerfulTokenizer (bReturnTokens = true) 13 hello:,:Today:,:"":"":I, am:,:going to:,:"":"":buy a book (where "" means string of length 0)
PowerfulTokenizer (bReturnTokens = false) 9 hello:Today:"":"":I am:going to:"":"":buy a book
Таблица 1. Результаты токенизации строки

 Исходная строка содержит 11 запятых (,), три из которых находятся внутри подстрок, а четыре появляются последовательно (так Today,,, содержит два последовательных разделителя, т.к. первая запятая относится к Tobay). Это и является примером организации подсчета количества частей в случае PowerfulTokenizer:

  1. Если bReturnTokens=true, умножаем количество разделителей внутри подстроки на 2 и вычитаем этот результат из общего количества, чтобы получить количество частей. смысл этой формулы покажем на примере: StringTokenizer разделит строку "buy, a, book" на пять частей (buy : , : a : , : book), в то время как PowerfulTokenizer только одну (buy, a, book). Разница между этими результатами равна четырем, т.е. 2 (кол-во разделителей в подстроке) умноженное на 2. Эта формула работает хорошо для любых подстрок, содержащих разделители. Вы должны понимать, что в особом случае, когда разделитель сам по себе равен информационной части, в этом случае уменьшать количество частей нельзя.
  2. Подобным образом, когда bReturnTokens=false, вычитаем выражение [общее количество разделителей(11) – последовательные разделители(4) + кол-во разделителей в подстроке(3)] из общего количества частей с учетом самих разделителей(19) и получаем кол-во частей в строке. Т.к. мы не возвращаем сами разделители в этом случае, они бесполезны для нас, и вышеописанная формула производит количество частей равное 9. Эти две формулы, являются сердцем класса PowerfulTokenizer. Они работоспособны практически во всех случаях. Тем не менее, если у Вас более сложный вариант, необходимо разработать свою формулу удовлетворяющую его потребностям.
    // check whether the delimiter is within a substring
    for (int i=1; i<aiIndex.length; i++)
    {
        iIndex = sInput.indexOf(sDelim, iIndex+1);
        if (iIndex == -1)
            break;
        // if the delimiter is within a substring, then parse up to the
        // end of the substring.
        while (sInput.substring(iIndex-iLen, iIndex).equals(sDelim))
        {
            iNextIndex = sInput.indexOf(sDelim, iIndex+1);
            if (iNextIndex == -1)
                break;
            iIndex = iNextIndex;
        }
        aiIndex[i] = iIndex;
        //System.out.println("aiIndex[" + i + "] = " + iIndex);
        if (isWithinQuotes(iIndex))
        {
            if (bIncludeDelim)
                iTokens -= 2;
            else
                iTokens -= 1;
        }
    }

 Метод countTokens() проверяет, содержит ли исходная строка двойные кавычки. Если содержит, то количество частей уменьшается и индекс приравнивается индексу следующих кавычек (как показано в следующем листинге). Если bReturnTokens = false, тогда уменьшаем количество частей на количество непоследовательных разделителей во входной строке.

    // return "" as token if consecutive delimiters are found.
    if ( (sPrevToken.equals(sDelim)) && (sToken.equals(sDelim)) )
    {
        sPrevToken = sToken;
        iTokenNo++;
        return "";
    }

     // check whether the token itself is equal to the delimiter
    if ( (sToken.trim().startsWith("\"")) && (sToken.length() == 1) )
    {
        // this is a special case when token itself is equal to delimiter
        String sNextToken = oTokenizer.nextToken();
        while (!sNextToken.trim().endsWith("\""))
        {
            sToken += sNextToken;
            sNextToken = oTokenizer.nextToken();
        }
        sToken += sNextToken;
        sPrevToken = sToken;
        iTokenNo++;
        return sToken.substring(1, sToken.length()-1);
    }
    // check whether there is a substring inside the string
    else if ( (sToken.trim().startsWith("\""))
                      && (!((sToken.trim().endsWith("\""))
                      && (!sToken.trim().endsWith("\"\"")))) )
    {
        if (oTokenizer.hasMoreTokens())
        {
            String sNextToken = oTokenizer.nextToken();
            // check for presence of "\"\""
             while (!((sNextToken.trim().endsWith("\""))
                          && (!sNextToken.trim().endsWith("\"\""))) )
            {
                sToken += sNextToken;
                if (!oTokenizer.hasMoreTokens())
                {
                    sNextToken = "";
                    break;
                }
                sNextToken = oTokenizer.nextToken();
            }
            sToken += sNextToken;
        }
    }

 Метод nextToken() получает количество частей, используя StringTokenizer.nextToken(), и проверяет наличие двойных кавычек внутри каждой части. Если метод находит их, то поиск ведется до тех пор, пока не встретится вторая пара кавычек. Метод также сохраняет часть в переменной (sPrevToken), для проверки последовательного появления разделителей. Если nextToken() обнаруживает последовательное появление разделителей в потоке, он возвращает ""(строку нулевой длины).

 Подобным образом, hasMoreTokens() проверяет меньше ли, затребованное количество частей общему.

Сократите время разработки

 Эта статья демонстрирует принципы построения мощных токенизаторов. Используя эти концепции, Вы легко можете создавать новые токенизаторы, не тратя драгоценное время.

Об авторе

Bhabani Padhi — программист и архитектор на Java сейчас разрабатывает приложения для веба и предприятий используя технологию Java в UniteSys, Австралия. Ранее он работал в Baltimore Technologies, Австралия над системой обеспечения безопасности и в Fujitsu, Australia над разработкой сервера EJB. Бхабани интересуется распределенными вычислениями, мобильными и веб-приложениями с использованием технологии Java.

Ресурсы

Reprinted with permission from JavaWorld magazine. Copyright © ITworld.com, Inc., an IDG Communications company.
View the original article at: http://www.javaworld.com/ javaworld/javatips/jw-javatip112.html



Source: http://www.javaworld.com/ javaworld/javatips/jw-javatip112.html
Category: Java | Added by: tsvetkov (23.01.2009)
Views: 1935 | Rating: 0.0/0