Как добавить функции, отсутствующие в реализации Java regex?

Я новичок в Java. Как разработчик .Net, я очень привык к классу Regex в .Net. Реализация Java Regex (Regular Expressions) не плоха, но в ней отсутствуют некоторые ключевые функции.

Я хотел создать свой собственный вспомогательный класс для Java, но я подумал, что, возможно, уже есть один доступный. Итак, есть ли бесплатный и простой в использовании продукт для Regex на Java или я должен сам его создать?

Если бы я написал свой собственный класс, как вы думаете, я должен поделиться им с другими, чтобы использовать его?


[изменить]

Были жалобы на то, что я не рассматривал проблему с текущим классом Regex. Я попытаюсь уточнить свой вопрос.

В .Net использование регулярного выражения проще, чем в Java. Поскольку оба языка объектно ориентированы и очень похожи во многих аспектах, я ожидаю, что у вас будет аналогичный опыт использования regex на обоих языках. К сожалению, это не так.


Здесь немного кода, сравниваемого в Java и С#. Первый - это С#, а второй - Java:

В С#:

string source = "The colour of my bag matches the color of my shirt!";
string pattern = "colou?r";

foreach(Match match in Regex.Matches(source, pattern))
{
    Console.WriteLine(match.Value);
}

В Java:

String source = "The colour of my bag matches the color of my shirt!";
String pattern = "colou?r";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(source);

while(m.find())
{
    System.out.println(source.substring(m.start(), m.end()));
}

Я попытался быть справедливым для обоих языков в приведенном выше примере кода.

Первое, что вы заметили здесь, - это член .Value класса Match (по сравнению с использованием .start() и .end() в Java).

Зачем мне создавать два объекта, когда я могу вызвать статическую функцию типа Regex.Matches или Regex.Match и т.д.?

В более продвинутом использовании разница проявляется гораздо больше. Посмотрите на метод Groups, длину словаря, Capture, Index, Length, Success и т.д. Это все очень необходимые функции, которые, на мой взгляд, также должны быть доступны для Java.

Конечно, все эти функции могут быть добавлены вручную с помощью специального класса прокси (помощника). Это основная причина, по которой я задал этот вопрос. У нас нет дуновения Regex в Perl, но по крайней мере мы можем использовать подход .Net к Regex, который, по моему мнению, очень продуманный.

Ответы

Ответ 1

Из вашего отредактированного примера я теперь могу видеть, что вы хотите. И у вас тоже есть мои симпатии. Регулярные выражения Javas - это длинный, длинный и длинный путь от удобства, которое вы найдете на языках программирования более высокого уровня, таких как Ruby или Perl. И они почти всегда будут; это не может быть исправлено, поэтому навсегда застряли в этом беспорядке - по крайней мере, на Java. Другие языки JVM лучше справляются с этим, особенно Groovy. Но они по-прежнему страдают некоторыми из присущих ему недостатков и могут только зайти.

С чего начать? Существуют так называемые методы удобства класса String: matches, replaceAll, replaceFirst и split. Иногда они могут быть одобрены в небольших программах, в зависимости от того, как вы их используете. Однако у них действительно есть несколько проблем, которые, как вам кажется, вы обнаружили. Это неполный список этих проблем, и что можно и не может сделать с ними.

  • Метод неудобства очень странно назван "совпадением", но он требует, чтобы вы поместили ваше регулярное выражение с обеих сторон, чтобы соответствовать всей строке. Этот контр-интуитивный смысл противоречит любому пониманию соответствия слова, используемого на любом предыдущем языке, и постоянно укусывает людей. Шаблоны, переданные в другие 3 неудобства, работают очень в отличие от этого, потому что в других 3 они работают, как обычные шаблоны, работают повсюду; просто не в matches. Это означает, что вы не можете просто скопировать свои шаблоны вокруг, даже в рамках методов в том же проклятом классе ради добра! И нет никакого метода find, чтобы делать то, что делает каждый другой помощник в мире. Метод matches должен был называться чем-то вроде FullMatch, и в класс String должен был быть добавлен метод PartialMatch или find.

  • Нет API, который позволяет передавать флаги Pattern.compile вместе со строками, которые вы используете для 4 удобных методов, связанных с шаблонами класса String. Это означает, что вам следует полагаться на строковые версии, такие как (?i) и (?x), но они не существуют для всех возможных флагов компиляции шаблонов. Это, по меньшей мере, неудобно.

  • Метод split не возвращает тот же результат в случаях краев, что и split возвращается на языках, от которых заимствован Java. Это непродуманная маленькая добыча. Сколько элементов, по вашему мнению, нужно вернуть в возвратный список, если вы разделите пустую строку, а? Java-разработчики представляют собой поддельный элемент возврата, где должен быть один, что означает, что вы не можете различать законные результаты и фиктивные. Это серьезный дефект дизайна, который разбивается на ":", вы не можете определить разницу между входами "" vs ":". Ой, да! Разве люди никогда не проверяют этот материал? И опять же, нарушение и принципиально ненадежное поведение неустойчиво: вы никогда не должны менять вещи, даже сломать вещи. Нехорошо сломать сломанные вещи в Java, так как это где-то еще. Сломанный навсегда здесь.

  • Обозначение обратных косов с регулярными выражениями конфликтует с символом обратной косой черты, используемым в строках. Это делает его супердуперу неудобным и склонным к ошибкам, потому что вам приходится постоянно добавлять много обратных косых ко всем, а его слишком легко забыть и не получить ни предупреждения, ни успеха. Простые шаблоны, такие как \b\w+\b, становятся кошмарами в типографском избытке: "\\b\\w+\\b". Удачи вам в чтении. Некоторые люди используют функцию слэш-инвертора на своих шаблонах, чтобы они могли записать это как "/b/w+/b". Помимо чтения в ваших шаблонах из строки, нет возможности построить ваш шаблон в WYSIWYG буквально; его всегда тяжело нагружены с обратной косой чертой. Вы получили их все, и достаточно, и в нужных местах? Если это так, очень трудно читать. Если это не так, вы, вероятно, не получили их всех. По крайней мере, языки JVM, такие как Groovy, нашли правильный ответ здесь: дайте людям регулярные выражения 1-го класса, чтобы вы не сходили с ума. Вот справедливая коллекция примеров Groovy regex, показывающая, насколько это просто и возможно.

  • Режим (?x) имеет глубокие недостатки. Он не принимает комментарии в стиле Java // COMMENT, а скорее в стиле оболочки # COMMENT. Он не работает с многострочными строками. Он не принимает литералы как литералы, вызывая проблемы с обратной косой чертой, перечисленные выше, что в корне компрометирует любую попытку выстроить вещи, например, все комментарии начинаются в одной колонке. Из-за обратных косых черт вы либо заставляете их начинать в том же столбце в строке исходного кода, и прикручивать их, если вы распечатываете их, или наоборот. Так много для удобочитаемости!

  • Это невероятно сложно - и в самом деле, принципиально неустойчиво сломано - ввести символы Unicode в регулярное выражение. Нет поддержки символически названных символов, таких как \N{QUOTATION MARK}, \N{LATIN SMALL LETTER E WITH GRAVE} или \N{MATHEMATICAL BOLD CAPITAL C}. Это означает, что вы застряли с недостижимыми магическими числами. И вы даже не можете вводить их по кодовой точке. Вы не можете использовать \u0022 для первого, потому что препроцессор Java делает это синтаксической ошибкой. Итак, вместо этого вы переходите к \\u0022, который работает до тех пор, пока вы не перейдете к следующему, \\u00E8, который не может быть введен таким образом или он сломает флаг CANON_EQ. И последний из них - чистый кошмар: его кодовая точка U + 1D402, но Java не поддерживает полный набор Unicode, используя номера кодовых точек в регулярных выражениях, заставляя вас вытащить калькулятор, чтобы выяснить, что это \uD835\uDC02 или \\uD835\\uDC02 (но не \\uD835\uDC02), безумно. Но вы не можете использовать их в классах символов из-за ошибки в дизайне, что делает невозможным совпадение, [\N{MATHEMATICAL BOLD CAPITAL A}-\N{MATHEMATICAL BOLD CAPITAL Z}], потому что компилятор regex закручивает UTF-16. Опять же, это никогда не может быть исправлено или оно изменит старые программы. Вы даже не можете обойти ошибку, используя обычное обходное решение проблем Javas Unicode в исходном коде, компилируя с помощью java -encoding UTF-8, потому что глупая вещь хранит строки как неприятные UTF-16, что обязательно ломает их в классах символов. OOPS!

  • Многие из элементов регулярных выражений, которые мы используем на других языках, отсутствуют на Java. Для примеров нет ни названных групп, ни даже относительно пронумерованных. Это делает построение более крупных моделей из меньших, в основном, подверженных ошибкам. Существует интерфейсная библиотека, которая позволяет вам иметь простые именованные группы, и действительно, это, наконец, придет в производство JDK7. Но даже в этом случае нет механизма для того, что делать с более чем одной группой с тем же именем. И вы все еще не имеете относительно пронумерованных буферов. Вернулись в "Бад-старые дни" снова, что было решено эоны назад.

  • Нет поддержки последовательности строк, которая является одной из двух "сильно рекомендованных" частей стандарта, что предполагает, что для этого используется \R. Это неудобно подражать из-за своей переменной длины, а у Джаваса нет поддержки графем.

  • Экраны класса символов не работают в наборе символов Javas! Да, это правильно: обычные вещи вроде \w и \s (вернее, "\\w" и "\\b") не работают в Unicode в Java! Это не крутой ретро. Хуже того, Javas \b (сделать, что "\\b", который не совпадает с "\b"), имеет некоторую чувствительность Unicode, хотя не то, что стандарт говорит, что это должно быть. Так, например, строка, подобная "élève", никогда не будет в Java соответствовать шаблону \b\w+\b, а не только целиком на Pattern.matches, но действительно без каких-либо проблем, как вы могли бы получить из Pattern.find. Это просто так испорчено, как нищая вера. Они нарушили неотъемлемую связь между \w и \b, а затем неправильно определили их для загрузки! Он даже не знает, что такое Unicode Алфавитный код. Это в высшей степени нарушено, и они никогда не смогут его исправить, потому что это изменит поведение существующего кода, который строго запрещен в Java Universe. Лучшее, что вы можете сделать, это создать библиотеку перезаписи, которая будет выступать в качестве интерфейса перед тем, как перейти к фазе компиляции; таким образом, вы можете принудительно перенести свои шаблоны с 1960-х годов на 21-й век обработки текста.

  • Поддерживаются только два свойства Unicode: общие категории и свойства блока. Свойства общей категории поддерживают только аббревиатуры типа \p{Sk}, в отличие от стандартов "Сильная рекомендация", чтобы также разрешать \p{Modifier Symbol}, \p{Modifier_Symbol} и т.д. Вы даже не получаете требуемые псевдонимы, которые, как утверждают стандарты, вам нужно. Это делает ваш код еще более нечитаемым и неподъемным. Наконец, вы получите поддержку свойства Script в производстве JDK7, но это все еще серьезно не соответствует минимальному набору из 11 основных свойств, которые Стандарт говорит, что вы должны предоставить даже минимальный уровень поддержки Unicode.

  • Некоторые из слабых свойств, которые предоставляет Java, являются faux amis: у них есть те же имена, что и официальные имена поддержки Unicode, , но они делают что-то совсем другое. Например, для Unicode требуется, чтобы \p{alpha} был таким же, как \p{Alphabetic}, но Java делает его только архаичным и не более длинным 7-битным алфавитом, что на 4 порядка меньше. Пробел - еще один недостаток, так как вы используете версию Java, которая маскируется как пробелы в Unicode, ваши партизаны UTF-8 будут ломаться из-за своих кодовых точек NO-BREAK SPACE, которые Unicode нормативно требует, чтобы их считали пробелами, но Java игнорирует это требование, поэтому перерывы ваш парсер.

  • Нет никакой поддержки графемам, как обычно предоставляет \X. Это делает невозможным неисчислимое множество общих задач, которые вам нужны и которые вы хотите делать с регулярными выражениями. Не только расширенные кластеры grapheme недоступны, потому что Java почти не поддерживает свойства Unicode, вы даже не можете приблизить старый кластеры наследия grapheme с использованием стандартного (?:\p{Grapheme_Base}\p{Grapheme_Extend}]*). Невозможность работать с графемами делает невозможными даже самые простые виды обработки текста в Юникоде. Например, вы не можете сопоставить гласную, независимо от диакритики на Java. Способ, которым вы это делаете на языке с поддержкой графемы, различается, но по крайней мере вы должны быть в состоянии выбросить вещь в NFD и соответствовать (?:(?=[aeiou])\X). На Java вы не можете этого сделать: графемы недоступны. И это означает, что Java не может даже обрабатывать собственный собственный набор символов. Он дает вам Unicode, а затем делает невозможным работу с ним.

  • Методы удобства в классе String не кэшируют скомпилированное регулярное выражение. На самом деле нет такой вещи, как шаблон времени компиляции, который проверяется синтаксисом во время компиляции - , когда предполагается синтаксическая проверка. Это означает, что ваша программа, которая использует только константу регулярные выражения, полностью понятые во время компиляции, будут выбиваться с ошибкой в ​​середине ее прогона, если вы забудете немного обратную косую черту здесь или там, как это обычно делается из-за недостатков, которые обсуждались ранее. Даже Groovy получает эту часть права. Регулярные выражения представляют собой слишком высокоуровневую конструкцию, с которой Джавас сталкивается с неприятной моделью с фактом, с болтами на стороне, и они слишком важны для рутинной обработки текста, которую нужно игнорировать. Java - это слишком низкоуровневый язык для этого материала, и он не может обеспечить простую механику, из которой вы можете сами построить то, что вам нужно: вы не можете добраться туда отсюда.

  • Классы String и Pattern помечены как final в Java. Это полностью уничтожает любую возможность использования надлежащего дизайна OO для расширения этих классов. Вы не можете создать лучшую версию метода matches путем подкласса и замены. Черт, ты не можешь даже подкласс! Финал не является решением; final - смертный приговор, из которого нет апелляции.

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

   String rx =
          "(?= ^ \\p{Lu} [_\\pL\\pM\\d\\-] + \$)\n"
        . "   # next is a big can't-have set    \n"
        . "(?! ^ .*                             \n"
        . "    (?: ^     \\d+              $    \n"
        . "      | ^ \\p{Lu} - \\p{Lu}     $    \n"
        . "      | Invitrogen                   \n"
        . "      | Clontech                     \n"
        . "      | L-L-X-X    # dashes ok       \n"
        . "      | Sarstedt                     \n"
        . "      | Roche                        \n"
        . "      | Beckman                      \n"
        . "      | Bayer                        \n"
        . "    )      # end alternatives        \n"
        . "    \\b    # only on a word boundary \n"
        . ")          # end negated lookahead   \n"
        ;

Вы видите, как это неестественно? Вы должны поставить литерные строки в строках; вы должны использовать комментарии, отличные от Java; вы не можете сделать что-либо из-за дополнительных обратных косых черт; вы должны использовать определения вещей, которые не работают прямо на Unicode. Есть еще много проблем.

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

Итак, Alireza Noori, если вы чувствуете, что Javas неуклюжие регулярные выражения слишком запущены для надежной и удобной обработки регулярных выражений, которые когда-либо были возможны на Java, я не могу вас разочаровать. Извините, но так оно и есть.

"Исправлено в следующей версии!"

Просто потому, что некоторые вещи никогда не могут быть исправлены, не означает, что ничто никогда не может быть исправлено. Это нужно сделать очень осторожно. Вот те вещи, о которых я знаю, которые уже исправлены в текущих JDK7 или предлагаемых сборках JDK8:

  • Теперь поддерживается свойство Unicode Script. Вы можете использовать любую из эквивалентных форм \p{Script=Greek}, \p{sc=Greek}, \p{IsGreek} или \p{Greek}. Это по своей сути превосходит старые неуклюжие свойства блока. Это означает, что вы можете делать такие вещи, как [\p{Latin}\p{Common}\p{Inherited}], что очень важно.

  • Ошибка UTF-16 имеет обходное решение. Теперь вы можете указать любую кодовую точку Юникода по ее номеру с помощью обозначения \x{⋯}, например \x{1D402}. Это работает даже внутри классов символов, что позволяет [\x{1D400}-\x{1D419}] работать правильно. Вы все равно должны удвоить обратную косую черту, но это работает только в регулярном выражении, а не в строках вообще, как это действительно должно быть.

  • Именованные группы теперь поддерживаются через стандартную нотацию (?<NAME>⋯) для ее создания, а \k<NAME> - для обратной ссылки. Они по-прежнему вносят вклад в числовые номера групп. Однако вы не можете получить более одного из них в одном шаблоне и не можете использовать их для рекурсии.

  • Новый флаг компиляции шаблона Pattern.UNICODE_CHARACTER_CLASSES и связанный с ним встраиваемый коммутатор (?U) теперь обмениваются всеми определениями таких вещей, как \w, \b, \p{alpha} и \p{punct}, так что теперь они соответствуют определениям тех вещей, которые требуются в стандарте Unicode.

  • Теперь будут поддерживаться отсутствующие или неверные двоичные свойства \p{IsLowercase}, \p{IsUppercase} и \p{IsAlphabetic}, которые соответствуют методам класса Character. Это важно, потому что Unicode делает значительное и широкое различие между буквами и общими или буквенными кодовыми точками. Эти ключевые свойства относятся к числу 11 основных свойств, которые абсолютно необходимы для соответствия уровня 1 UTS # 18, "Unicode Regular Expresions" , без которого вы действительно не можете работать с Unicode,

Эти улучшения и исправления очень важны, чтобы, наконец, иметь, и поэтому я рад, даже взволнован, чтобы иметь их.

Но для промышленной прочности, состояния регулярных выражений и/или Unicode я не буду использовать Java. Theres слишком много пропавших без вести в Javas, все еще - пятнадцатилетняя модель Unicode, чтобы получить реальную работу, если вы решитесь использовать набор символов, который дает Java. И модель с болтовым соединением никогда не работает, и это все регулярные выражения Java. Вы должны начать с первых принципов, как это делал Groovy.

Конечно, это может работать для очень ограниченных приложений, чья небольшая клиентская база ограничена англоязычными монологами в сельской Айове без каких-либо внешних взаимодействий или какой-либо необходимости в персонажах, помимо того, что мог отправить телеграф старого образца. Но для каких проектов это действительно так? Меньше, чем вы думаете, оказывается.

Именно по этой причине определенный (и очевидный) многомиллиардный доллар недавно отменял международное развертывание важного приложения. Поддержка Javas Unicode - не только в регулярных выражениях, но и во всем - оказалась слишком слабой, чтобы необходимая интернационализация была надежно выполнена на Java. Из-за этого они были вынуждены отступить от своего первоначально запланированного развертывания по всему миру до простого развертывания в США. Его позитивный приход. И нет, есть Nᴏᴛ Hᴀᴘᴘʏ; вы были бы?

У Java было 20 лет, чтобы понять это, и они явно не сделали этого до сих пор, поэтому я бы не затаил дыхание. Или бросить хорошие деньги после неудачи; урок здесь состоит в том, чтобы игнорировать ажиотаж и вместо этого применять должную осмотрительность, чтобы быть уверенным, что вся необходимая инфраструктурная поддержка существует до того, как вы инвестируете слишком много. В противном случае вы тоже можете застрять без каких-либо реальных вариантов, как только вы окажетесь слишком далеко, чтобы спасти свой проект.

Caveat Emptor

Ответ 2

Можно писать, или просто написать:

public class Regex {

    /**
     * @param source 
     *        the string to scan
     * @param pattern
     *        the regular expression to scan for
     * @return the matched 
     */
    public static Iterable<String> matches(final String source, final String pattern) {
        final Pattern p = Pattern.compile(pattern);
        final Matcher m = p.matcher(source);
        return new Iterable<String>() {
            @Override
            public Iterator<String> iterator() {
                return new Iterator<String>() {
                    @Override
                    public boolean hasNext() {
                        return m.find();
                    }
                    @Override
                    public String next() {
                        return source.substring(m.start(), m.end());
                    }    
                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }

}

Используется по вашему желанию:

public class RegexTest {

    @Test
    public void test() {
       String source = "The colour of my bag matches the color of my shirt!";
       String pattern = "colou?r";
       for (String match : Regex.matches(source, pattern)) {
           System.out.println(match);
       }
    }
}

Ответ 3

Мальчик, я слышу тебя на этой Алиреезе! Regex достаточно сбивают с толку, не вызывая так много изменений синтаксиса. Я тоже делаю намного больше С#, чем программирование на Java, и имел ту же проблему.

Я нашел, что это очень полезно: http://www.tusker.org/regex/regex_benchmark.html - это список альтернативных реализаций регулярных выражений для Java, ориентированных на результаты.

Ответ 4

Некоторые из недостатков API, упомянутых в ответе @tchrist, были исправлены в Kotlin.