Читаються регулярні вирази, не втрачаючи своєї сили?


77

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

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

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

Багато програмістів також знають, що потрібно редагувати регулярний вираз або просто кодувати навколо регулярного виразу в застарілій базі коду. З невеликим редагуванням, щоб розділити його, вище regexp все ще дуже легко зрозуміти для всіх, хто добре знайомий з регулярними експедиціями, і ветеран regexp повинен відразу побачити, що він робить (відповісти в кінці публікації, якщо хтось захоче вправу самі розібратися).

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

Отже, коротке оповідання, чи є рішення / альтернатива для читання рішень / альтернативи для регулярних виразів, не втрачаючи своєї сили? Як би виглядало вищезгадане regexp з альтернативним підходом? Будь-яка мова є чудовою, хоча найкращим є багатомовне рішення, оскільки ступінь зворотного зразка є багатомовною.


І тоді, що робить більш раннє regexp, це це: проаналізувати рядок чисел у форматі 1:2:3.4, захоплюючи кожне число, де дозволено пробіли і 3потрібно лише .


2
пов'язана річ на SO: stackoverflow.com/a/143636/674039
wim

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]+)";


Тільки тепер помітив , як і [\\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

Режим «багатослівний», який пропонують деякі мови та бібліотеки, є однією з відповідей на ці проблеми. У цьому режимі пробіл у рядку regexp викреслений (тому потрібно використовувати \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)

На будь-якій мові, яка цього не робить, реалізація перекладача з багатослівного в «нормальний» режим має бути простим завданням. Якщо ви стурбовані читабельністю своїх regexps, ви, ймовірно, виправдаєте цей час інвестиції досить легко.


15

Кожна мова, що використовує регулярні вирази, дозволяє складати їх з більш простих блоків, щоб полегшити читання, і з чим-небудь складнішим, ніж (або настільки складним, як) ваш приклад, ви обов'язково повинні скористатися цим варіантом. Особлива проблема з Java та багатьма іншими мовами полягає в тому, що вони не трактують регулярні вирази як "першокласні" громадяни, а вимагають від них прокрастися до мови за допомогою рядкових літералів. Це означає, що багато лапок та зворотних косих ринків, які насправді не є частиною синтаксису регулярних виразів та ускладнюють їх читання, а також означає, що ви не можете отримати набагато більше читабельності, ніж це, без ефективного визначення власної міні-мови та інтерпретатора.

Прототиповим кращим способом інтеграції регулярних виразів був, звичайно, Perl, з його варіантом пробілів та операторами цитування регулярних виразів. Perl 6 розширює концепцію створення регексів з частин до фактичних рекурсивних граматик, що набагато краще використовувати, це насправді зовсім не порівняння. Мова, можливо, пропустила човен своєчасності, але його підтримка регулярного вираження була The Good Stuff (tm).


1
Під "простішими блоками", згаданими на початку відповіді, ви маєте на увазі просто об'єднання рядків або щось більш досконале?
Гайд

7
Я мав на увазі визначення підвиразів як коротших літеральних рядків, присвоєння їм локальних змінних зі значущими іменами, а потім об'єднання. Я вважаю, що імена важливіші для читабельності, ніж просто покращення макета.
Кіліан Фот

11

Мені подобається використовувати Expresso: http://www.ultrapico.com/Expresso.htm

Ця безкоштовна програма має такі функції, які мені здаються корисними з часом:

  • Ви можете просто скопіювати і вставити ваш регулярний вираз, і програма розбере його для вас
  • Щойно ваш регекс буде написаний, ви можете протестувати його безпосередньо з програми (додаток надасть вам список захоплень, замін ...)
  • Після того, як ви його протестували, він генерує код C # для його реалізації (зауважте, що код буде містити пояснення щодо вашого регулярного виразу).

Наприклад, щойно ви подали регулярний вираз, він виглядатиме так: Зразковий екран із початково заданим регулярним виразом

Звичайно, спробувати варто описати це тисячу слів. Зауважте також, що я звертаю увагу на будь-який спосіб з редактором цієї програми.


4
Ви не проти пояснити це детальніше - як і чому він відповідає на поставлене запитання? "Відповіді лише на посилання" не дуже вітаються на біржі стеків
gnat

5
@gnat Вибачте з цього приводу. Ви абсолютно праві. Я сподіваюся, що моя відредагована відповідь дає більше розумінь.
E. Jaep

9

Для деяких речей може допомогти просто використовувати граматику типу BNF. Вони можуть бути набагато простішими для читання, ніж регулярні вирази. Інструмент, такий як GoldParser Builder, може потім перетворити граматику в аналізатор, який робить важкий підйом для вас.

Граматики BNF, EBNF тощо можуть бути набагато простішими для читання та складання, ніж складне регулярне вираження. GOLD - це один із інструментів для таких речей.

Посилання на вікі 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
  • ЙмовірністьBaseParsing ...

Ви б не хотіли б пояснити більше про те, що робить це посилання та для чого це добре? "Відповіді лише на посилання" не дуже вітаються на біржі стеків
gnat

1
Ласкаво просимо до програмістів, Нік П. Будь ласка, ігноруйте downvote / r, але читайте сторінку на мета, на яку посилання @gnat пов'язане.
Крістофер Летте

@ Christoffer Lette Оцініть вашу відповідь. Постараємося мати це на увазі в майбутніх публікаціях. @ gnat Пауло Скардін віддзеркалює наміри моїх публікацій. Граматики BNF, EBNF тощо можуть бути набагато простішими для читання та складання, ніж складне регулярне вираження. GOLD - це один із інструментів для таких речей. Посилання c2 містить список можливих альтернатив, які можна гуглювати, включаючи деяку дискусію щодо них. По суті, це посилання "див. Також", щоб доповнити мою рекомендацію щодо граматики.
Nick P

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 (і так, це з рядкового конкомату)

проте в якості альтернативи ви також можете використати бібліотеку комбінаторів парсера, наприклад http://jparsec.codehaus.org/, яка дасть вам повний рекурсивний гідний аналізатор. знову реальна сила тут походить від композиції (цього разу функціональної композиції).


3

Я подумав, що варто було б згадати гроцькі вирази логсташа . Grok ґрунтується на ідеї складання довгих синтаксичних виразів із коротших. Це дозволяє зручно протестувати ці будівельні блоки та поставляється з упаковкою з понад 100 часто використовуваних моделей . Крім цих шаблонів, він дозволяє використовувати всі синтаксиси регулярних виразів.

Наведений вище шаблон, виражений у grok, (я тестував у програмі налагодження, але міг помилитися):

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

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


2

У F # у вас є модуль FsVerbalExpressions . Це дозволяє складати Regexes з вербальних виразів, у нього також є кілька попередньо вбудованих регулярних виразів (наприклад, 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.

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


-2

По-перше, зрозумійте, що код, який просто працює, - це поганий код. Хороший код також повинен точно повідомляти про виниклі помилки.

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

Тепер подумайте про свій приклад "розбору рядка чисел у форматі 1: 2: 3.4". Все, що робить регулярний вираз, - це повідомлення про "пропуск / помилку", який не дозволяє адекватному зворотному зв’язку представляти користувачеві (чи цей відгук є повідомленням про помилку в журналі або інтерактивним графічним інтерфейсом, де помилки відображаються червоним кольором як типи користувачів або що-небудь ще). Які типи помилок не вдається правильно описати? Поганий символ у першому номері, перший номер занадто великий, двокрапка після першого числа тощо.

Щоб перетворити "поганий код, який просто працює", в "хороший код, який забезпечує адекватні описові помилки", вам потрібно розбити регулярний вираз на багато менших регулярних регексів (як правило, такі маленькі регулярні вирази, що простіше зробити це без регексів в першу чергу ).

Зробити код читабельним / ремонтопридатним - лише випадковий наслідок покращення коду.


6
Напевно, не гарне припущення. Моє тому, що A) Це не стосується питання ( як зробити його читабельним?), В) Регулярне відповідність виразам - це пропустити / провалити, і якщо ви розбиєте його на точку, де ви можете точно сказати, чому він не вдався, ви втрачають багато сили та швидкості та збільшують складність, C) З питання немає жодних ознак того, що існує навіть можливість збігу матчу - це просто питання про те, щоб зробити Regex читабельним. Якщо ви контролюєте дані, що надходять та / або перевіряють їх перед початком, ви можете вважати, що вони дійсні.
Бобсон

A) Розбиття його на більш дрібні шматки робить його більш читабельним (як наслідок, щоб зробити його корисним). C) Якщо невідомі / недійсні рядки вводять частину програмного забезпечення, розумний розробник проаналізує (із повідомленням про помилки) в цей момент і перетворить дані у форму, яка не потребує повторного повторного аналізу - після цього регулярний вираз не потрібен. Б) є дурницею, яка стосується лише поганого коду (див. Пункти А та С).
Брендан

Перехід від вашого C: Що робити , якщо це є його логіка перевірки? Код ОП може бути саме тим, що ви пропонуєте - перевірити вхід, звітувати, якщо він недійсний, і перетворити його в придатну форму (через захоплення). Все, що ми маємо, - це сам вираз. Як би ви запропонували проаналізувати його, крім регексу? Якщо ви додасте зразок коду, який дасть той самий результат, я вилучу свій нижчий запис.
Бобсон

Якщо це "C: перевірка (з повідомленням про помилки)", це неправильний код, оскільки повідомлення про помилки є поганим. Якщо вона не вдається; це тому, що рядок був NULL, або тому, що в першому номері було занадто багато цифр, або тому, що перший роздільник не був :? Уявіть компілятор, який мав лише одне повідомлення про помилку ("ПОМИЛКА"), яке було занадто дурним, щоб сказати користувачеві, у чому проблема. А тепер уявіть тисячі веб-сайтів, які так само дурні і відображають (наприклад) "Неправильну адресу електронної пошти" та більше нічого.
Брендан

Крім того, уявіть, що напівпідготовлений оператор служби підтримки отримує звіт про помилку від абсолютно непідготовленого користувача, який говорить: Програмне забезпечення перестало працювати - останній рядок у журналі журналу програмного забезпечення - "ПОМИЛКА: Не вдалося витягнути незначний номер версії з рядка версії" 1: 2-3.4 '(очікується двокрапка після другого числа) "
Брендан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.