__TOC__
Отримання потрібної частки з великого масиву тексту може стати викликом. Рядки в Python мають методи для пошуку та заміни: index(), find(), split(), count(), replace() та подібні. Але ці методи обмежені найпростішими випадками. Наприклад, метод index() шукає єдиний, жорстко зафіксований підрядок, і пошук завжди чутливий до регістру. Щоб здійснити нечутливі до регістру пошуки в рядку s, потрібно застосувати s.lower(), чи s.upper() і переконатись що запит написаний в тому ж регістрі. Методи replace() та split() мають ті ж обмеження.
Якщо вашої цілі можна досягти використовуючи методи для роботи з рядками, ви повинні використовувати їх. Вони швидкі, прості, їх просто читати. Про швидкий, простий і читабельний код можна розказувати багато гарного. Але якщо ви використовуєте багато цих функцій в перемішку з операторами if для обробки особливих випадків, чи якщо ви з'єднуєте виклики split() та join() щоб різати та перемішувати ваші рядки, можливо, вам варто буде перейти до регулярних виразів.
Регулярні вирази - це потужний і (переважно) стандартизований спосіб пошуку, заміни та лексичного аналізу тексту з складними шаблонами символів. Хоча, синтаксис регулярних виразів досить щільний і не схожий на звичайний код, але результат може виявитись більш читабельним ніж саморобне рішення, що використовує велику кількість рядкових функцій. Існує навіть спосіб вставити в регулярний вираз коментарів, тому ви можете включати в них документацію.
Приклад: Адреси Ця серія прикладів викликана проблемою з реального життя, з якою я стикнувся на моїй роботі кілька років тому, коли я хотів отримати та стандартизувати адреси вулиць з старої системи перед тим, як імпортувати її в нову. (Бачте, я не просто даю висмоктані з пальця приклади, вони справді корисні). Цей приклад показує як я підійшов до проблеми.
>>> s = '100 NORTH MAIN ROAD' >>> s.replace('ROAD', 'RD.') '100 NORTH MAIN RD.'
Моєю метою було стандартизувати адреси вулиць так, щоб 'ROAD' завжди записувалось скорочено, як 'RD.'. На перший погляд, це виглядає просто, і я подумав, що можна використати метод для роботи з рядками replace(). В будь-якому випадку дані були завжди в верхньому регістрі, тому чутливість до регістру не мала бути проблемою. І в цьому надзвичайно простому прикладі, s.replace() чудово виконала свою роботу.
>>> s = '100 NORTH BROAD ROAD' >>> s.replace('ROAD', 'RD.') '100 NORTH BRD. RD.'
Життя, на жаль, повне контрприкладів, і я швидко виявив це. Проблемою є те, що 'ROAD' з'являється в адресі двічі, один раз як частина імені вулиці 'BROAD', і один раз як ціле слово. Метод replace() бачить ці два випадки, і сліпо замінює обидвох, тим часом, я бачу як мої адреси псуються.
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') '100 NORTH BROAD RD.'
Щоб розв'язати проблему адрес з більш ніж одним підрядком 'ROAD', можна, наприклад, зробити так: шукати 'ROAD' лише в останніх чотирьох символах адреси (s[-4:1]), і не чіпати решту рядка (s[:-4]). Але ви можете бачити, що це вже досить заплутано. (Якщо б ви замінювали 'STREET' на 'ST.', ви повинні були б використовувати s[:-6] та s[-6:].replace(...).) Ви б хотіли повернутись через пів року щоб зневаджувати це? Я знаю що я б не хотів.
>>> import re
Настав час перейти до регулярних виразів. В мові Python, вся функціональність пов'язана з регулярними виразами міститься в модулі re.
>>> re.sub('ROAD$', 'RD.', s) '100 NORTH BROAD RD.'
Погляньте на перший параметр: 'ROAD$'. Це простий регулярний вираз, який співставляється 'ROAD' лише тоді, коли знаходить його в кінці рядка. $ означає "кінець рядка". (Існує також протилежний за значенням символ - ^, який означає "початок рядка"). Використовуючи метод re.sub() шукаємо в рядку s співпадіння з регулярним виразом 'ROAD$' та замінюємо його на 'RD.'. Це спрацьовує для ROAD в кінці рядка s, але не спрацьовує для слова BROAD, тому що воно знаходиться посередині.
Продовжуючи історію з очисткою адрес: я невдовзі відкрив, що в попередньому прикладі, співставляння 'ROAD' кінцю рядка, не достатньо добре, бо деякі адреси просто закінчуються назвою вулиці. Це працювало в більшості випадків, але коли назвою вулиці було 'BROAD', регулярний вираз виявляв співпадіння з кінцівкою 'ROAD', а це не те що мені було потрібно.
>>> s = '100 BROAD' >>> re.sub('ROAD$', 'RD.', s) '100 BRD.' >>> re.sub('\\bROAD$', 'RD.', s) '100 BROAD'
Що мені справді було потрібно - це співпадіння з словом ROAD на кінці рядка, але тільки тоді, коли це окреме слово, а не частина якогось іншого слова. Щоб виразити це в регулярному виразі використовують символ \b який означає що в цьому місці повинна знаходитись межа (boundary) слова. В Python, це ускладнюється тим фактом, що символ '\' в рядку сам повинен екрануватись. Це іноді називаються "чумою бекслешів", і це є причиною того що писати регулярні вирази в Perl простіше ніж в Python. З іншого боку, Perl змішує регулярні вирази з рештою синтаксису, тому іноді важко відрізнити синтаксичну помилку від помилки в регулярному виразі.
>>> re.sub(r'\bROAD$', 'RD.', s) '100 BROAD'
Щоб позбутись надлишку бекслешів, ви можете використати те, що називається "чистим (raw) рядком", додавши перед рядком символ r. Це повідомляє інтерпретатору Python, що нічого в цьому рядку не повинно екрануватись. '\t' - це рядок з одного символу табуляції, а r'\t' - це рядок з двох символів: бекслеша та букви t. Раджу вам завжди використовувати чисті рядки для опису регулярних виразів, бо інакше все робиться занадто заплутаним занадто швидко (а регулярні вирази з самого початку достатньо заплутані).
>>> s = '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD$', 'RD.', s) '100 BROAD ROAD APT. 3'
Ох! На жаль, я пізніше знайшов ще більше прикладів, які суперечать моїм міркуванням. В цьому випадку, адреса вулиці містила слово 'ROAD' як окреме слово, але воно не було на кінці рядка, бо адреса містила номер квартири. Через те що 'ROAD' не в кінці рядка виклик re.sub() не здійснював взагалі ніяких замін, і ви отримували незмінний рядок назад, що не те що вам було потрібно.
>>> re.sub(r'\bROAD\b', 'RD.', s) '100 BROAD RD. APT 3'
Щоб розв’язати цю проблему, я вилучив симовол $ і додав ще один \b. Тепер регулярний вираз читається як "знайди відповідність шаблону 'ROAD' коли це окреме слово де-небудь в рядку", не залежно від того на початку, в кінці чи десь посередині.
Приклад: Римські числа Швидше за все ви вже бачили римські числа, навіть якщо ви їх і не впізнали. Їх можна побачити на фасадах старих будинків (MCMXLVI замість "збудовано в 1946), чи при нумерації розділів в деяких книгах. Така система запису чисел з’явилась ще в римській імперії, звідки й назва.
В римській системі числення існує кілька символів, які повторююься і комбінуються різним чином щоб утворювати числа:
I - 1 V - 5 X - 10 L - 50 C - 100 D - 500 M - 1000
А ось деякі загальні правила утворення римських чисел:
Іноді символи аддитивні. I це 1, II це 2, і III це 3. VI це 6 (буквально, "5 і 1"), VII це 7, і VIII - 8. Символи що позначають степені десяти (I, X, C, та M) можуть повторюватись до трьох разів. Далі потрібно віднімати від наступного за значенням символа, зазвичай кратного 5. Не можна записувати 4 як IIII, замість цього пишуть IV (на 1 менше ніж 5). 40 записується як XL (на 10 менше ніж 50), 41 як XLI, 42 як XLII, 43 як XLIII, а 44 як XLIV (на 10 менше ніж 50, і ще на одиницю менше ніж 5). Іноді символи ... пряма протилежність аддитивним. Ставлячи деякі символи перед іншими, ви віднімаєте їх від загального значення. Наприклад, щоб записати 9, ви віднімаєте 1 від 10, записуючи це як IX. 8 це VIII, але 9 - це IX, бо не можна писати VIIII (дивись попереднє правило). 90 це XC, 900 - CM. Символи що не є степенями 10 не повторюються двічі підряд. 10 записується як X, і ніколи як VV. 100 - завжди C, і ніколи не LL. Римські цифри читаються зліва направо, і порядок символів має велике значення. DC - це 600, а CD - зовсім інше значення - 400 (на 100 менше ніж 500). CI це 101, а IC навіть не є правильним римським числом, бо не можна віднімати 1 одразу від 100. Число 99 записується як XCIX (на 10 менше ніж 100, і ще на 1 менше ніж 10).
Перевірка тисяч Що потрібно щоб перевірити що рядок є правильним римським числом? Давайте проаналізуємо це по одній цифрі за раз. Так як римські числа завжди записуються від найбільших до найменших цифр, почнемо з найбільших - тисяч. Для чисел починаючи від значення 1000 і вище, тисячі в них записуються послідовністю символів M.
>>> import re >>> pattern = '^M?M?M?$' Цей шаблон складається з трьох частин. ^ співставляється тільки з тим що знаходиться на початку рядка. Без нього відповідності шаблону знаходились би в довільних місцях рядка, а це не те що вам потрібно. Вам навпаки, потрібно переконатись, що символи M, якщо вони є, знаходяться на початку рядка. M? відповідає або символу M, або відсутності будь-яких символів. Так як воно повторене тричі, то шаблону відповідатиме від нуля до трьох символів M підряд. $ співставляється з кінцем рядка. В поєднанні з символом ^ на початку, він означає, що рядок повинен відповідати шаблону повністю, без жодних невідповідних символів на початку чи в кінці.
>>> re.search(pattern, 'M') <_sre.SRE_Match object at 0106FB58>
Основою модуля re є функція search(), яка бере регулярний вираз (pattern), і рядок ('M'), і намагається знайти відповідності. Якщо знаходить, то повертає об’єкт який містить методи що описують знайдену відповідність, інакше повертає None. Нас на поки що цікавить лише наявність чи відсутність співпадіння, а це можна вияснити лише просто за значенням яке повертається. 'M' відповідає шаблону, тому що співпадає з першим символом M в шаблоні, а решта (як і він сам) позначені як не обов’язкові.
>>> re.search(pattern, 'MM') <_sre.SRE_Match object at 0106C290>
'MM' теж відповідає шаблону, бо співставляється першим двом M?M?, а третє не враховується.
>>> re.search(pattern, 'MMM') <_sre.SRE_Match object at 0106AA38>
'МММ' відповідає всім трьом символам шаблону.
>>> re.search(pattern, 'MMMM')
'MMMM' не відповідає шаблону. Перші три символи M утворюють відповідність з шаблоном, але далі регулярний вираз вимагає закінчення рядка (через символ $), а так як рядок ще не закінчився (є четверте M), то повертається None.
>>> re.search(pattern, '') <_sre.SRE_Match object at 0106F4A8>
Що цікаво, порожній рядок теж відповідає шаблону, бо всі три символи в ньому необов’язкові.
Перевірка сотень
Сотні перевіряти дещо складніше ніж тисячі, тому що існує кілька взаємновиключних способів їх запису залежно від значення.
100 = C 200 = CC 300 = CCC 400 = CD 500 = D 600 = DC 700 = DCC 800 = DCCC 900 = CM
Тому, існує чотири можливі шаблони:
CM CD Від нуля до трьох символів C D після якого іде від нуля до трьох символів C
Останні два можна поєднати: Необов’язкове D після якого іде від нуля до трьох символів C
Наступний приклад покаже як перевірити сотні в римських числах. >>> import re >>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'
Цей шаблон починається як попередній, символом відповідності початку рядка ^, і перевіркою тисяч M?M?M?. Далі в дужках іде нова частина, яка описує набір трьох взаємновиключних шаблонів розділених вертикальними рисками: CM, CD, та D?C?C?C?. Парсер регулярних виразів перевіряє ці три шаблони по порядку, бере перший, який містить відповідність рядку, і ігнорує всі інші.
>>> re.search(pattern, 'MCM') <_sre.SRE_Match object at 01070390>
'MCM' відповідає шаблону, тому що перше M відповідає, друге і третє необов’язкові, і CM відповідає (тому частини CD і D?C?C?C? навіть не беруться до розгляду). MCM це римський запис числа 1900.
>>> re.search(pattern, 'MD') <_sre.SRE_Match object at 01073A50>
'MD' містить відповідність, тому що перше M відповідає рядку, друге і третє ігноруються, а шаблону D?C?C?C? відповідає символу D (три символи C необов’язкові і ігноруються). MD - це римський запис числа 1500.
>>> re.search(pattern, 'MMMCCC') <_sre.SRE_Match object at 010748A8>