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!
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!
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.
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
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
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.
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!
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."
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...'
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 :)