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

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

Стаття з групи Random UA
RegEx: 20 коротких кроків для освоєння регулярних виразів. Частина 1. RegEx: 20 коротких кроків для освоєння регулярних виразів. Частина 2. У цій частині ми перейдемо до речей ще більш складним. Але, освоїти їх, як і раніше, не складе особливих труднощів. Повторюся, що RegEx насправді легше, ніж він може здатися спочатку, і не потрібно бути семи п'ядей на лобі, щоб його освоїти, і почати застосовувати на ділі. Англомовний оригінал цієї статті тут . 20 коротких кроків для освоєння регулярних виразів.  Частина 3 - 1

Крок 11: круглі дужки ()як групи захоплення

20 коротких кроків для освоєння регулярних виразів.  Частина 3 - 2В останній задачі ми шукали різні види цілісних значень та числових значень з плаваючою комою (точкою). Але механізм регулярних виразів не робив відмінностей між цими двома типами значень, оскільки все відбито в одному великому регулярному вираженні. Ми можемо сказати движку регулярних виразів, що потрібно розрізняти різні види збігів, якщо укладемо наші міні-шаблони в круглі дужки:
pattern: ([AZ])|([az]) 
string:   The current President of Bolivia is Evo Morales .
matches: ^^^ ^^^^^^^^ ^^^^^^^^^^ ^^ ^^^^^^^ ^^ ^^^ ^^^^^^^ 
group:    122 2222222 122222222 22 1222222 22 122 1222222  
( Приклад ) Наведене вище регулярне вираз визначає дві групи захоплення, які індексуються починаючи з 1. Перша група захоплення відповідає будь-якій окремій літері, а друга група захоплення відповідає будь-якій окремій рядковій літері. Використовуючи знак 'або' |та круглі дужки ()як групу захоплення, ми можемо визначити один регулярний вираз, який відповідає декільком видам рядків. Якщо ми застосуємо це до нашого регулярного виразу для пошуку long/float із попередньої частини статті, то механізм регулярних виразів фіксуватиме відповідні збіги у відповідних групах. Перевіряючи, якій групі відповідає підрядок, ми можемо відразу визначити, чи є вона значенням float або long:
pattern: (\d*\.\d+[fF]|\d+\.\d*[fF]|\d+[fF])|(\d+[lL]) string: 42L 
12   x 3.4f 6l 3.3 0F LF .2F 0.
matches: ^^^ ^^^^ ^^ ^^ ^^^ 
group:    222 1111 22 11 111  
( Приклад ) Це регулярне вираз досить складне, і щоб його краще зрозуміти, давайте розберемо його на частини, і розглянемо кожен з цих шаблонів:
( // збігається з будь-яким "float" підрядком
  \d*\.\d+[fF]
  |
  \d+\.\d*[fF]
  |
  \d+[fF]
)
| // OR
( // збігається з будь-яким "long" підрядком
  \d+[lL]
)
Знак |та групи захоплення в дужках ()дозволяють нам зіставляти різні типи підрядків. У цьому випадку ми зіставляємо або числа з плаваючою комою "float", або довгі довгі цілі числа "long".
(
  \d*\.\d+[fF] // 1+ цифр праворуч від десяткової точки
  |
  \d+\.\d*[fF] // 1+ цифр зліва від десяткової точки
  |
  \d+[fF] // без крапки, лише 1+ цифр
)
|
(
  \d+[lL] // без крапки, лише 1+ цифр
)
У групі захоплення "float" у нас є три варіанти: числа з мінімум 1 цифрою праворуч від десяткової точки, числа з мінімум однією цифрою зліва від десяткової точки та числа без десяткової точки. Будь-які з них є "float", за умови, що до кінця додані літери "f" або "F". Усередині групи захоплення "long" у нас є тільки одна опція - у нас має бути 1 або більше цифр, за якими слідує символ "l" або "L". Механізм регулярних виразів шукатиме ці підрядки в даному рядку і індексуватиме їх у відповідній групі захоплення. Зверніть увагу, що ми не зіставляємо жодне з чисел, до яких не додано жодного з "l", "L", "f" або "F". Як слід класифікувати ці цифри? Ну, якщо вони мають десяткову точку, за замовчуванням у Java використовується значення "double". Інакше вони мають бути "int".

Закріпимо пройдене парою завдань:

Додайте ще дві групи захоплення до наведеного вище регулярного виразу, щоб воно класифікувало числа double або int. (Це ще одне складне питання, не засмучуйтесь, якщо це займе деякий час, у крайньому випадку дивіться моє рішення.)
pattern:
string:   42L 12 x 3.4f 6l 3.3 0F LF .2F 0. 
matches: ^^^ ^^ ^^^^ ^^ ^^^ ^^ ^^^ ^^ 
group:    333 44 1111 33 222 11 111
( Рішення ) Наступне завдання трохи простіше. Використовуйте групи захоплення у дужках (), знак 'або' |та діапазони символів для сортування наступного віку: "дозволено пити в США". (>= 21) та "заборонено пити в США" (<21):
pattern:
string:   7 10 17 18 19 20 21 22 23 24 30 40 100 120 
matches: ^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^^ ^^^ 
group:    2 22 22 22 22 22 11 11 11 11 11 11 111 111 
( Рішення )

Крок 12: спочатку визначте конкретніші збіги

20 коротких кроків для освоєння регулярних виразів.  Частина 3 - 3Можливо, у вас виникли деякі проблеми з останнім завданням, якщо ви спробували визначити "законно п'ють" як першу групу захоплення, а не другу. Щоб зрозуміти чому, погляньмо на інший приклад. Припустимо, ми хочемо записати окремо прізвища, що містять менше 4 символів, та прізвища, що містять 4 або більше символів. Давайте віддамо більш короткі імена першою групою захоплення, і подивимося, що станеться:
pattern: ([AZ][az]?[az]?)|([AZ][az][az][az]+) string 
:   Kim Job s Xu Clo yd Moh r Ngo Roc k.
matches: ^^^ ^^^ ^^ ^^^ ^^^ ^^^ ^^^ 
group:    111 111 11 111 111 111 111   
( Приклад ) За замовчуванням більшість движків регулярних виразів використовують жадібне зіставлення з основними символами, які ми бачабо досі. Це означає, що механізм регулярних виразів захоплюватиме максимально довгу групу, визначену якомога раніше у наданому регулярному виразі. Таким чином, хоча друга група, наведена вище, могла б захопити більше символів в іменах, таких як, наприклад, "Jobs" і "Cloyd", але оскільки перші три символи цих імен вже були захоплені першою групою захоплення, вони не можуть бути захоплені знову другий. Тепер внесемо невелике виправлення - просто змінимо порядок груп захоплення, помістивши першу більш конкретну (довшу) групу:
pattern: ([AZ][az][az][az]+)| ( [AZ][az]?[az]?) 
string:   Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
group:    222 1111 22 11111 1111 222 1111    
( Приклад )

Завдання... цього разу лише одне :)

"Більш конкретний" шаблон майже завжди означає "довший". Припустимо, що ми хочемо знайти два види "слів": спочатку ті, які починаються з голосних (конкретніше), потім ті, які не починаються з голосних (будь-яке інше слово). Спробуйте написати регулярний вираз для захоплення та ідентифікації рядків, які відповідають цим двом групам. (Групи нижче позначені літерами, а не пронумеровані. Ви повинні визначити, яка група має відповідати першій, а яка – другий.)
pattern:
string:   pds6f uub 24r2gp ewqrty l ui_op 
matches: ^^^^^ ^^^ ^^^^^ ^^^^^^ ^ ^^^^^ 
group:    NNNNN VVV NNNNNN VVVVVV N VVVVV
( Рішення ) Загалом, чим точніше ваше регулярне вираження, тим довшим воно вийде в результаті. І чим воно точніше, тим менша ймовірність, що ви захопите те, що вам не потрібно. Тому, хоча вони можуть виглядати лякаючими, довші регулярні вирази ~= кращі регулярні вирази. На жаль .

Крок 13: фігурні дужки {}для певної кількості повторень

20 коротких кроків для освоєння регулярних виразів.  Частина 3 - 4У прикладі з прізвищами з попереднього кроку у нас було 2 групи, що майже повторюються, в одному шаблоні:
pattern: ([AZ][az][az][az]+)| ( [AZ][az]?[az]?) 
string:   Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
group:    222 1111 22 11111 1111 222 1111    
Для першої групи нам потрібні були прізвища із чотирма або більше літерами. Друга група мала захоплювати прізвища з трьома або менш літерами. Чи існує якийсь простіший спосіб написати це, ніж повторювати ці [a-z]групи знову і знову? Існує, якщо використовувати для цього фігурні дужки {}. Фігурні дужки {}дозволяють нам вказати мінімальну та (необов'язково) максимальну кількість збігів попереднього символу або групи захоплення. Є три варіанти використання {}:
{X} // збігається точно X разів
{X,} // збігається >= X разів
{X,Y} // збігається >= X and <= Y разів
Ось приклади цих трьох різних синтаксисів:
pattern: [az] {11} 
string:   humuhumunuk unukuapua'a.
matches: ^^^^^^^^^^^^   
( Приклад )
pattern: [az] {18,} 
string:   humuhumunukunukuapua 'a.
matches: ^^^^^^^^^^^^^^^^^^^^^    
( Приклад )
pattern: [az] {11,18} 
string:   humuhumunukunukuap ua'a.
matches: ^^^^^^^^^^^^^^^^^^^    
( Приклад ) У наведених вище прикладах є кілька моментів, на які слідуєзвернути увагу:. По-перше, використовуючи нотацію {X}, попередній символ або група відповідатиме саме цій кількості (X) разів. Якщо в "слові" є більше символів (ніж число X), які могли б відповідати шаблону (як показано в першому прикладі), вони не будуть включені у відповідність. Якщо кількість символів менша за X, то повне зіставлення завершиться невдало (спробуйте змінити 11 на 99 у першому прикладі). По-друге, позначення {X,} та {X,Y} є жадібними. Вони намагатимуться відповідати якомога більшій кількості символів, водночас задовольняючи заданому регулярному виразу. Якщо ви вкажете {3,7}, можна буде зіставити від 3 до 7 символів, і якщо наступні 7 символів дійсні, тоді будуть зіставлені всі 7 символів. Якщо ви вкажете {1,}, і всі наступні 14 000 символів збігаються, то всі 14 000 із цих символів будуть включені у відповідний рядок. Як ми можемо використовувати це знання, щоб переписати наш вираз вище? Найпростішим покращенням може бути заміна сусідніх груп[a-z]на [a-z]{N}, де N вибирається відповідним чином:
pattern: ([AZ][az]{2}[az]+)|([AZ][az]?[az]?)  
... але це не робить ситуацію набагато кращою. Подивіться на першу групу захоплення: у нас є [a-z]{2}(що відповідає рівно 2 малим літерам), за якими слідує [a-z]+(що відповідає 1 або більше малим літерам). Ми можемо спростити це, запитивши 3 або більше малих літер, використовуючи фігурні дужки:
pattern: ([AZ][az]{3,})|([AZ][az]?[az]?) 
Друга група захоплення відрізняється. Нам потрібно не більше трьох символів у цих прізвищах, що означає, що у нас є верхня межа, але наша нижня межа дорівнює нулю:
pattern: ([AZ][az]{3,})|([AZ][az]{0,2}) 
Специфічність завжди краще при використанні регулярних виразів, тому було б розумно зупинитися на цьому, але я не можу не помітити, що ці два діапазони символів ( [AZ]і [az]) поряд один з одним виглядають майже як клас "word character" (символ слова), \w( [A-Za-z0-9_]) . Якщо ми впевнені, що наші дані містять лише добре відформатовані прізвища, то ми могли б спростити наше регулярне вираження, і написати просто:
pattern: (\w{4,})|(\w{1,3}) 
Перша група захоплює будь-яку послідовність з 4 або більше "word characters" ( [A-Za-z0-9_]), а друга група захоплює будь-яку послідовність від 1 до 3 "word characters" (включно). Чи це спрацює?
pattern: (\w{4,})|(\w{1,3}) 
string:   Kim Jobs Xu Cloyd Mohr Ngo Rock .
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
group:    222 1111 22 11111 1111 222 1111    
( Приклад ) Спрацювало! Як щодо такого підходу? І це набагато чистіше, ніж у попередньому прикладі. Оскільки перша група захоплення відповідає всім прізвищам з чотирма або більше символами, то ми могли б навіть змінити другу групу захоплення просто на \w+, так як це дозволило б нам захопити всі прізвища, що залишабося (з 1, 2 або 3 символами):
pattern: (\w{4,})|(\w+) 
string:   Kim Jobs Xu Cloyd Mohr Ngo Rock .
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
group:    222 1111 22 11111 1111 222 1111    
( Приклад )

Давайте допоможемо мозку засвоїти це, і вирішимо наступні 2 завдання:

Використовуйте фігурні дужки {}, щоб переписати регулярний вираз для пошуку номера соціального страхування з кроку 7:
pattern:
string: 113-25=1902 182-82-0192 H23-_3-9982 1I1-O0-E38B
matches:              ^^^^^^^^^^^^
( Рішення ) Припустимо, що система перевірки надійності пароля на веб-сайті вимагає, щоб паролі користувачів складали від 6 до 12 символів. Напишіть регулярний вираз, що позначає невірні паролі у списку нижче. Кожен пароль міститься в дужках ()для зручності зіставлення, тому переконайтеся, що регулярний вираз починається і закінчується буквальними (та )символами. Підказка: переконайтеся, що ви забороняєте літеральні дужки в паролях за допомогою [^()]або аналогічних, в іншому випадку ви отримаєте зрештою відповідність всьому рядку!
pattern:
string:   (12345) (my password) (Xanadu.2112) (su_do) (OfSalesmen!)
matches: ^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^  
( Рішення )

Крок 14: \bсимвол межі нульової ширини

20 коротких кроків для освоєння регулярних виразів.  Частина 3 – 5Останнє завдання було досить складним. Але що, якщо ми ще трохи ускладнимо її, уклавши паролі в лапки ""замість дужок ()? Чи можемо ми написати аналогічне рішення просто замінивши всі символи круглих дужок на символи лапок?
pattern: \"[^"]{0,5}\"|\"[^"]+\s[^"]*\" string: "12345" " 
my   password" "Xanadu.2112 " " su_do" " OfSalesmen!"
matches: ^^^^^^^ ^^^^^^^^^^^^^^ ^^^ ^^^  
( Приклад ) Вийшло не дуже вражаюче. Ви вже здогадалися, чому? Проблема полягає в тому, що ми шукаємо тут неправильні паролі. "Xanadu.2112" - хороший пароль, тому, коли регулярний вираз розуміє, що ця послідовність не містить прогалин або літеральних символів ", воно здається безпосередньо перед символом ", який обмежує пароль у правій частині. (Оскільки ми вказали, що символи "не можуть бути знайдені всередині паролів, використовуючи [^"].) Як тільки механізм регулярних виразів переконається, що ці символи не відповідають певному регулярному виразу, він запускається знову, саме в тому місці, де він зупинився - де був символ ", котрий обмежує "Xanadu.2112" праворуч. Звідти він бачить один символ пропуску,"– для нього це неправильний пароль! Загалом він знаходить цю послідовність " "і йде далі. Це зовсім не те, що ми хотіли б отримати... Було б чудово, якби ми могли вказати, що перший символ пароля має бути не пропуском. Чи є спосіб зробити це? (Наразі ви, напевно, вже зрозуміли, що відповіддю на всі мої риторичні питання є "так".) Так! Такий спосіб є! Багато движків регулярних виразів надають таку escape-послідовність як "кордон слова" \b. "Кордон слова" \b- це escape-послідовність нульової ширини, яка, як не дивно, відповідає межі слова. Пам'ятайте, що коли ми говоримо "слово", то маємо на увазі як будь-яку послідовність символів у класі \w, так і таку[A-Za-z0-9_]. Збіг за кордоном слова означає, що символ безпосередньо перед або відразу після послідовності \bповинен бути несимволом слова. Однак, при порівнянні, ми не включаємо цей символ в нашу захоплену підрядок. Це і є нульова ширина. Щоб побачити, як це працює, давайте розглянемо невеликий приклад:
pattern: \b[^]+\b 
string:   Ve still vant ze money , Lebowski .
matches: ^^ ^^^^^ ^^^^ ^^ ^^^^^ ^^^^^^^^  
( Приклад ) Послідовність [^ ]повинна відповідати будь-якому символу, який не є символом буквального пробілу. Так чому ж це не відповідає комою ,після money або точці " .після Lebowski? Це тому, що кома ,і точка .не є символами слова, тому між символами слова і несловесними символами створюються межі. Вони з'являються між yв кінці слова money і комою ,яка слідує за ним, і між " iсловом Lebowski і точкою .(повною зупинкою / періодом), яка слідує за ним. Регулярний вираз збігається на межах цих слів (але не на несловесних символах, які лише допомагають їх визначити). Але що станеться, якщо ми не ввімкнемо послідовність\bу наш шаблон?
pattern: [^] + 
string:   Ve still vant ze money, Lebowski. 
matches: ^^ ^^^^^ ^^^^ ^^ ^^^^^^ ^^^^^^^^^  
( Приклад ) Ага, тепер ми знаходимо і ці розділові знаки. Тепер давайте скористаємося межами слів, щоб виправити регулярний вираз для паролів у лапках:
pattern: \"\b[^"]{0,5}\b\"|\"\b[^"]+\s[^"]*\b\" string: "12345" "my 
password   " " Xanadu.2112" "su_do" "OfSalesmen!"
matches: ^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^  
( Приклад ) Розміщуючи межі слів усередині лапок ("\b...\b"), ми фактично говоримо, що перший і останній символи паролів, що збігаються, повинні бути "символами слова". Так що тут все працює нормально, але не працюватиме так само добре, якщо перший або останній символ пароля користувача не є символом слова:
pattern: \"\b[^"]{0,5}\b\"|\"\b[^"]+\s[^"]*\b\"
string: "відповідно про wordwordshort" "C++"
matches:   
( Приклад ) Подивіться, як другий пароль не позначений як "неправильний", навіть якщо він явно занадто короткий. Ви маєте бутиобережніз послідовностями \b, тому що вони відповідають кордонам тільки між символами \wі не \w. У наведеному вище прикладі, оскільки в паролях ми допустабо символи не \w, межа між \і першим/останнім символом пароля не гарантується як межа слова \b.

На завершення цього кроку вирішимо лише одне просте завдання:

Кордони слова корисні у механізмах підсвічування синтаксису, коли ми хочемо зіставити певну послідовність символів, але хочемо переконатися, що вони зустрічаються лише на початку чи наприкінці слова (або самі по собі). Припустимо, ми пишемо підсвічування синтаксису і хочемо виділити слово var, але тільки тоді, коли воно з'являється саме собою (не торкаючись інших символів слова). Чи зможете ви написати регулярний вираз для цього? Звичайно зможете, адже це дуже просте завдання ;)
pattern:
string:   var varx _var ( var j) barvarcar * var var -> { var }
matches: ^^^ ^^^ ^^^ ^^^ ^^^  
( Рішення )

Крок 15: "caret" ^як "початок рядка" та знак долара $як "кінець рядка"

20 коротких кроків для освоєння регулярних виразів.  Частина 3 - 6Послідовність меж слова \b(з останнього кроку попередньої частини статті) – не єдина спеціальна послідовність нульової ширини, доступна для використання у регулярних виразах. Двома найбільш популярними з них є "caret" ^- "початок рядка" та знак долара $- "кінець рядка". Включення одного з них у ваші регулярні вирази означає, що цей збіг повинен з'явитися на початку або в кінці вихідного рядка:
pattern: ^start|end$ 
string:   start end start end start end start end 
matches: ^^^^^ ^^^  
( Приклад ) Якщо ваш рядок містить розриви рядків, то ^startвідповідатиме послідовності "start" на початку будь-якого рядка, а end$відповідатиме послідовності "end" наприкінці будь-якого рядка (хоча це важко показати тут). Ці символи особливо корисні при роботі з даними розділів. Повернімося до проблеми "розміру файлу" з кроку 9, використовуючи ^"початок рядка". У цьому прикладі наші розміри файлів розділені пробілами " ". Тому ми хочемо, щоб кожен розмір файлу починався з цифри, якій передує символ пробілу або початок рядка:
pattern: (^| )(\d+|\d+\.\d+)[KMGT]B 
string:   6.6KB 1..3KB 12KB 5G 3.3MB KB .6.2TB 9MB .
matches: ^^^^^ ^^^^^ ^^^^^^ ^^^^ 
group:    222 122 1222 12    
( Приклад ) Ми вже так близько до мети! Але ви можете помітити, що у нас ще одна невелика проблема: ми зіставляємо символ пробілу перед допустимим розміром файлу. Тепер ми можемо просто ігнорувати цю групу захоплення (1), коли наш двигун регулярних виразів знайде її, або ми можемо використовувати групу без захоплення, яку ми побачимо на наступному кроці.

А поки що, вирішимо ще 2 завдання для тонусу:

Продовжуючи наш приклад підсвічування синтаксису з останнього кроку, деякі підсвічування синтаксису будуть відзначати кінцеві пробіли, тобто будь-які пробіли, які знаходяться між символом і кінцем рядка. Чи можете ви написати регулярний вираз для підсвічування лише кінцевих прогалин?
pattern:
string: myvec <- c(1, 2, 3, 4, 5)  
matches:                          ^^^^^^^  
( Рішення ) Простий синтаксичний аналізатор з роздільниками-комами (CSV) шукатиме "токени", розділені комами. Як правило, пробіл не має значення, якщо він не укладений у лапки "". Напишіть простий регулярний вираз для розбору CSV, який зіставляє токени між комами, але ігнорує (не захоплює) пробіл, який не знаходиться між лапками.
pattern:
string:   a, "b", "c d", e, f, "g h", dfgi,, k, "", l matches: 
^^ ^^^^ ^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^ ^^^ ^ 
group:    21 2221 2222212121 222221 222211 21 221 2    
( Рішення ) RegEx: 20 коротких кроків для освоєння регулярних виразів. Частина 4
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ