JavaRush /Java блог /Random UA /RegEx: 20 коротких кроків для освоєння регулярних виразів...
Artur
40 рівень
Tallinn

RegEx: 20 коротких кроків для освоєння регулярних виразів. Частина 4

Стаття з групи Random UA
RegEx: 20 коротких кроків для освоєння регулярних виразів. Частина 1 RegEx: 20 коротких кроків для освоєння регулярних виразів. Частина 2 20 коротких кроків для освоєння регулярних виразів. Частина 3 Ця, заключна частина, у середині торкнеться таких речей, якими користуються переважно майстри регулярних висловів. Але ж вам легко давався матеріал із попередніх частин, адже правда? Значить і з цим матеріалом ви справитеся з тією ж легкістю! Оригінал тут RegEx: 20 коротких кроків для освоєння регулярних виразів.  Частина 4 - 1 <h2>Крок 16: групи без захоплення (?:)</h2> RegEx: 20 коротких кроків для освоєння регулярних виразів.  Частина 4 - 2У двох прикладах на попередньому кроці ми захоплювали текст, який насправді нам не потрібний. У задачі "Розміри файлів" ми захопабо прогалини перед першою цифрою розмірів файлів, а в задачі "CSV" ми захопабо коми між кожним токеном. Нам не потрібно захоплювати ці символи, але нам потрібно використовувати їх для структурування нашого регулярного вираження. Це ідеальні варіанти використання групи без захоплення, (?:). Група без захоплення робить саме те, на що це схоже за змістом - вона дозволяє групувати символи та використовувати їх у регулярних виразах, але не захоплює їх у пронумерованій групі:
pattern: (?:")([^"]+)(?:") 
string: I тільки want "the text inside these quotes" .
matches:             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
group:                 1111111111111111111111111111    
( Приклад ) Тепер регулярний вираз відповідає тексту в лапках, а також символам лапок, але група захоплення захопила тільки текст в лапках. Для чого нам так робити? Справа в тому, що більшість двигунів регулярних виразів дозволяють вам відновлювати текст із груп захоплення, визначених у ваших регулярних виразах. Якщо ми зможемо обрізати зайві символи, які нам не потрібні, не включивши їх у наші групи захоплення, це спростить аналіз і маніпулювання текстом пізніше. Ось як можна почистити парсер CSV з попереднього кроку:
pattern: (?:^|,)\s*(?:\"([^",]*)\"|([^", ]*)) string: a , " 
b   " , " cd " , e , f , " gh ", dfgi ,, k , "", l 
matches: ^ ^ ^^^ ^ ^ ^^^ ^^^^ ^ ^ 
group:    2 1 111 2 2 111 2222 2 2    
( Приклад ) Тут є кілька речей, на які варто звернути увагу: По-перше, ми більше не захоплюємо коми, так як ми змінабо групу захоплення на групу (^|,)без захоплення (?:^|,). По-друге, ми вклали групу захоплення у групу без захоплення. Це корисно, коли, наприклад, вам потрібно, щоб група символів відображалася в певному порядку, але ви дбаєте лише про підмножини цих символів. У нашому випадку нам потрібно, щоб символи не лапок і не коми [^",]*відображалися в лапках, але насправді нам не потрібні самі символи лапок, тому їх не потрібно було захоплювати. Нарешті, <mark>зверніть увагу</mark>,kl. Лапки ""є підстрокою, але між лапками немає символів, тому відповідний підрядок не містить символів (має нульову довжину). <h3>Закріпимо знання? Ось дві з половиною завдання, які допоможуть нам у цьому:</h3> Використовуючи групи без захоплення (і групи захоплення, і класи символів, і т.д.), напишіть регулярний вираз, який захоплює лише правильно відформатовані розміри файлів у рядку нижче :
pattern:
string:   6.6KB 1..3KB 12KB 5G 3.3MB KB .6.2TB 9MB .
matches: ^^^^^ ^^^^^ ^^^^^^ ^^^^ 
group:    11111 1111 11111 111    
( Рішення ) HTML-теги, що відкривають, починаються з символу <і закінчуються символом >. Теги HTML, що закривають, починаються з послідовності символів </і закінчуються символом >. Ім'я тега міститься між цими символами. Чи можете ви написати регулярний вираз, щоб захопити лише імена у наступних тегах? (Можливо, вам вдасться вирішити цю проблему без використання груп без захоплення. Спробуйте вирішити це двома способами! Один раз за допомогою груп та один раз без них.)
pattern:
string:   <p> </span> <div> </kbd> <link> 
matches: ^^^ ^^^^^^ ^^^^^ ^^^^^^ ^^^^^^ 
group:    1 1111 111 111 1111    
( Рішення за допомогою груп без захоплення ) ( Рішення без допомоги груп без захоплення ) <h2>Крок 17: зворотні посилання \Nта іменовані групи захоплення</h2> RegEx: 20 коротких кроків для освоєння регулярних виразів.  Частина 4 – 3Хоча я і попереджав вас у вступі, що спроба створити HTML-парсер за допомогою регулярних виразів зазвичай призводить до душевних страждань, останній приклад - гарний перехід до іншої (іноді) корисної функції більшості регулярних виразів: зворотних посилань (backreferences). Зворотні посилання схожі на групи, що повторюються, в яких ви можете спробувати захопити один і той же текст двічі. Але вони відрізняються в одному важливому аспекті - вони будуть захоплювати лише один і той самий текст, символ за символом. У той час як група, що повторюється, дозволить нам захопити щось на кшталт цього:
pattern: (he(?:[az])+) 
string:   heyabcdefg hey heyo heyellow heyyyyyyyyy 
matches: ^^^^^^^^^^^ ^^^ ^^^^ ^^^^^^^^^ ^^^ ^^^^^^^^ 
group:    1111111111 111 1111 111111111 11111111111    
( Приклад ) ... то зворотне посилання відповідатиме тільки цьому:
pattern: (he([az])(\2+)) 
string: heyabcdefg hey heyo heyellow heyyyyyyyyy 
matches:                              ^^^^^^^^^^^ 
group:                                 11233333333    
( Приклад ) Групи захоплення, що повторюються, корисні, коли ви хочете повторно зіставити один і той же шаблон, тоді як зворотні посилання хороші, коли ви хочете зіставити один і той же текст. Наприклад, ми могли б використовувати зворотне посилання, щоб спробувати знайти відповідні HTML-теги:
pattern: <(\w+)[^>]*>[^<]+<\/\1> 
string:   <span style="color: red">hey</span> 
matches: ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
group:    1111    
( Приклад ) <mark>Зверніть увагу</mark>, що це надзвичайно спрощений приклад, і я настійно рекомендую вам не намагатися писати аналізатор HTML на основі регулярних виразів. Це дуже складний синтаксис, і вам швидше за все стане погано. Іменовані групи захоплення дуже схожі на зворотні посилання, тому я коротко розповім про них тут. Єдина різниця між зворотними посиланнями та іменованою групою захоплення полягає в тому, що... іменована група захоплення має ім'я:
pattern: <(?<tag>\w+)[^>]*>[^<]+<\/(?P=tag)></tag> string: < 
span   style="color: red">hey< /span> 
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
group:    1111    
( Приклад ) Ви можете створити іменовану групу захоплення за допомогою (?<name>...) або (?'name'...) синтаксису (.NET-сумісний регулярний вираз) або з таким синтаксисом (?P<name>). ..) або (?P'name'...) (Python-сумісний регулярний вираз). Оскільки ми використовуємо PCRE (Perl-сумісний регулярний вираз), що підтримує обидві версії, ми можемо використовувати будь-який з них тут. (Java 7 скопіювала синтаксис . name) (Python). Знову ж таки, PCRE підтримує всі ці різні варіанти. Ви можете прочитати більше про іменовані групи захоплення тутале це була велика частина того, що вам дійсно потрібно знати про них. <h3>Завдання нам на допомогу:</h3> Використовуйте зворотні посилання, щоб допомогти мені згадати ... еммм ... ім'я цієї людини.
pattern:
string: "Hi my name's Joe." [later] "What's that guy's name? Joe ?".
matches:        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^ 
group:                  111    
( Рішення ) <h2>Крок 18: погляд вперед (lookahead) і погляд назад (lookbehind)</h2> RegEx: 20 коротких кроків для освоєння регулярних виразів.  Частина 4 - 4Зараз ми заглибимося у деякі розширені функції регулярних виразів. Все, аж до кроку 16, я використовую досить часто. Але ці останні кілька кроків призначені тільки для людей, які дуже серйозно використовують regex для порівняння складних виразів. Іншими словами, майстри регулярних виразів. "Погляд уперед" і "погляд назад" можуть здатися досить складними, але насправді вони не надто складні. Вони дозволяють зробити щось схоже на те, що ми робабо з групами без захоплення раніше - перевіряти, чи існує якийсь текст безпосередньо перед або відразу після фактичного тексту, який ми хочемо зіставити. Наприклад, припустимо, що ми хочемо зіставляти лише назви речей, які люди люблять, але тільки якщо вони з ентузіазмом ставляться до цього (тільки якщо вони закінчують свою пропозицію знаком оклику). Ми могли б зробити щось на зразок:
pattern: (\w+)(?=!) 
string: I like desk. I appreciate stapler. I love lamp !
matches:                                           ^^^^ 
group:                                              1111    
( Приклад ) Ви можете бачити, як зазначена вище група захоплення (\w+), яка зазвичай відповідає будь-якому з слів у уривку, відповідає тільки слову lamp. Позитивний "погляд вперед" (?=!)означає, що ми можемо порівнювати лише ті послідовності, які закінчуються на!але, насправді, ми не зіставляємо сам символ знака оклику. Це важлива відмінність, тому що з групами без захоплення ми зіставляємо символ, але не захоплюємо його. За допомогою lookaheads та lookbehinds ми використовуємо символ для побудови нашого регулярного виразу, але потім ми навіть не зіставляємо його з ним самим. Ми можемо порівняти його пізніше у нашому регулярному вираженні. Усього існує чотири види lookaheads і lookbehinds: позитивний погляд вперед (?=...), негативний погляд вперед (?!...), позитивний погляд назад (?<=...) і негативний погляд назад (?<!.) ..). Вони роблять те, на що вони схожі - позитивні lookahead і lookbehind дозволяють обробнику регулярних виразів продовжувати зіставлення, тільки коли текст, що міститься в lookahead / lookbehind, дійсно збігається. Негативні lookahead і lookbehind роблять протилежне - вони дозволяють регулярному виразу збігатися лише тоді, коли текст, що міститься в lookahead/lookbehind, не збігається. Наприклад, хочемо зіставити імена методів лише у ланцюжку послідовностей методів, а чи не об'єкт, з якого вони працюють. І тут кожному імені методу має передувати символ.. Тут може допомогти регулярне вираження, що використовує простий погляд:
pattern: (?<=\.)(\w+) 
string: myArray. flatMap.aggregate.summarise.print !
matches:         ^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^ ^^^^^ 
group:            11111111 111111111 111111111 11111    
( Приклад ) У наведеному вище тексті ми зіставляємо будь-яку послідовність символів слова \w+, але тільки в тому випадку, якщо їм передує символ .. Ми могли б досягти чогось подібного, використовуючи групи без захоплення, але результат вийде трохи брудніше:
pattern: (?:\.)(\w+) 
string: myArray .flatMap.aggregate.summarise.print !
matches:        ^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ ^^^^^ 
group:            11111111 111111111 111111111 11111    
( Приклад ) Незважаючи на те, що він коротший, він відповідає символам, які нам не потрібні. Хоча цей приклад може здатися тривіальним, lookaheads і lookbehinds дійсно можуть допомогти нам очистити наші регулярні вирази. <h3>Залишилося зовсім небагато до фінішу! Наступні 2 завдання наблизять нас до нього ще на 1 крок: Негативний lookbehind (?<!...) дозволяє движку регулярних виразів продовжувати спроби знайти збіг, тільки якщо текст, що міститься всередині негативного lookbehind, не відображається до частини тексту, що залишилася. , з якою потрібно знайти відповідність. Наприклад, ми могли б використовувати регулярний вираз, щоб знайти відповідності лише прізвищам жінок, які відвідують конференцію. Для цього ми хотіли б переконатися, що прізвища людини не передуєMr.. Чи можете ви написати регулярний вираз для цього? (Можна припустити, що прізвища мають довжину не менше чотирьох символів.)
pattern:
string: Mr. Brown, Ms. Smith , Mrs. Jones , Miss Daisy , Mr. Green
matches:                ^^^^^ ^^^^^ ^^^^^ 
group:                   11111 11111 11111    
( Рішення ) Припустимо, що ми очищаємо базу даних і ми маємо стовпець інформації, який позначає відсотки. На жаль, деякі люди записали числа у вигляді десяткових значень у діапазоні [0,0, 1,0], у той час як інші написали відсотки в діапазоні [0,0%, 100,0%], а треті написали відсоткові значення, але забули літерал знак відсотка %. Використовуючи негативний погляд вперед (?!...), чи можете ви помітити ті значення, які мають бути відсотками, але у яких відсутні знаки %? Це повинні бути значення, що строго перевищують 1,00, але без кінцевого %. (Жодне число не може містити більше двох цифр до або після десяткової точки.) <mark>Зверніть увагу</mark>, що це рішення надзвичайно складне. Якщо ви зможете вирішити цю проблему, не заглядаючи у мою відповідь, то у вас вже є величезні навички у регулярних виразах!
pattern:
string: 0.32 100.00 5.6 0.27 98% 12.2% 1.01 0.99% 0.99 13.13 1.10 
matches:      ^^^^^^ ^^^ ^^^^ ^^^^^ ^^^^ 
group:         111111 111 1111 11111 1111    
( Рішення ) <h2>Крок 19: умови в регулярних виразах</h2> RegEx: 20 коротких кроків для освоєння регулярних виразів.  Частина 4 – 5Зараз перейшли до того етапу, коли більшість людей вже не використовуватимуть регулярні висловлювання. Ми розглянули, ймовірно, 95% сценаріїв використання простих регулярних виразів, і все, що робиться на кроках 19 і 20 зазвичай виконується більш повнофункціональною мовою маніпулювання текстом, таким як awk або sed (або мовою програмування загального призначення). Тим не менш, давайте продовжимо, просто щоб ви знали, на що дійсно здатне регулярне вираження. Хоча регулярні вирази не є повними за Тьюрингом, Деякі движки регулярних виразів пропонують функції, які дуже схожі на повну мову програмування. Одна з таких особливостей є "умовою". Умовні висловлювання Regex допускають оператори if-then-else, де обрана гілка визначається або "поглядом вперед", або "поглядом назад", про які ми дізналися на попередньому кроці. Наприклад, ви можете захотіти порівняти лише дійсні записи у списку дат:
pattern: (?<=Feb )([1-2][0-9])|(?<=Mar )([1-2][0-9]|3[0-1]) string: Dates 
worked : Feb 28 , Feb 29 , Feb 30, Mar 30 , Mar 31  
матчі:                   ^^ ^^ ^^ ^^ 
group:                      11 11 22 22    
( Приклад ) <mark>Зверніть увагу</mark>, що зазначені вище групи також індексуються за місяцями. Ми могли б написати регулярний вираз для всіх 12 місяців і зафіксувати лише дійсні дати, які потім були б об'єднані у групи, що проіндексовані за місяцем року. Вище використовується свого роду структура, подібна до if, яка шукатиме збіги в першій групі, тільки якщо "Feb" передує числу (і аналогічно для другої). Але що, якби ми хотіли використовувати спеціальну обробку лише для лютого? Щось на кшталт "якщо передує "Feb", зробіть це, інакше зробіть цю іншу річ". Ось як це роблять умовні вирази:
pattern: (?(?<=Feb )([1-2][0-9])|([1-2][0-9]|3[0-1])) 
string: Dates worked: Feb 28 , Feb 29 , Feb 30, Mar 30 , Mar 31  
матчі:                   ^^ ^^ ^^ ^^ 
group:                      11 11 22 22    
( Приклад ) Структура if-then-else виглядає як (?(If)then|else), де (if) замінюється "поглядом вперед" або "поглядом назад". У наведеному прикладі (if) записаний як (?<=Feb). Ви можете бачити, що ми зіставляли дати більше 29, але тільки якщо вони не йшли за "Feb". Використання ж lookbehinds ("поглядів назад") в умовних виразах корисно, якщо ви хочете переконатися, що збіг передує будь-який текст. Позитивні lookahead умовні вирази можуть збивати з пантелику, тому що сама умова не відповідає жодному тексту. Тому, якщо ви хочете, щоб умова if коли-небудь мало значення, вона повинна бути порівнянна з lookahead, як показано нижче:
pattern: (?(?=exact)exact|else)wo 
string: exact else exactwo elsewo  
matches:            ^^^^^^^ ^^^^^^
( Приклад ) Це означає, що позитивні lookahead умовні вислови марні. Ви перевіряєте, чи знаходиться цей текст попереду, а потім надаєте шаблон відповідності, щоб слідувати йому, коли він є. Умовне вираження не допомагає нам тут взагалі. Ви також можете просто замінити вищенаведене на більш простий регулярний вираз:
pattern: (?:exact|else)wo 
string: exact else exactwo elsewo  
matches:            ^^^^^^^ ^^^^^^
( Приклад ) Отже, емпіричне правило для висловлювань з умовами: тест, тест, і ще раз тест. Інакше рішення, які ви вважаєте очевидними, зазнають невдачі найбільш захоплюючими та несподіваними способами :) <h3>Ось ми і підійшли до останнього блоку завдань, який відокремлює нас від завершального, 20-го кроку:</h3> Напишіть регулярний вираз, який використовує негативний lookahead умовний вираз, щоб перевірити, чи починається наступне слово з великої літери. Якщо це так, захопіть лише одну заголовну літеру, а потім малі літери. Якщо це не так, то захопіть будь-які символи слова.
pattern:
string:   Jones Smith 9sfjn Hobbes 23r4tgr9h CSV Csv vVv 
matches: ^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^ ^^^ ^^^ 
group:    22222 22222 11111 222222 111111111 222 111    
( Рішення ) Напишіть негативний lookbehind умовний вираз, який захоплює текст owns, тільки якщо йому не передує текст cl, і яке захоплює текст ouds, тільки коли йому передує текст cl. (Трохи надуманий приклад, але що поробиш...)
pattern:
string: Those clowns owns some cl ouds . ouds.
matches:              ^^^^ ^^^^   
( Рішення ) <h2>Крок 20: рекурсія та подальше навчання</h2> RegEx: 20 коротких кроків для освоєння регулярних виразів.  Частина 4 – 6Насправді, є дуже багато всього, що можна втиснути в 20-крокове введення в будь-яку тему, і регулярні висловлювання не є винятком. Існує безліч різних реалізацій та стандартів для регулярних виразів , які можна знайти в Інтернеті. Якщо ви хочете дізнатися більше, я пропоную вам відвідати чудовий сайт regularexpressions.info , це фантастичний довідник, і я, звичайно, багато дізнався від туди про регулярні висловлювання. Я настійно рекомендую його, а також regex101.comдля тестування та публікації ваших творінь. На цьому завершальному кроці я дам вам ще трохи знання про регулярні вирази, а саме: як писати рекурсивні вирази. Прості рекурсії досить прості, але подумаємо, що це означає в контексті регулярного вираження. Синтаксис простий рекурсії у регулярному вираженні записується так: (?R)?. Але, звичайно, цей синтаксис має з'являтися усередині самого виразу. Те, що ми зробимо, це вкладемо вираз у себе, довільну кількість разів. Наприклад:
pattern: (hey(?R)?oh) 
string:   heyoh heyyoh heyheyohoh hey oh heyhey heyheyyohoh  
matches: ^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^ 
group:    11111 11111111111 11111111111    
( Приклад ) Оскільки вкладене вираз є необов'язковим ( (?R)супроводжується ?), то найпростіший збіг - просто повністю ігнорувати рекурсію. Отже, hey, а потім ohзбігається ( heyoh). Щоб порівняти будь-який складніший вираз, ніж це, ми повинні знайти цю збігаючу підрядок, вкладену всередину себе в тій точці виразу, в яку ми вставабо (?R)послідовність. Іншими словами, ми могли б знайти heyheyohoh або heyheyheyohohoh, і так далі. Одна з чудових особливостей цих вкладених виразів полягає в тому, що, на відміну від зворотних посилань та іменованих груп захоплення, вони не обмежують вас відповідно до точного тексту, який ви зіставляли раніше, символ за символом. Наприклад:
pattern: ([Hh][Ee][Yy](?R)?oh) 
string:   heyoh heyyoh hEyHeYohoh hey oh heyhey hEyHeYHEyohohoh  
matches: ^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^ 
group:    11111 1111111111 111111111111111    
( Приклад ) Ви можете собі уявити, що механізм регулярних виразів буквально копіює і вставляє ваш регулярний вираз довільну кількість разів. Звичайно, це означає, що іноді воно може робити не те, на що ви могли сподіватися:
pattern: ((?:\(\*)[^*)]*(?R)?(?:\*\))) 
string: (* comment (* nested *) not *)
matches:            ^^^^^^^^^^^^^ 
group:               111111111111    
( Приклад ) Чи можете ви сказати, чому цей регулярний вираз захопив лише вкладений коментар, а не зовнішній коментар? Одне можна сказати, напевно: при написанні складних регулярних виразів завжди перевіряйте їх, щоб переконатися, що вони працюють так, як ви думаєте. Ось і добігло кінця це швидкісне ралі по дорогах регулярних виразів. Сподіваюся, вам сподобалася ця подорож. Ну, і насамкінець, я залишу тут, як і обіцяв на початку, кілька корисних посилань для більш поглибленого вивчення матеріалу:
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ