Avage kiirem ja tõhusam kood. Õppige olulisi regulaaravaldiste optimeerimise tehnikaid, alates tagasivõtust ja ahnest vs. laisast sobitamisest kuni täiustatud mootorispetsiifilise häälestamiseni.
Regulaaravaldiste optimeerimine: sügav sukeldumine Regexi jõudluse häälestamisse
Regulaaravaldised ehk regex on tänapäeva programmeerija tööriistakastis asendamatu vahend. Alates kasutaja sisendi valideerimisest ja logifailide parsimisest kuni keerukate otsingu- ja asendustoimingute ning andmete eraldamiseni on nende võimsus ja mitmekülgsus vaieldamatud. Selle võimsusega kaasneb aga varjatud kulu. Halvasti kirjutatud regulaaravaldis võib muutuda vaikseks jõudluse tapjaks, tekitades märkimisväärset latentsust, põhjustades protsessori koormuse hüppeid ja halvimal juhul peatades teie rakenduse täielikult. Siin muutub regulaaravaldiste optimeerimine mitte lihtsalt 'heaks oskuseks', vaid kriitiliseks oskuseks robustse ja skaleeritava tarkvara ehitamisel.
See põhjalik juhend viib teid sügavale regexi jõudluse maailma. Uurime, miks pealtnäha lihtne muster võib olla katastroofiliselt aeglane, mõistame regexi mootorite sisemist toimimist ja varustame teid võimsa põhimõtete ja tehnikate kogumiga, et kirjutada regulaaravaldisi, mis pole mitte ainult korrektsed, vaid ka välkkiired.
Mõistmine 'miks': halva regexi hind
Enne kui sukeldume optimeerimistehnikatesse, on ülioluline mõista probleemi, mida püüame lahendada. Kõige tõsisem regulaaravaldistega seotud jõudlusprobleem on tuntud kui katastroofiline tagasivõtt (ingl. Catastrophic Backtracking), seisund, mis võib viia regulaaravaldise teenusetõkestamise (ReDoS) haavatavuseni.
Mis on katastroofiline tagasivõtt?
Katastroofiline tagasivõtt toimub siis, kui regexi mootoril kulub vaste leidmiseks (või selle puudumise tuvastamiseks) erakordselt kaua aega. See juhtub teatud tüüpi mustritega teatud tüüpi sisendstringide puhul. Mootor jääb kinni peadpööritavasse permutatsioonide labürinti, proovides kõiki võimalikke teid mustri rahuldamiseks. Sammude arv võib sisendstringi pikkusega eksponentsiaalselt kasvada, mis viib näiliselt rakenduse külmumiseni.
Vaatleme klassikalist näidet haavatavast regulaaravaldisest: ^(a+)+$
See muster tundub piisavalt lihtne: see otsib stringi, mis koosneb ühest või mitmest 'a'-st. See töötab ideaalselt stringide puhul nagu "a", "aa" ja "aaaaa". Probleem tekib siis, kui testime seda stringiga, mis peaaegu sobib, kuid lõpuks ebaõnnestub, näiteks "aaaaaaaaaaaaaaaaaaaaaaaaaaab".
Põhjus, miks see nii aeglane on:
- Välimine
(...)+ja siseminea+on mõlemad ahned kvantorid. - Sisemine
a+sobitab esmalt kõik 27 'a'-d. - Välimine
(...)+on selle ühe vastega rahul. - Seejärel proovib mootor sobitada stringi lõpu ankrut
$. See ebaõnnestub, sest seal on 'b'. - Nüüd peab mootor tagasi võtma. Välimine grupp loobub ühest märgist, nii et sisemine
a+sobitab nüüd 26 'a'-d ja välimise grupi teine iteratsioon proovib sobitada viimast 'a'-d. Ka see ebaõnnestub 'b' juures. - Mootor proovib nüüd kõiki võimalikke viise 'a'-de stringi jaotamiseks sisemise
a+ja välimise(...)+vahel. N 'a'-st koosneva stringi jaoks on 2N-1 jaotamisviisi. Keerukus on eksponentsiaalne ja töötlemisaeg kasvab hüppeliselt.
See üksainus, pealtnäha kahjutu regex võib lukustada protsessori tuuma sekunditeks, minutiteks või isegi kauemaks, keelates tõhusalt teenuse teistele protsessidele või kasutajatele.
Asja tuum: regexi mootor
Regexi optimeerimiseks peate mõistma, kuidas mootor teie mustrit töötleb. On olemas kaks peamist tüüpi regexi mootoreid ja nende sisemine toimimine määrab jõudlusomadused.
DFA (determineeritud lõplik automaat) mootorid
DFA-mootorid on regexi maailma kiirusekuningad. Nad töötlevad sisendstringi ühe läbimisega vasakult paremale, märk-märgilt. Igal ajahetkel teab DFA-mootor täpselt, milline on järgmine olek, tuginedes praegusele märgile. See tähendab, et see ei pea kunagi tagasi võtma. Töötlemisaeg on lineaarne ja otseselt proportsionaalne sisendstringi pikkusega. Näited tööriistadest, mis kasutavad DFA-põhiseid mootoreid, hõlmavad traditsioonilisi Unixi tööriistu nagu grep ja awk.
Plussid: Äärmiselt kiire ja prognoositav jõudlus. Immuunne katastroofilise tagasivõtu suhtes.
Miinused: Piiratud funktsioonide komplekt. Nad ei toeta täiustatud funktsioone nagu tagasiviited, ette- ja tahavaated ega hõivamisgrupid, mis sõltuvad tagasivõtu võimest.
NFA (määramata lõplik automaat) mootorid
NFA-mootorid on kõige levinum tüüp, mida kasutatakse kaasaegsetes programmeerimiskeeltes nagu Python, JavaScript, Java, C# (.NET), Ruby, PHP ja Perl. Need on "mustripõhised", mis tähendab, et mootor järgib mustrit, liikudes stringis edasi. Kui see jõuab mitmetähenduslikkuse punktini (nagu alternatiiv | või kvantor *, +), proovib see ühte teed. Kui see tee lõpuks ebaõnnestub, võtab see tagasi viimase otsustuspunktini ja proovib järgmist saadaolevat teed.
See tagasivõtuvõime teeb NFA-mootorid nii võimsaks ja funktsioonirikkaks, võimaldades keerukaid mustreid ette- ja tahavaadete ning tagasiviidetega. Kuid see on ka nende Achilleuse kand, kuna see on mehhanism, mis võimaldab katastroofilist tagasivõttu.
Selle juhendi ülejäänud osas keskenduvad meie optimeerimistehnikad NFA-mootori taltsutamisele, kuna just siin puutuvad arendajad kõige sagedamini kokku jõudlusprobleemidega.
NFA-mootorite peamised optimeerimispõhimõtted
Nüüd sukeldume praktilistesse ja rakendatavatesse tehnikatesse, mida saate kasutada suure jõudlusega regulaaravaldiste kirjutamiseks.
1. Olge spetsiifiline: täpsuse jõud
Kõige levinum jõudluse vastane muster on liiga üldiste metamärkide nagu .* kasutamine. Punkt . sobib (peaaegu) iga märgiga ja tärn * tähendab "null või rohkem korda". Kombineerituna käsivad nad mootoril ahnelt haarata kogu ülejäänud stringi ja seejärel tagasi võtta märk-märgilt, et näha, kas ülejäänud muster sobib. See on uskumatult ebaefektiivne.
Halb näide (HTML-i pealkirja parsimine):
<title>.*</title>
Suure HTML-dokumendi puhul sobitab .* esmalt kõik kuni faili lõpuni. Seejärel võtab see tagasi, märk-märgilt, kuni leiab viimase </title>. See on palju ebavajalikku tööd.
Hea näide (kasutades eitatud märgiklassi):
<title>[^<]*</title>
See versioon on palju tõhusam. Eitatud märgiklass [^<]* tähendab "sobita iga märk, mis ei ole '<' null või rohkem korda". Mootor liigub edasi, haarates märke, kuni jõuab esimese '<'-ni. See ei pea kunagi tagasi võtma. See on otsene, ühemõtteline juhis, mis toob kaasa tohutu jõudluse kasvu.
2. Valitse ahnust vs. laiskust: küsimärgi jõud
Regexi kvantorid on vaikimisi ahned. See tähendab, et nad sobitavad nii palju teksti kui võimalik, lubades samal ajal üldisel mustril siiski sobituda.
- Ahne:
*,+,?,{n,m}
Saate muuta mis tahes kvantori laisaks, lisades selle järele küsimärgi. Laisk kvantor sobitab nii vähe teksti kui võimalik.
- Laisk:
*?,+?,??,{n,m}?
Näide: paksus kirjas siltide sobitamine
Sisendstring: <b>Esimene</b> ja <b>Teine</b>
- Ahne muster:
<b>.*</b>
See sobitab:<b>Esimene</b> ja <b>Teine</b>..*haaras ahnelt kõik kuni viimase</b>-ni. - Laisk muster:
<b>.*?</b>
See sobitab esimesel katsel<b>Esimene</b> ja uuesti otsides<b>Teine</b>..*?sobitas minimaalse arvu märke, mis on vajalikud ülejäänud mustri (</b>) sobitumiseks.
Kuigi laiskus võib lahendada teatud sobitamisprobleeme, ei ole see jõudluse imerohi. Iga laisa sobitamise samm nõuab, et mootor kontrolliks, kas mustri järgmine osa sobib. Väga spetsiifiline muster (nagu eelmise punkti eitatud märgiklass) on sageli kiirem kui laisk.
Jõudluse järjekord (kiireimast aeglasemani):
- Spetsiifiline/eitatud märgiklass:
<b>[^<]*</b> - Laisk kvantor:
<b>.*?</b> - Ahne kvantor suure tagasivõtuga:
<b>.*</b>
3. Vältige katastroofilist tagasivõttu: pesastatud kvantorite taltsutamine
Nagu esialgsest näitest nägime, on katastroofilise tagasivõtu otsene põhjus muster, kus kvantifitseeritud grupp sisaldab teist kvantorit, mis suudab sobitada sama teksti. Mootor seisab silmitsi mitmetähendusliku olukorraga, kus on mitu võimalust sisendstringi jaotamiseks.
Probleemsed mustrid:
(a+)+(a*)*(a|aa)+(a|b)*, kus sisendstring sisaldab palju 'a'-sid ja 'b'-sid.
Lahendus on muuta muster ühemõtteliseks. Peate tagama, et mootoril oleks antud stringi sobitamiseks ainult üks viis.
4. Kasutage atomaarseid gruppe ja possessiivseid kvantoreid
See on üks võimsamaid tehnikaid tagasivõtu eemaldamiseks oma avaldistest. Atomaarsed grupid ja possessiivsed kvantorid ütlevad mootorile: "Kui olete selle mustri osa sobitanud, ärge kunagi andke ühtegi märki tagasi. Ärge võtke sellesse avaldisse tagasi."
Possessiivsed kvantorid
Possessiivne kvantor luuakse, lisades tavalise kvantori järele + (nt *+, ++, ?+, {n,m}+). Neid toetavad mootorid nagu Java, PCRE (PHP, R) ja Ruby.
Näide: numbri sobitamine, millele järgneb 'a'
Sisendstring: 12345
- Tavaline Regex:
\d+a\d+sobitab "12345". Seejärel proovib mootor sobitada 'a' ja ebaõnnestub. See võtab tagasi, nii et\d+sobitab nüüd "1234" ja proovib sobitada 'a' märgiga '5'. See jätkub, kuni\d+on kõik oma märgid tagasi andnud. Ebaõnnestumiseks tehakse palju tööd. - Possessiivne Regex:
\d++a\d++sobitab possessiivselt "12345". Seejärel proovib mootor sobitada 'a' ja ebaõnnestub. Kuna kvantor oli possessiivne, on mootoril keelatud\d++osasse tagasi võtta. See ebaõnnestub kohe. Seda nimetatakse 'kiireks ebaõnnestumiseks' ja see on äärmiselt tõhus.
Atomaarsed grupid
Atomaarsetel gruppidel on süntaks (?>...) ja neid toetatakse laiemalt kui possessiivseid kvantoreid (nt .NET-is, Pythoni uuemas `regex` moodulis). Nad käituvad täpselt nagu possessiivsed kvantorid, kuid kehtivad tervele grupile.
Regex (?>\d+)a on funktsionaalselt samaväärne \d++a-ga. Saate kasutada atomaarseid gruppe algse katastroofilise tagasivõtu probleemi lahendamiseks:
Algne probleem: (a+)+
Atomaarne lahendus: ((?>a+))+
Nüüd, kui sisemine grupp (?>a+) sobitab 'a'-de jada, ei anna see neid kunagi tagasi, et välimine grupp saaks uuesti proovida. See eemaldab mitmetähenduslikkuse ja hoiab ära eksponentsiaalse tagasivõtu.
5. Alternatiivide järjekord on oluline
Kui NFA-mootor kohtab alternatiivi (kasutades | sümbolit), proovib see alternatiive vasakult paremale. See tähendab, et peaksite kõige tõenäolisema alternatiivi esimesena paigutama.
Näide: käsu parsimine
Kujutage ette, et parsitate käske ja teate, et `GET` käsk ilmub 80% ajast, `SET` 15% ajast ja `DELETE` 5% ajast.
Vähem tõhus: ^(DELETE|SET|GET)
80% teie sisenditest proovib mootor esmalt sobitada `DELETE`, ebaõnnestub, võtab tagasi, proovib sobitada `SET`, ebaõnnestub, võtab tagasi ja lõpuks õnnestub `GET`-iga.
Tõhusam: ^(GET|SET|DELETE)
Nüüd saab mootor 80% ajast vaste kohe esimesel katsel. Sellel väikesel muudatusel võib olla märgatav mõju miljonite ridade töötlemisel.
6. Kasutage mittehõivavaid gruppe, kui te ei vaja hõivamist
Sulgudes (...) olev regex teeb kahte asja: grupeerib alamustri ja hõivab teksti, mis sellele alamustrile vastas. See hõivatud tekst salvestatakse mällu hilisemaks kasutamiseks (nt tagasiviidetes nagu `\1` või kutsuva koodi poolt eraldamiseks). Sellel salvestamisel on väike, kuid mõõdetav lisakulu.
Kui vajate ainult grupeerimiskäitumist, kuid ei pea teksti hõivama, kasutage mittehõivavat gruppi: (?:...).
Hõivav: (https?|ftp)://([^/]+)
See hõivab "http" ja domeeninime eraldi.
Mittehõivav: (?:https?|ftp)://([^/]+)
Siin grupeerime endiselt `https?|ftp`, et `://` rakenduks õigesti, kuid me ei salvesta sobitatud protokolli. See on veidi tõhusam, kui hoolite ainult domeeninime (mis on grupis 1) eraldamisest.
Täiustatud tehnikad ja mootorispetsiifilised näpunäited
Ette- ja tahavaated: võimsad, kuid kasutage ettevaatlikult
Ette- ja tahavaated (ettevaade (?=...), (?!...) ja tahavaade (?<=...), (?) on null-laiusega väited. Nad kontrollivad tingimust ilma tegelikult ühtegi märki tarbimata. See võib olla väga tõhus konteksti valideerimiseks.
Näide: parooli valideerimine
Regex parooli valideerimiseks, mis peab sisaldama numbrit:
^(?=.*\d).{8,}$
See on väga tõhus. Ettevaade (?=.*\d) skannib edasi, et tagada numbri olemasolu, ja seejärel lähtestatakse kursor algusesse. Mustri põhiosa, .{8,}, peab seejärel lihtsalt sobitama 8 või enam märki. See on sageli parem kui keerulisem, üherajaline muster.
Eelkompileerimine ja kompileerimine
Enamik programmeerimiskeeli pakub võimalust regulaaravaldis "kompileerida". See tähendab, et mootor parsib mustri stringi üks kord ja loob optimeeritud sisemise esituse. Kui kasutate sama regexi mitu korda (nt tsüklis), peaksite selle alati üks kord väljaspool tsüklit kompileerima.
Pythoni näide:
import re
# Kompileeri regex ĂĽks kord
log_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
for line in log_file:
# Kasuta kompileeritud objekti
match = log_pattern.search(line)
if match:
print(match.group(1))
Selle tegemata jätmine sunnib mootorit stringimustrit iga iteratsiooni korral uuesti parsima, mis on märkimisväärne protsessori tsüklite raiskamine.
Praktilised tööriistad regexi profileerimiseks ja silumiseks
Teooria on tore, aga oma silm on kuningas. Kaasaegsed veebipõhised regexi testijad on hindamatud tööriistad jõudluse mõistmiseks.
Veebisaidid nagu regex101.com pakuvad "Regex Debugger" või "samm-sammulise selgituse" funktsiooni. Saate kleepida oma regexi ja teststringi ning see annab teile samm-sammulise ülevaate sellest, kuidas NFA-mootor stringi töötleb. See näitab selgelt iga vastekatset, ebaõnnestumist ja tagasivõttu. See on parim viis visualiseerida, miks teie regex on aeglane, ja testida meie arutatud optimeerimiste mõju.
Praktiline kontrollnimekiri regexi optimeerimiseks
Enne keeruka regexi kasutuselevõttu käige see läbi sellest vaimsest kontrollnimekirjast:
- Spetsiifilisus: Kas olen kasutanud laiska
.*?või ahnet.*, kus spetsiifilisem eitatud märgiklass nagu[^"\r\n]*oleks kiirem ja turvalisem? - Tagasivõtt: Kas mul on pesastatud kvantoreid nagu
(a+)+? Kas on mitmetähenduslikkust, mis võiks teatud sisendite puhul põhjustada katastroofilist tagasivõttu? - Possessiivsus: Kas ma saan kasutada atomaarset gruppi
(?>...)või possessiivset kvantorit*+, et vältida tagasivõttu alamustrisse, mida ma tean, et ei tohiks uuesti hinnata? - Alternatiivid: Kas minu
(a|b|c)alternatiivides on kõige levinum alternatiiv esimesena loetletud? - Hõivamine: Kas ma vajan kõiki oma hõivamisgruppe? Kas mõned saab muuta mittehõivavateks gruppideks
(?:...), et vähendada lisakulusid? - Kompileerimine: Kui ma kasutan seda regexi tsüklis, kas ma kompileerin selle eelnevalt?
Juhtumiuuring: logiparseri optimeerimine
Paneme kõik kokku. Kujutage ette, et parsistame standardset veebiserveri logirida.
Logirida: 127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
Enne (aeglane regex):
^(\S+) (\S+) (\S+) \[(.*)\] "(.*)" (\d+) (\d+)$
See muster on funktsionaalne, kuid ebaefektiivne. (.*) kuupäeva ja päringu stringi jaoks võtab märkimisväärselt tagasi, eriti kui on valesti vormindatud logiridu.
Pärast (optimeeritud regex):
^(\S+) (\S+) (\S+) \[[^\]]+\] "(?:GET|POST|HEAD) ([^ "]+) HTTP/[\d.]+" (\d{3}) (\d+)$
Täiustuste selgitus:
\[(.*)\]muutus\[[^\]]+\]. Asendasime üldise, tagasivõtva.*väga spetsiifilise eitatud märgiklassiga, mis sobitab kõike peale sulgeva klambri. Tagasivõttu pole vaja."(.*)"muutus"(?:GET|POST|HEAD) ([^ "]+) HTTP/[\d.]+". See on tohutu täiustus.- Oleme oodatavate HTTP-meetodite osas selgesõnalised, kasutades mittehõivavat gruppi.
- Sobitame URL-i tee
[^ "]+-ga (üks või mitu märki, mis ei ole tühik ega jutumärk) üldise metamärgi asemel. - Määrame HTTP-protokolli vormingu.
- Staatusekoodi
(\d+)pingutati(\d{3})-ks, kuna HTTP-staatusekoodid on alati kolmekohalised.
'Pärast' versioon ei ole mitte ainult dramaatiliselt kiirem ja turvalisem ReDoS-rünnakute eest, vaid on ka robustsem, kuna see valideerib logirea vormingut rangemalt.
Kokkuvõte
Regulaaravaldised on kahe teraga mõõk. Hoolikalt ja teadmistega käsitsedes on need elegantne lahendus keerukatele tekstitöötlusprobleemidele. Hooletult kasutatuna võivad neist saada jõudluse õudusunenägu. Peamine järeldus on olla teadlik NFA-mootori tagasivõtumehhanismist ja kirjutada mustreid, mis suunavad mootori võimalikult sageli ühele, ühemõttelisele teele.
Olles spetsiifiline, mõistes ahnuse ja laiskuse kompromisse, kõrvaldades mitmetähenduslikkuse atomaarsete gruppidega ja kasutades õigeid tööriistu oma mustrite testimiseks, saate muuta oma regulaaravaldised potentsiaalsest kohustusest võimsaks ja tõhusaks varaks oma koodis. Alustage oma regexi profileerimist juba täna ja avage kiirem ja usaldusväärsem rakendus.