Ištirkite Python reguliariųjų išraiškų variklio vidų. Šis vadovas išaiškina šablonų atitikimo algoritmus, tokius kaip NFA ir atgalinis sekimas, padėdamas rašyti efektyvias reguliariąsias išraiškas.
Variklio atskleidimas: gilus nardymas į Python reguliariųjų išraiškų šablonų atitikimo algoritmus
Reguliariosios išraiškos, arba regex, yra šiuolaikinio programinės įrangos kūrimo kertinis akmuo. Nesuskaičiuojamai daugybei programuotojų visame pasaulyje jos yra pagrindinis įrankis teksto apdorojimui, duomenų patvirtinimui ir žurnalų analizei. Mes jas naudojame norėdami rasti, pakeisti ir išgauti informaciją tokiu tikslumu, kurio paprasti eilutės metodai negali pasiekti. Vis dėlto daugeliui regex variklis išlieka juoda dėžė – stebuklingas įrankis, kuris priima paslaptingą šabloną ir eilutę ir kažkaip pateikia rezultatą. Šio supratimo trūkumas gali lemti neefektyvų kodą ir, kai kuriais atvejais, katastrofiškas našumo problemas.
Šis straipsnis atskleidžia Python re modulio paslaptis. Mes keliausime į jo šablonų atitikimo variklio šerdį, tyrinėdami pagrindinius algoritmus, kurie jį maitina. Suprasdami, kaip veikia variklis, galėsite rašyti efektyvesnes, patikimesnes ir nuspėjamas reguliariąsias išraiškas, paversdami šio galingo įrankio naudojimą iš spėliojimų į mokslą.
Reguliariųjų išraiškų esmė: kas yra Regex variklis?
Iš esmės, reguliariųjų išraiškų variklis yra programinės įrangos dalis, kuri priima du įvesties duomenis: šabloną (regex) ir įvesties eilutę. Jo užduotis yra nustatyti, ar šabloną galima rasti eilutėje. Jei taip, variklis praneša apie sėkmingą atitikimą ir dažnai pateikia informaciją, pvz., atitikto teksto pradžios ir pabaigos pozicijas bei visas užfiksuotas grupes.
Nors tikslas yra paprastas, įgyvendinimas nėra. Regex varikliai paprastai yra sukurti remiantis vienu iš dviejų pagrindinių algoritmų metodų, įsišaknijusių teorinėje kompiuterių moksle, konkrečiai baigtinių automatų teorijoje.
- Teksto krypties varikliai (DFA pagrindu): Šie varikliai, pagrįsti Determinuotais baigtiniais automatais (DFA), apdoroja įvesties eilutę po vieną simbolį. Jie yra neįtikėtinai greiti ir užtikrina nuspėjamą, linijinio laiko našumą. Jiems niekada nereikia atsekti ar iš naujo įvertinti eilutės dalių. Tačiau šis greitis pasiekiamas funkcijų sąskaita; DFA varikliai negali palaikyti pažangių konstrukcijų, tokių kaip atgalinės nuorodos ar tingūs kvantoriai. Tokie įrankiai kaip `grep` ir `lex` dažnai naudoja DFA pagrįstus variklius.
- Regex krypties varikliai (NFA pagrindu): Šie varikliai, pagrįsti Nedeterminuotais baigtiniais automatais (NFA), yra pagrįsti šablonais. Jie juda per šabloną, bandydami suderinti jo komponentus su eilute. Šis metodas yra lankstesnis ir galingesnis, palaikantis platų funkcijų rinkinį, įskaitant fiksavimo grupes, atgalines nuorodas ir apžvalgas. Dauguma šiuolaikinių programavimo kalbų, įskaitant Python, Perl, Java ir JavaScript, naudoja NFA pagrįstus variklius.
Python re modulis naudoja tradicinį NFA pagrįstą variklį, kuris remiasi esminiu mechanizmu, vadinamu atgaliniu sekimu. Šis dizaino pasirinkimas yra raktas į jo galią ir galimus našumo spąstus.
Pasakojimas apie du automatus: NFA prieš DFA
Norint tikrai suprasti, kaip veikia Python regex variklis, naudinga palyginti du dominuojančius modelius. Pagalvokite apie juos kaip apie dvi skirtingas strategijas, kaip naršyti labirintą (įvesties eilutę) naudojant žemėlapį (regex šabloną).
Determinuotas baigtinis automatas (DFA): Neabejotinas kelias
Įsivaizduokite mašiną, kuri skaito įvesties eilutę po vieną simbolį. Bet kuriuo duotu momentu jis yra tiksliai vienoje būsenoje. Kiekvienam simboliui, kurį jis skaito, yra tik viena galima kita būsena. Nėra jokio dviprasmiškumo, jokio pasirinkimo, jokio grįžimo atgal. Tai yra DFA.
- Kaip tai veikia: DFA pagrįstas variklis sukuria būsenų mašiną, kurioje kiekviena būsena atspindi galimų pozicijų rinkinį regex šablone. Jis apdoroja įvesties eilutę iš kairės į dešinę. Perskaitęs kiekvieną simbolį, jis atnaujina savo dabartinę būseną, remdamasis deterministine perėjimo lentele. Jei jis pasiekia eilutės pabaigą būdamas „priėmimo“ būsenoje, atitikimas yra sėkmingas.
- Privalumai:
- Greitis: DFA apdoroja eilutes linijiniu laiku, O(n), kur n yra eilutės ilgis. Šablono sudėtingumas neturi įtakos paieškos laikui.
- Nuspėjamumas: Našumas yra nuoseklus ir niekada nesumažėja iki eksponentinio laiko.
- Trūkumai:
- Ribotos funkcijos: Deterministinis DFA pobūdis neleidžia įdiegti funkcijų, kurios reikalauja įsiminti ankstesnį atitikimą, pvz., atgalines nuorodas (pvz.,
(\w+)\s+\1). Tingūs kvantoriai ir apžvalgos taip pat paprastai nepalaikomi. - Būsenų sprogimas: Sudėtingo šablono kompiliavimas į DFA kartais gali lemti eksponentiškai didelį būsenų skaičių, sunaudojant daug atminties.
- Ribotos funkcijos: Deterministinis DFA pobūdis neleidžia įdiegti funkcijų, kurios reikalauja įsiminti ankstesnį atitikimą, pvz., atgalines nuorodas (pvz.,
Nedeterminuotas baigtinis automatas (NFA): Galimybių kelias
Dabar įsivaizduokite kitokią mašiną. Kai ji skaito simbolį, ji gali turėti kelias galimas kitas būsenas. Atrodo, kad mašina gali klonuoti save, kad vienu metu ištirtų visus kelius. NFA variklis imituoja šį procesą, paprastai bandydamas vieną kelią vienu metu ir atsekiant, jei jis nepavyksta. Tai yra NFA.
- Kaip tai veikia: NFA variklis eina per regex šabloną ir kiekvienam šablono ženklui bando jį atitikti su dabartine eilutės pozicija. Jei ženklas leidžia kelias galimybes (pvz., alternatyvą `|` arba kvantorių `*`), variklis pasirenka ir išsaugo kitas galimybes vėlesniam laikui. Jei pasirinktas kelias nesukuria visiško atitikimo, variklis atseka atgal į paskutinį pasirinkimo tašką ir bando kitą alternatyvą.
- Privalumai:
- Galingos funkcijos: Šis modelis palaiko turtingą funkcijų rinkinį, įskaitant fiksavimo grupes, atgalines nuorodas, apžvalgas, apžvalgas ir godžius bei tingius kvantorius.
- Išraiškingumas: NFA varikliai gali apdoroti platesnę sudėtingų šablonų įvairovę.
- Trūkumai:
- Našumo kintamumas: Geriausiu atveju NFA varikliai yra greiti. Blogiausiu atveju atgalinio sekimo mechanizmas gali lemti eksponentinio laiko sudėtingumą, O(2^n), reiškinį, žinomą kaip „katastrofiškas atgalinis sekimas“.
Python re modulio širdis: Atgalinio sekimo NFA variklis
Python regex variklis yra klasikinis atgalinio sekimo NFA pavyzdys. Suprasti šį mechanizmą yra svarbiausia sąvoka norint rašyti efektyvias reguliariąsias išraiškas Python. Naudokime analogiją: įsivaizduokite, kad esate labirinte ir turite nurodymų rinkinį (šabloną). Jūs einate vienu keliu. Jei atsidursite aklavietėje, grįšite atgal į paskutinę sankryžą, kurioje turėjote pasirinkimą, ir bandysite kitą kelią. Šis „atsekti ir bandyti iš naujo“ procesas yra atgalinis sekimas.
Žingsnis po žingsnio atgalinio sekimo pavyzdys
Pažiūrėkime, kaip variklis apdoroja atrodytų paprastą šabloną. Šis pavyzdys parodo pagrindinę godaus atitikimo ir atgalinio sekimo sąvoką.
- Šablonas:
a.*b - Eilutė:
axbyc_bzd
Tikslas yra rasti poeilutę, kuri prasideda „a“, baigiasi „b“ ir turi bet ką tarp jų.
- Variklis prasideda nuo pirmojo eilutės simbolio. Pirmoji šablono dalis yra
a. Jis atitinka „a“ eilutės pradžioje. Variklio padėtis dabar yra po „a“. - Kitas yra
.*. Kvantorius*pagal numatytuosius nustatymus yra godus. Tai yra esminis dalykas. Godus kvantorius atitiks kuo daugiau simbolių. Taigi,.*sunaudoja visą likusią eilutės dalį: `xbyc_bzd`. - Variklis dabar pasiekė šablono dalies
.*pabaigą ir bando atitikti kitą ženklą:b. Tačiau jis jau yra eilutės pabaigoje. Nebeliko simbolių, kuriuos būtų galima atitikti. Atitikimas nepavyksta šioje pozicijoje. - ATGALINIS SEKIMAS! Variklis turi iš naujo apsvarstyti savo paskutinį pasirinkimą. Paskutinis svarbus pasirinkimas buvo, kiek simbolių turėtų sunaudoti godus
.*. Jis buvo per daug godus. Taigi, jis atseka:.*grąžina vieną simbolį.- Eilutė, kurią sunaudoja
.*, dabar yra `xbyc_bz`. - Simbolis, kuris grąžinamas, yra `d`.
- Variklis bando atitikti
bsu `d`. Tai nepavyksta.
- Eilutė, kurią sunaudoja
- ATGALINIS SEKIMAS VĖL!
.*grąžina kitą simbolį.- Eilutė, kurią sunaudoja
.*, dabar yra `xbyc_b`. - Simbolis, kuris grąžinamas, yra `z`.
- Variklis bando atitikti
bsu `z`. Tai nepavyksta.
- Eilutė, kurią sunaudoja
- ATGALINIS SEKIMAS VĖL!
.*grąžina kitą simbolį.- Eilutė, kurią sunaudoja
.*, dabar yra `xbyc_`. - Simbolis, kuris grąžinamas, yra `b`.
- Variklis bando atitikti
bsu `b`. Sėkmė!
- Eilutė, kurią sunaudoja
- Visas šablonas
a.*bdabar buvo atitiktas. Galutinis atitikimas yraaxbyc_b.
Šis paprastas pavyzdys parodo variklio bandymų ir klaidų pobūdį. Esant sudėtingiems šablonams ir ilgoms eilutėms, šis sunaudojimo ir grąžinimo procesas gali įvykti tūkstančius ar net milijonus kartų, sukeldamas rimtų našumo problemų.
Atgalinio sekimo pavojus: Katastrofiškas atgalinis sekimas
Katastrofiškas atgalinis sekimas yra specifinis, blogiausio atvejo scenarijus, kai variklio bandymų permutacijų skaičius auga eksponentiškai. Tai gali sukelti programos pakibimą, sunaudojant 100% CPU branduolio sekundes, minutes ar net ilgiau, efektyviai sukuriant reguliariųjų išraiškų paslaugos trikdymo (ReDoS) pažeidžiamumą.
Ši situacija paprastai kyla dėl šablono, kuris turi įdėtus kvantorius su persidengiančiu simbolių rinkiniu, pritaikytu eilutei, kuri beveik, bet ne visai, atitinka.
Apsvarstykite klasikinį patologinį pavyzdį:
- Šablonas:
(a+)+z - Eilutė:
aaaaaaaaaaaaaaaaaaaaaaaaaz(25 „a“ ir vienas „z“)
Tai atitiks labai greitai. Išorinis `(a+)+` atitiks visus „a“ vienu ypu, o tada `z` atitiks „z“.
Bet dabar apsvarstykite šią eilutę:
- Eilutė:
aaaaaaaaaaaaaaaaaaaaaaaaab(25 „a“ ir vienas „b“)
Štai kodėl tai yra katastrofiška:
- Vidinis
a+gali atitikti vieną ar daugiau „a“. - Išorinis
+kvantorius sako, kad grupė(a+)gali būti kartojama vieną ar daugiau kartų. - Norint atitikti 25 „a“ eilutę, variklis turi daug, daug būdų ją padalyti. Pavyzdžiui:
- Išorinė grupė atitinka vieną kartą, o vidinis
a+atitinka visus 25 „a“. - Išorinė grupė atitinka du kartus, o vidinis
a+atitinka 1 „a“, tada 24 „a“. - Arba 2 „a“, tada 23 „a“.
- Arba išorinė grupė atitinka 25 kartus, o vidinis
a+kiekvieną kartą atitinka vieną „a“.
- Išorinė grupė atitinka vieną kartą, o vidinis
Variklis pirmiausia bandys godžiausią atitikimą: išorinė grupė atitinka vieną kartą, o vidinis `a+` sunaudoja visus 25 „a“. Tada jis bando atitikti `z` su `b`. Tai nepavyksta. Taigi, jis atseka. Jis bando kitą galimą „a“ padalijimą. Ir kitą. Ir kitą. Būdų padalinti „a“ eilutę skaičius yra eksponentinis. Variklis yra priverstas išbandyti kiekvieną iš jų, prieš padarydamas išvadą, kad eilutė neatitinka. Turint tik 25 „a“, tai gali užtrukti milijonus žingsnių.
Kaip nustatyti ir išvengti katastrofiško atgalinio sekimo
Raktas į efektyvų regex rašymą yra nukreipti variklį ir sumažinti atgalinio sekimo žingsnių skaičių, kurį jis turi atlikti.
1. Venkite įdėtų kvantorių su persidengiančiais šablonais
Pagrindinė katastrofiško atgalinio sekimo priežastis yra šablonas, pvz., (a*)*, (a+|b+)* arba (a+)+. Atidžiai patikrinkite savo šablonus, ar nėra šios struktūros. Dažnai ją galima supaprastinti. Pavyzdžiui, (a+)+ yra funkciškai identiškas daug saugesniam a+. Šablonas (a|b)+ yra daug saugesnis nei (a+|b+)*.
2. Padarykite godžius kvantorius tingiais (ne godžiais)
Pagal numatytuosius nustatymus kvantoriai (`*`, `+`, `{m,n}`) yra godūs. Galite padaryti juos tingiais pridėdami `?`. Tingus kvantorius atitinka kuo mažiau simbolių, tik išplėsdamas savo atitikimą, jei tai būtina, kad likusi šablono dalis pavyktų.
- Godus:
<h1>.*</h1>eilutėje"<h1>Title 1</h1> <h1>Title 2</h1>"atitiks visą eilutę nuo pirmojo<h1>iki paskutinio</h1>. - Tingus:
<h1>.*?</h1>toje pačioje eilutėje pirmiausia atitiks"<h1>Title 1</h1>". Tai dažnai yra norimas elgesys ir gali žymiai sumažinti atgalinį sekimą.
3. Naudokite posesyvius kvantorius ir atomines grupes (kai įmanoma)
Kai kurie pažangūs regex varikliai siūlo funkcijas, kurios aiškiai draudžia atgalinį sekimą. Nors Python standartinis `re` modulis jų nepalaiko, puikus trečiosios šalies `regex` modulis tai daro, ir tai yra vertingas įrankis sudėtingam šablonų atitikimui.
- Posesyvūs kvantoriai (`*+`, `++`, `?+`): Jie yra tarsi godūs kvantoriai, bet kai jie atitinka, jie niekada negrąžina jokių simbolių. Varikliui neleidžiama atsekti į juos. Šablonas
(a++)+zbeveik akimirksniu nepavyktų mūsų probleminėje eilutėje, nes `a++` sunaudotų visus „a“ ir tada atsisakytų atsekti, todėl visas atitikimas iškart nepavyktų. - Atominės grupės `(?>...)`: Atominė grupė yra nefiksuojanti grupė, kuri, išėjus iš jos, atmeta visas atgalinio sekimo pozicijas joje. Variklis negali atsekti į grupę, kad išbandytų skirtingas permutacijas. `(?>a+)z` elgiasi panašiai kaip `a++z`.
Jei susiduriate su sudėtingais regex iššūkiais Python, labai rekomenduojama įdiegti ir naudoti `regex` modulį vietoj `re`.
Žvilgsnis į vidų: Kaip Python kompiliuoja Regex šablonus
Kai naudojate reguliariąją išraišką Python, variklis tiesiogiai neveikia su neapdorota šablono eilute. Pirmiausia jis atlieka kompiliavimo žingsnį, kuris transformuoja šabloną į efektyvesnį, žemesnio lygio vaizdą – baitų kodo tipo instrukcijų seką.
Šį procesą tvarko vidinis `sre_compile` modulis. Žingsniai yra maždaug tokie:
- Parsiavimas: Eilutės šablonas yra išanalizuojamas į medžio tipo duomenų struktūrą, kuri atspindi jo loginius komponentus (literales, kvantorius, grupes ir t. t.).
- Kompiliavimas: Šis medis tada apeinamas ir generuojama linijinė operacijų kodų seka. Kiekvienas operacijos kodas yra paprasta instrukcija atitikimo varikliui, pvz., „atitikti šį literalų simbolį“, „pereiti į šią poziciją“ arba „pradėti fiksavimo grupę“.
- Vykdymas: Tada `sre` variklio virtuali mašina vykdo šiuos operacijų kodus prieš įvesties eilutę.
Galite gauti šio sukompiliuoto vaizdo vaizdą naudodami `re.DEBUG` vėliavą. Tai yra galingas būdas suprasti, kaip variklis interpretuoja jūsų šabloną.
import re
# Išanalizuokime šabloną 'a(b|c)+d'
re.compile('a(b|c)+d', re.DEBUG)
Išvestis atrodys maždaug taip (komentarai pridėti aiškumo dėlei):
LITERAL 97 # Atitikti simbolį 'a'
MAX_REPEAT 1 65535 # Pradėti kvantorių: atitikti šią grupę 1–daugelį kartų
SUBPATTERN 1 0 0 # Pradėti fiksavimo grupę 1
BRANCH # Pradėti alternatyvą (simbolis '|')
LITERAL 98 # Pirmojoje šakoje atitikti 'b'
OR
LITERAL 99 # Antrojoje šakoje atitikti 'c'
MARK 1 # Užbaigti fiksavimo grupę 1
LITERAL 100 # Atitikti simbolį 'd'
SUCCESS # Visas šablonas buvo sėkmingai atitiktas
Išnagrinėjus šią išvestį, parodoma tiksli žemo lygio logika, kurios laikysis variklis. Galite pamatyti `BRANCH` operacijos kodą alternatyvai ir `MAX_REPEAT` operacijos kodą `+` kvantoriui. Tai patvirtina, kad variklis mato pasirinkimus ir ciklus, kurie yra atgalinio sekimo ingredientai.
Praktinės našumo pasekmės ir geriausia praktika
Apsiginklavę šiuo supratimu apie variklio vidų, galime nustatyti geriausios praktikos rinkinį, skirtą rašyti didelio našumo reguliariąsias išraiškas, kurios yra veiksmingos bet kuriame pasauliniame programinės įrangos projekte.
Geriausia praktika rašant efektyvias reguliariąsias išraiškas
- 1. Iš anksto kompiliuokite savo šablonus: Jei savo kode kelis kartus naudojate tą patį regex, kompiliuokite jį vieną kartą naudodami
re.compile()ir pakartotinai naudokite gautą objektą. Taip išvengiama šablono eilutės parsiavimo ir kompiliavimo kiekvieną kartą naudojant. - 2. Būkite kuo konkretesni: Konkretesnis šablonas suteikia varikliui mažiau pasirinkimų ir sumažina poreikį atsekti atgal. Venkite pernelyg bendrinių šablonų, pvz., `.*`, kai tinka tikslesnis.
- Mažiau efektyvus: `key=.*`
- Efektyvesnis: `key=[^;]+` (atitikti bet ką, kas nėra kabliataškis)
- 3. Pritvirtinkite savo šablonus: Jei žinote, kad jūsų atitikimas turėtų būti eilutės pradžioje arba pabaigoje, naudokite inkarus `^` ir `$`. Tai leidžia varikliui labai greitai nepavykti eilutėms, kurios neatitinka reikiamos pozicijos.
- 4. Naudokite nefiksavimo grupes `(?:...)`: Jei jums reikia sugrupuoti šablono dalį kvantoriui, bet nereikia gauti atitikusio teksto iš tos grupės, naudokite nefiksavimo grupę. Tai yra šiek tiek efektyviau, nes varikliui nereikia skirti atminties ir saugoti užfiksuotos poeilutės.
- Fiksavimas: `(https?|ftp)://...`
- Nefiksavimas: `(?:https?|ftp)://...`
- 5. Teikite pirmenybę simbolių klasėms, o ne alternatyvai: Kai atitinka vieną iš kelių vienų simbolių, simbolių klasė `[...]` yra žymiai efektyvesnė nei alternatyva `(...)`. Simbolių klasė yra vienas operacijos kodas, o alternatyva apima šakojimąsi ir sudėtingesnę logiką.
- Mažiau efektyvus: `(a|b|c|d)`
- Efektyvesnis: `[abcd]`
- 6. Žinokite, kada naudoti kitą įrankį: Reguliariosios išraiškos yra galingos, bet jos nėra kiekvienos problemos sprendimas. Norėdami paprastai patikrinti poeilutes, naudokite `in` arba `str.startswith()`. Norėdami analizuoti struktūruotus formatus, pvz., HTML arba XML, naudokite specialią analizatoriaus biblioteką. Regex naudojimas šioms užduotims dažnai yra trapus ir neefektyvus.
# Gera praktika
COMPILED_REGEX = re.compile(r'\d{4}-\d{2}-\d{2}')
for line in data:
COMPILED_REGEX.search(line)
Išvada: Nuo juodos dėžės iki galingo įrankio
Python reguliariųjų išraiškų variklis yra puikiai suderintas programinės įrangos elementas, sukurtas remiantis dešimtmečiais kompiuterių mokslo teorijos. Pasirinkdamas atgalinio sekimo NFA pagrįstą metodą, Python suteikia kūrėjams turtingą ir išraiškingą šablonų atitikimo kalbą. Tačiau ši galia ateina su atsakomybe suprasti jo pagrindinę mechaniką.
Dabar esate aprūpinti žiniomis apie tai, kaip veikia variklis. Jūs suprantate bandymų ir klaidų atgalinio sekimo procesą, didžiulį jo katastrofiško blogiausio atvejo scenarijaus pavojų ir praktinius metodus, kaip nukreipti variklį link efektyvaus atitikimo. Dabar galite pažvelgti į šabloną, pvz., (a+)+, ir iškart atpažinti jo keliamą našumo riziką. Galite užtikrintai rinktis tarp godaus .* ir tingaus .*?, tiksliai žinodami, kaip kiekvienas iš jų elgsis.
Kitą kartą, kai rašysite reguliariąją išraišką, pagalvokite ne tik apie tai, ką norite atitikti. Pagalvokite apie tai, kaip variklis ten pateks. Pereidami už juodos dėžės ribų, atrakinate visą reguliariųjų išraiškų potencialą, paversdami jas nuspėjamu, efektyviu ir patikimu įrankiu savo kūrėjo įrankių rinkinyje.