Читаемые регулярные выражения, не теряя своей силы?

77

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

Для простого случая результат может быть примерно таким, как регулярное выражение Java:

Pattern re = Pattern.compile(
  "^\\s*(?:(?:([\\d]+)\\s*:\\s*)?(?:([\\d]+)\\s*:\\s*))?([\\d]+)(?:\\s*[.,]\\s*([0-9]+))?\\s*$"
);

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

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

Итак, вкратце, есть ли решение / альтернатива для записи-чтения для регулярных выражений, не теряя своей силы? Как будет выглядеть приведенное выше регулярное выражение с альтернативным подходом? Любой язык хорош, хотя многоязычное решение было бы лучше, в той степени, что регулярные выражения являются многоязычными.


И затем, что делает более раннее регулярное выражение, так это: анализирует строку чисел в формате 1:2:3.4, захватывая каждое число, где пробелы разрешены и 3требуются только .

Хайд
источник
2
связанные вещи на SO: stackoverflow.com/a/143636/674039
Вим
24
Чтение / редактирование регулярных выражений на самом деле тривиально, если вы знаете, что они должны захватывать. Возможно, вы слышали об этой редко используемой функции большинства языков, называемой «комментарии». Если вы не поместите один над сложным регулярным выражением, объясняющим, что он делает, вы заплатите цену позже. Кроме того, обзор кода.
TC1
2
Два варианта очистки, не разбивая его на более мелкие части. Их наличие или отсутствие варьируется от языка к языку. (1) регулярные выражения расширенной строки, где пропуски в регулярном выражении игнорируются (если не экранированы) и добавляется форма однострочного комментария, так что вы можете разбить ее на логические куски с отступом, межстрочным интервалом и комментариями. (2) именованные группы захвата, где вы можете дать имя каждой скобке, которая одновременно добавляет некоторую самодокументированность и автоматически заполняет хэш совпадений - намного лучше, чем числовой индексированный массив совпадений или переменные $ N.
Бен Ли
3
Частично проблема заключается в самом языке регулярных выражений и неудачных исторических решениях в его дизайне, которые тащатся как багаж. На нормальном языке группировка скобок - это чисто синтаксическое устройство для формирования дерева разбора. Но в реализациях регулярных выражений, восходящих к Unix, они имеют семантику: привязка регистров к совпадениям подвыражений. Итак, вам нужны более сложные, некрасивые скобки только для того, чтобы получить чистую группировку!
Каз
2
Не совсем практический ответ, но может быть полезно упомянуть, что сила регулярного выражения точно такая же, как у конечного автомата. То есть регулярные выражения могут проверять / анализировать один и тот же класс строк, проверяемых и анализируемых конечными автоматами. Следовательно, читаемое человеком представление регулярного выражения, вероятно, должно быть в состоянии быстро построить граф, и я полагаю, что большинство текстовых языков действительно плохи в этом; Вот почему мы используем визуальные инструменты для таких вещей. Загляните на hackingoff.com/compilers/regular-expression-to-nfa-dfa, чтобы получить вдохновение.
damix911

Ответы:

80

Несколько человек упомянули сочинение из небольших партий, но никто еще не привел пример, так что вот мой:

string number = "(\\d+)";
string unit = "(?:" + number + "\\s*:\\s*)";
string optionalDecimal = "(?:\\s*[.,]\\s*" + number + ")?";

Pattern re = Pattern.compile(
  "^\\s*(?:" + unit + "?" + unit + ")?" + number + optionalDecimal + "\\s*$"
);

Не самая читаемая, но я чувствую, что она понятнее оригинала.

Кроме того , C # имеет @оператор , который может предшествовать строки для того , чтобы указать , что следует понимать буквально (без экранирующих символов), так что numberбыло бы@"([\d]+)";

Bobson
источник
Только сейчас заметил, как и так [\\d]+и [0-9]+должно быть просто \\d+(ну, некоторые могут найти [0-9]+более читабельным). Я не собираюсь редактировать вопрос, но вы можете исправить этот ответ.
Hyde
@hyde - Хороший улов. Технически они не совсем одно и то же - \dбудут соответствовать любому, что считается числом, даже в других системах нумерации (китайская, арабская и т. Д.), В то время как [0-9]будут совпадать только со стандартными цифрами. Я все же стандартизировал \\dи учел это в optionalDecimalструктуре.
Бобсон
42

Ключом к документированию регулярного выражения является его документирование. Слишком часто люди бросают в то, что кажется шумом линии, и оставляют это на этом.

В Perl/x оператор в конце регулярного выражения подавляет пробелы позволяя документировать регулярное выражение.

Вышеупомянутое регулярное выражение тогда станет:

$re = qr/
  ^\s*
  (?:
    (?:       
      ([\d]+)\s*:\s*
    )?
    (?:
      ([\d]+)\s*:\s*
    )
  )?
  ([\d]+)
  (?:
    \s*[.,]\s*([\d]+)
  )?
  \s*$
/x;

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

И затем, что делает более раннее регулярное выражение, так это: анализирует строку чисел в формате 1: 2: 3.4, захватывая каждое число, где разрешены пробелы и требуется только 3.

Глядя на это регулярное выражение, можно увидеть, как оно работает (и не работает). В этом случае это регулярное выражение будет соответствовать строке 1.

Подобные подходы могут быть приняты на другом языке. Там работает опция python re.VERBOSE .

Perl6 (вышеприведенный пример был для perl5) развивает это с концепцией правил, которая приводит к еще более мощным структурам, чем PCRE (он обеспечивает доступ к другим грамматикам (контекстно-зависимым и контекстно-зависимым), чем просто обычные и расширенные регулярные).

В Java (откуда берется этот пример) можно использовать конкатенацию строк для формирования регулярного выражения.

Pattern re = Pattern.compile(
  "^\\s*"+
  "(?:"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #1
    ")?"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #2
    ")"+
  ")?"+ // First groups match 0 or 1 times
  "([\\d]+)"+ // Capture group #3
  "(?:\\s*[.,]\\s*([0-9]+))?"+ // Capture group #4 (0 or 1 times)
  "\\s*$"
);

По общему признанию, это создает намного больше "в строке, что может привести к некоторой путанице, ее легче читать (особенно с подсветкой синтаксиса в большинстве IDE) и документировать.

Ключ в том, чтобы распознать силу и «написать один раз» природу, в которую часто попадают регулярные выражения Написание кода для защиты от этого, чтобы регулярное выражение оставалось ясным и понятным, является ключевым. Для ясности мы форматируем код Java - регулярные выражения ничем не отличаются, когда язык дает вам возможность сделать это.


источник
13
Существует большая разница между «документированием» и «добавлением разрывов строк».
4
@JonofAllTrades Сделать код читаемым - это первый шаг ко всему. Добавление разрывов строк также позволяет добавлять комментарии для этого подмножества RE в одну и ту же строку (что труднее сделать в одной длинной строке текста регулярного выражения).
2
@JonofAllTrades, я совершенно не согласен. «Документирование» и «добавление разрывов строк» ​​не так уж и отличаются, поскольку они служат одной и той же цели - облегчают понимание кода. А для плохо отформатированного кода «добавление разрывов строк» ​​служит этой цели гораздо лучше, чем добавление документации.
Бен Ли
2
Добавление разрывов строк - это начало, но это примерно 10% работы. Другие ответы дают больше подробностей, что полезно.
26

Режим многословия, предлагаемый некоторыми языками и библиотеками, является одним из ответов на эти вопросы. В этом режиме пробелы в строке регулярного выражения удаляются (так что вам нужно использовать \s), и комментарии возможны. Вот короткий пример в Python, который поддерживает это по умолчанию:

email_regex = re.compile(r"""
    ([\w\.\+]+) # username (captured)
    @
    \w+         # minimal viable domain part
    (?:\.w+)    # rest of the domain, after first dot
""", re.VERBOSE)

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

Xion
источник
15

Каждый язык, использующий регулярные выражения, позволяет вам составлять их из более простых блоков, чтобы упростить чтение, и с помощью чего-либо более сложного, чем (или столь же сложного, как) ваш пример, вы обязательно должны воспользоваться этой опцией. Особая проблема с Java и многими другими языками состоит в том, что они не рассматривают регулярные выражения как «первоклассных» граждан, вместо этого требуя, чтобы они проникли в язык через строковые литералы. Это означает, что многие кавычки и обратные слэши не являются частью синтаксиса регулярных выражений и затрудняют чтение, а также означает, что вы не можете получить намного более читабельную информацию, чем это, без эффективного определения своего собственного мини-языка и интерпретатора.

Прототипом лучшего способа интеграции регулярных выражений был, конечно, Perl с опцией пробела и операторами регулярных кавычек. Perl 6 расширяет концепцию построения регулярных выражений от частей к рекурсивным грамматикам, так что гораздо удобнее в использовании, так что на самом деле это вообще не сравнение. Язык, возможно, пропустил лодку своевременности, но его регулярное выражение поддержки было Хорошим Материалом (тм).

Килиан Фот
источник
1
Под «более простыми блоками», упомянутыми в начале ответа, вы подразумеваете просто конкатенацию строк или что-то более сложное?
Hyde
7
Я имел в виду определение подвыражений как более коротких строковых литералов, назначение их локальным переменным со значимыми именами, а затем конкатенация. Я считаю, что имена важнее для читабельности, чем просто улучшение макета.
Килиан Фот
11

Мне нравится использовать Expresso: http://www.ultrapico.com/Expresso.htm

Это бесплатное приложение имеет следующие функции, которые я считаю полезными с течением времени:

  • Вы можете просто скопировать и вставить свое регулярное выражение, и приложение проанализирует его для вас
  • После того как ваше регулярное выражение написано, вы можете проверить его непосредственно из приложения (приложение предоставит вам список перехватов, замен ...)
  • Как только вы протестируете его, он сгенерирует код C # для его реализации (обратите внимание, что этот код будет содержать пояснения о вашем регулярном выражении).

Например, с регулярным выражением, которое вы только что отправили, оно будет выглядеть так: Пример экрана с изначально заданным регулярным выражением

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

Э. Яеп
источник
4
Не могли бы вы объяснить это более подробно - как и почему это отвечает на заданный вопрос? «Ответы только на ссылки» не очень приветствуются на Stack Exchange
gnat
5
@gnat Извините за это. Вы совершенно правы. Я надеюсь, что мой отредактированный ответ дает больше информации.
Э. Яп
Я также могу порекомендовать: regex101.com
Epskampie
9

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

Грамматики BNF, EBNF и т. Д. Гораздо проще читать и составлять, чем сложные регулярные выражения. ЗОЛОТО является одним из инструментов для таких вещей.

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

Альтернативы регулярным выражениям

Принимая «альтернативный» для обозначения «семантически эквивалентного средства с другим синтаксисом», есть по крайней мере эти альтернативы / с RegularExpressions:

  • Основные регулярные выражения
  • «Расширенные» регулярные выражения
  • Perl-совместимые регулярные выражения
  • ... и много других вариантов ...
  • Синтаксис RE в стиле SNOBOL (SnobolLanguage, IconLanguage)
  • Синтаксис SRE (RE как EssExpressions)
  • разные синхронизации FSM
  • Конечные грамматики пересечений (довольно выразительные)
  • ParsingExpressionGrammars, как в OMetaLanguage и LuaLanguage ( http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html )
  • Режим разбора RebolLanguage
  • ProbabilityBasedParsing ...
Ник П
источник
Вы не могли бы объяснить больше о том, что делает эта ссылка и для чего она нужна? «Ответы только на ссылки» не очень приветствуются на Stack Exchange
gnat
1
Добро пожаловать в Программисты, Ник П. Пожалуйста, не обращайте внимания на downvote / r, но прочитайте страницу с мета, на которую ссылается @gnat.
Кристоффер Летт
@ Christoffer Lette Ценю ваш ответ. Постараюсь учесть это в следующих постах. @ gnat Комментарий Пауло Скардина отражает намерения моих постов. Грамматики BNF, EBNF и т. Д. Гораздо легче читать и создавать, чем сложные регулярные выражения. ЗОЛОТО является одним из инструментов для таких вещей. Ссылка c2 содержит список возможных альтернатив, которые можно найти, с некоторыми комментариями по ним. По сути, это была ссылка "см. Также", чтобы дополнить мою рекомендацию по грамматическому движку.
Ник П
6

Это старый вопрос, и я не видел никаких упоминаний о словесных выражениях, поэтому я решил добавить эту информацию сюда и для будущих искателей. Вербальные выражения были специально разработаны, чтобы сделать регулярное выражение человеком понятным, без необходимости изучать символическое значение регулярного выражения. Смотрите следующий пример. Я думаю, что это лучше всего то, что вы просите.

// Create an example of how to test for correctly formed URLs
var tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();

// Create an example URL
var testMe = 'https://www.google.com';

// Use RegExp object's native test() function
if (tester.test(testMe)) {
    alert('We have a correct URL '); // This output will fire}
} else {
    alert('The URL is incorrect');
}

console.log(tester); // Outputs the actual expression used: /^(http)(s)?(\:\/\/)(www\.)?([^\ ]*)$/

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

Паривар Сарафф
источник
2
Это круто!
Джереми Томпсон
3

Простейшим способом было бы по-прежнему использовать регулярные выражения, но построить свое выражение из составления более простых выражений с описательными именами, например, http://www.martinfowler.com/bliki/ComposedRegex.html (и да, это из строки concat)

однако в качестве альтернативы вы также можете использовать библиотеку комбинатора парсера, например, http://jparsec.codehaus.org/, которая даст вам полный рекурсивный приличный парсер. опять же, настоящая сила здесь заключается в композиции (на этот раз в функциональной композиции).

JK.
источник
3

Я подумал, что стоит упомянуть logstash в ГРКАХ выражения. Grok основывается на идее составления длинных выражений парсинга из более коротких. Это позволяет удобно тестировать эти строительные блоки и поставляется в комплекте с более чем 100 часто используемыми шаблонами . Помимо этих шаблонов, он позволяет использовать весь синтаксис регулярных выражений.

Вышеуказанный шаблон, выраженный в grok: (Я тестировал в приложении отладчика, но мог ошибиться):

"(( *%{NUMBER:a} *:)? *%{NUMBER:b} *:)? *%{NUMBER:c} *(. *%{NUMBER:d} *)?"

Необязательные части и пробелы делают его немного уродливее обычного, но и здесь, и в других случаях использование grok может сделать вашу жизнь намного приятнее.

yoniLavi
источник
2

В F # у вас есть модуль FsVerbalExpressions . Он позволяет вам составлять регулярные выражения из словесных выражений, а также имеет несколько готовых регулярных выражений (например, URL).

Одним из примеров этого синтаксиса является следующее:

let groupName =  "GroupNumber"

VerbEx()
|> add "COD"
|> beginCaptureNamed groupName
|> any "0-9"
|> repeatPrevious 3
|> endCapture
|> then' "END"
|> capture "COD123END" groupName
|> printfn "%s"

// 123

Если вы не знакомы с синтаксисом F #, groupName - это строка «GroupNumber».

Затем они создают словесное выражение (VerbEx), которое они конструируют как «COD (? <GroupNumber> [0-9] {3}) END». Что они затем проверяют на строке «COD123END», где они получают именованную группу захвата «GroupNumber». Это приводит к 123.

Я, честно говоря, считаю, что нормальное регулярное выражение гораздо легче понять.

CodeMonkey
источник
-2

Во-первых, поймите, что просто работающий код - это плохой код. Хороший код также должен точно сообщать о любых обнаруженных ошибках.

Например, если вы пишете функцию для перевода денег со счета одного пользователя на счет другого пользователя; вы не просто вернете логическое значение «сработало или не сработало», потому что это не дает вызывающей стороне никакого представления о том, что пошло не так, и не позволяет вызывающей стороне должным образом информировать пользователя. Вместо этого у вас может быть набор кодов ошибок (или набор исключений): не удалось найти целевой аккаунт, недостаточно средств на исходном счете, отказано в доступе, невозможно подключиться к базе данных, слишком большая загрузка (повторите попытку позже) и т. Д. ,

Теперь подумайте о своем примере «разбора строки чисел в формате 1: 2: 3.4». Все, что делает регулярное выражение - это сообщает "пройти / не пройти", что не позволяет пользователю предоставить адекватную обратную связь (независимо от того, является ли эта обратная связь сообщением об ошибке в журнале, или интерактивным графическим интерфейсом, где ошибки отображаются красным цветом как пользовательские типы или что-то еще). Какие типы ошибок он не может описать должным образом? Плохой символ в первом числе, слишком большое первое число, пропущенное двоеточие после первого числа и т. Д.

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

Делать код читаемым / обслуживаемым - это просто случайное следствие того, что код будет хорошим.

Brendan
источник
6
Вероятно, не очень хорошее предположение. Моя причина в том, что А) Это не решает вопрос ( Как сделать его читабельным?), Б) Соответствие регулярному выражению проходит / не выполняется , и если вы разберетесь с ним до такой степени, что вы сможете точно сказать, почему оно не удалось, вы потерять много мощности и скорости, а также увеличить сложность, C) Нет никаких сомнений в том, что есть даже вероятность провала матча - это просто вопрос о том, чтобы сделать Regex читабельным. Если у вас есть контроль над данными, поступающими и / или проверяющими их заранее, вы можете предположить, что они действительны.
Бобсон
А) Разбиение его на более мелкие части делает его более читабельным (как следствие, делает его хорошим). C) Когда неизвестные / непроверенные строки вводят часть программного обеспечения, в этот момент здравомыслящий разработчик будет анализировать (с отчетами об ошибках) и преобразовывать данные в форму, которая не требует повторного анализа - после этого регулярное выражение не требуется. Б) ерунда, которая относится только к плохому коду (см. Пункты А и С).
Брендан,
Переход от вашего C: Что делать , если это является его логика проверки? Код OP может быть именно тем, что вы предлагаете - проверять ввод, сообщать, если он не действителен, и преобразовывать его в пригодную для использования форму (через перехваты). Все, что у нас есть, это само выражение. Как бы вы предложили синтаксический анализ, кроме как с помощью регулярного выражения? Если вы добавите пример кода, который даст тот же результат, я уберу свое понижение.
Бобсон
Если это «C: валидация (с сообщениями об ошибках)», то это плохой код, потому что сообщения об ошибках плохие. Если это не удается; было ли это потому, что строка была NULL, или потому что первое число имело слишком много цифр, или потому что первый разделитель не был :? Представьте себе компилятор, у которого было только одно сообщение об ошибке («ОШИБКА»), который был слишком глуп, чтобы сообщить пользователю, в чем проблема. Теперь представьте тысячи веб-сайтов, которые так же глупы и отображают (например) «Плохой адрес электронной почты» и ничего более.
Брендан,
Кроме того, представьте себе, что полуобученный оператор службы поддержки получает отчет об ошибке от полностью неподготовленного пользователя, который говорит: «Программное обеспечение перестало работать - последняя строка в журнале программного обеспечения:« ОШИБКА: не удалось извлечь вспомогательный номер версии из строки версии »1: 2-3.4 «(ожидается двоеточие после второго числа)»
Брендан