Regex


fredag 23. desember 2011 DSL Julekalender Regex

Dagens blogpost handler ikke om et programmeringsspråk som kan brukes til generelle formål. Regulære uttrykk – også kalt regex eller regexp – er et domenespesifikt språk man kan bruke til å "matche" tekst. Og alle utviklere, uansett hvilket språk de ellers bruker, kan dra nytte av dette. Desverre behersker alt for få utviklere den enorme styrken som bor i disse uttrykkene – om dette gjelder deg har du herved mottatt min oppfordring om å gjøre noe med det!

regex

Steve Yegge, en erfaren utvikler og bråkebøtte med bakgrunn fra Amazon og Google, har følgende å si om regulære uttrykk:

"If you aren't extremely proficient with regular expressions right now, then you should drop everything and go become proficient with them. I bet I use regular expressions on 350 days out of every year: in my editor, on the command-line, in my code — anywhere that using one would save me time or make my code clearer. Oh, how it hurts to think about all the so-called "programmers" out there who don't know how to use regexps. Argh." (kilde)

Kanskje du har hørt et uttrykk som sier noe sånt som: "Du har ett problem, og bestemmer deg for å løse det med regex. Nå har du to problemer." Bakgrunnen for dette er at regex-syntaksen er svært tett og komprimert, og lett å gjøre feil om man ikke kan det godt nok eller ikke sikrer seg godt ved å teste. Men dette er ikke grunner til å la være å bruke dem. Kraften er verdt anstrengelsen!

Hva kan jeg gjøre med regex?

Et veldig vanlig bruksområde for regex er input-validering. Du kan f.eks. validere at noe er en gyldig epost-adresse, url, telefonnummer, postkode osv. Man bruker det også til søking i større mengder tekst, og ulike parsing-oppgaver, f.eks. om man ønsker å fargelegge tekst etter forskjellige regler.

De fleste programmeringsspråk har regex-støtte, men vær oppmerksom på at metodenavn og måte de brukes på vil være forskjellig fra språk til språk. Selve regex-syntaksen kan også variere noe. Denne gangen velger jeg å vise eksempler i Ruby.

Validering

Se for deg at jeg har en rekke norske telefonnumre som jeg skal validere. De skal i utgangspunktet bestå av 8 siffer, men kan også ha en landkode, og den kan være formatert på tre ulike måter.

I eksempelet nedefor looper jeg over et array med numre, validerer det med et regex, og skriver ut om det er gyldig eller ikke.

 10 inputs = [
 11   '90909090',     # Valid: normal number
 12   '4790909090',   # Valid: number with country code
 13   '+4790909090',  # Valid: country code using +
 14   '004790909090', # Valid: country code using 00
 15 
 16   '+47909090',    # Invalid: + without country code 
 17                   #          or too short number
 18   '9090909o',     # Invalid: invalid character
 19   '9090909',      # Invalid: too few digits
 20   '+4690909090',  # Invalid: wrong country code
 21   '909090909',    # Invalid: too many digits
 22   '00474790909090' # Invalid: Trying to fool the regex now
 23 ]
 24 
 25 # Note: Assuming all spaces have been removed from numbers
 26 validation_format = /^((0047)?|(\+47)?|(47)?)\d{8}$/
 27 
 28 inputs.each do |x|
 29   valid = validation_format.match x
 30   puts "#{x} is #{valid ? "valid" : "INVALID!"}"
 31 end

Kommentere regex

Som sagt kan regulære utrykk være litt kryptiske, og det er viktig å dokumentere dem godt. I Ruby har jeg muligheten til å splitte dem opp over flere linjer, og da kan jeg enkelt kommentere hver lille del.

Her ser du samme regex som i telefonnummer-eksempelet over, men med kommentarer:

 35 validation_format =
 36   /^          # Start match at beginning of input
 37   (           
 38     (0047)?   # Optional "0047"
 39    |          # or
 40     (\+47)?   # optional "+47"
 41    |          # or
 42     (47)?     # optional "47"
 43   )
 44   \d{8}       # Followed by 8 digits
 45   $           # And the end should have been reached
 46   /x

Bruk av grupper

Parantesene i regexen danner det vi kaller grupper i uttrykket. Mange regex-metoder returnerer verdien av hver enkelt gruppe når jeg matcher. Dette kan jeg benytte meg av.

I neste eksempel endrer jeg litt på telefonnummer-regexen, og kjører en ny loop over alle numrene hvor jeg bruker resultatet til å normalisere dem og skrive dem ut. Resultatet av scan-metoden inneholder fem grupper (det er fem sett med paranteser i uttrykket), men jeg er kun interessert i det siste, som jeg binder til variabelen number:

 50 # Adding a group around \d{8}
 51 validation_format = /^((0047)?|(\+47)?|(47)?)(\d{8})$/
 52 
 53 inputs.each do |x|
 54   x.scan(validation_format) do |_, _, _, _, number|
 55     puts "+47 #{number}"
 56   end
 57 end
 58 # .. prints "+47 90909090" four times ..

Merk at denne loopen kun skriver ut de fire gyldige numrene fra arrayet mitt – kodeblokken blir ikke kalt om jeg ikke får en match.

Finne alle treff

Et annet vanlig bruksområde for regulære uttrykk er å finne alle treff av et mønster i en tekst. Her parser jeg et fritekst-felt hvor brukeren kan skrive inn telefonnummeret sitt. For å gjøre den enkelt er jeg optimistisk denne gangen og forutsetter at nummeret alltid kun består av 8 siffer. Da kan jeg meget enkelt finne telefonnumrene i teksten slik som dette:

 62 text = "Nummeret mitt er 90909090! (eventuelt 80808080)"
 63 
 64 number_finder = /\d{8}/
 65 
 66 print "Numbers: "
 67 puts text.scan(number_finder).join(", ")
 68 
 69 # .. prints "Numbers: 90909090, 80808080"

Hvordan ville du gjort dette uten regex? Du måtte ha behandlet teksten som et array av karakterer, testet hver karakter om det var et siffer, og hvis du fant et siffer måtte du sjekke om de syv neste også var det før du plukket ut delstrengen basert på indeks. Grisete!

Søk og erstatt

Jeg bruker regex til å søke og erstatte tekst hele tiden – ikke i kode men når jeg editerer tekst. Men i kode er det også ofte nyttig. I dette eksempelet har jeg en tekst som inneholder et versjonsnummer. Når vi oppraderer til en ny revisjon av produktet trenger jeg å oppdatere teksten. Tenk deg at versjonsnummeret står mange steder i teksten, og kanskje i mange dokumenter – da er det ikke kjekt å måtte gjøre det manuelt.

 75 text = "Bla bla. Product version: 1.5.16. Bla bla."
 76 
 77 version_pattern = /Product version: \d+\.\d+\.\d+/
 78 
 79 text.gsub! version_pattern, "Product version: 1.5.17"
 80 
 81 puts text
 82 # .. prints "Bla bla. Product version: 1.5.17. Bla bla." 

Greit nok, men vi kan bedre. Nedenfor har jeg laget kode som øker revisjonsnummeret med 1, slik at jeg slipper å spesifisere det nye nummeret eksplisitt. Jeg bruker da en kombinasjon av gsub! som du nettopp så, og scan som jeg brukte da jeg viste deg bruk av grupper. Slik kan jeg hente ut versjonsnummeret, gjøre hvilke endringer jeg vil, og oppdatere teksten med endringene.

 86 text = "Bla bla. Product version: 1.5.16. Bla bla."
 87 
 88 version_pattern = /Product version: (\d+)\.(\d+)\.(\d+)/
 89 
 90 text.gsub!(version_pattern) do |version|
 91   version.scan(version_pattern) do |major, minor, rev|
 92     version = "Product version: #{major}.#{minor}.#{rev.to_i + 1}"
 93   end
 94   version
 95 end
 96 
 97 puts text
 98 # .. prints "Bla bla. Product version: 1.5.17. Bla bla." 

Enkel parsing av en logg

TIl slutt vil jeg vise hvordan jeg kan parse en loggfil. Denne filen har en viss struktur, men det er ikke en fastbredde-fil, og den har ikke en fast felt-separator. Dette gjør at det er vanskelig å parse den med kun substring- og split-funksjoner.

Studer regexen nøye. Den lar meg plukke ut elementene jeg er interessert i, slik at jeg kan printe ut en rapport på et litt mere vennlig format.

102 file_content = <<EOF
103 2011-12-23 04:32:56 235 [Main] This is a test
104 2011-12-23 04:32:57 311 [Dispatcher] Some stuff..
105 2011-12-23 04:33:02 1344 [Dispatcher] More stuff...
106 EOF
107 
108 pattern =
109   /^                        # beginning of line
110   (                         # Group 1:
111   \d{4}\-\d{1,2}\-\d{1,2}   #   date part
112   \s{1}                     #   a space
113   \d{1,2}:\d{1,2}:\d{1,2}   #   time part
114   )
115   \s{1}\d+\s{1}             # irrelevant thread id
116   \[(\w+)\]                 # Group 2: logging entity
117   \s                        # a space
118   (.*)                      # Group 3: the message
119   $                         # line ending
120   /x
121 
122 require 'time'
123 file_content.each_line do |line|
124   line.chomp.scan(pattern) do |time, logger, message|
125     time = Time.parse(time).strftime('%H:%M')
126     puts "At #{time} #{logger} said: '#{message}'"
127   end
128 end
129 
130 # prints:
131 # At 04:32 Main said: 'This is a test'
132 # At 04:32 Dispatcher said: 'Some stuff..'
133 # At 04:33 Dispatcher said: 'More stuff...'

Oppsummering

Dette var bare de mest grunnleggende måtene å bruke regex på. Etterhvert som dette blir et naturlig verktøy for deg vil du se flere bruksområder. Språket vil gjøre deg mere produktiv, og vil gjøre at du unngår å skrive annen parse-logikk som det er lett å introdusere bugs med.

Du finner et hav av gode regex-referanser og tutorials på nettet. Let opp dokumentasjonen som gjelder for akkurat ditt programmeringsspråk, og lær deg dette grundig. Og med regex er det veldig viktig å eksperimentere – du blir ikke god i dette kun ved å lese, det lover jeg!

Og så var det bare én dag igjen til Jul :)


comments powered by Disqus