Stringovi

Ovo poglavlje nas uvodi u manipulaciju stringovima u R-u. Nauičićemo osnove o tome kako stringovi rade i kako ih kreirati ručno, ali fokus će biti na regularnim izrazima (regexps). Regularni izrazi čine jezik za opisivanje šablona u stringovima. Kada prvi put pogledamo regexp, može izgledati nezgodno, ali kako se razumevanje poboljšava, uskoro počinje da ima smisla.

Ovo poglavlje se fokusira na stringr paket za manipulaciju stringovima.

library(stringr)
library(dplyr)

Osnove

Stringove možemo kreirati pojedinačnim ili dvostrukim navodnicima. Za razliku od drugih programskih jezika, u R-u nema razlike u ponašanju, mada se preporučuje korišćenje dvostrukih navodnika ("), osim ako ne želimo da kreiramo string koji sadrži više znakova ".

string1 = "Ovo je string"
string2 = 'Ako zelimo string koji sadrzi "citat" unutar stringa, koristimo "jednostruke" navodnike'

Da bismo uključili pojedinačni ili dvostruki navodnik u string, možemo koristiti specijalni escape karakter \:

single_quote = '\'' # ili "'"
double_quote = "\"" # ili '"'

To znači da ako želimo da uključimo samu kosu crtu, moramo pisati:

"\\"

Treba da imamo na umu da štampani prikaz stringa nije isti kao sam string, jer štampani prikaz prikazuje i escape karaktere. Da bismo štampali samo sadržaj stringa, koristimo writeLines().

x = c("\"", "\\")
x
## [1] "\"" "\\"
writeLines(x)
## "
## \

Postoji mnoštvo drugih specijalnih znakova. Najčešći su "\n", novi red, i "\t", tab, a možemo videti kompletnu listu tako što zatražimo pomoć za ": ?'"', ili ?"'". Takođe, nekad ćemo videti stringove poput "\u00b5": to je način zapisivanja ne-engleskih karaktera, koji radi na svim platformama:

x = "\u00b5"

Višestruki stringovi se često čuvaju u vektoru karaktera, koji možemo kreirati uz pomoć c():

c("one", "two", "three")
## [1] "one"   "two"   "three"
writeLines(c("one", "two", "three"))
## one
## two
## three

Baza R-a sadrži mnoge funkcije za rad sa stringovima, ali mi ćemo ih izbeći, jer mnogu biti nekonzistentne što ih čini teškim za pamćenje. Umesto njih, koristićemo funkcije iz stringr paketa. One imaju intuituvnija imena i sve počinju sa str_. Na primer, str_length() nam govori koliko ima karaktera u stringu:

str_length(c("a", "R for data science", NA))
## [1]  1 18 NA

Uobičajeni str_ prefiks je naročito koristan ako koristimo RStudio, jer, kao što znamo, kucanje str_ će pokrenuti automatsko dovršavanje, dozvoljavajući nam da vidimo sve strngr funkcije:

Kombinovanje stringova

Za kombinovanje dva ili više stringa, koristimo str_c():

str_c("x", "y")
## [1] "xy"
str_c("x", "y", "z")
## [1] "xyz"

Koristimo argument sep da kontrolišemo kako su razdvojeni u rezultujućem stringu:

str_c("x", "y", sep = ", ")
## [1] "x, y"

Kao i kod većine drugih funkcija u R-u, nedostajuće vrednosti su zarazne. Ako želimo da se štampaju kao “NA”, koristimo str_replace_na():

x = c("abc", NA)
writeLines(str_c("|-", x, "-|"))
## |-abc-|
## NA
writeLines(str_c("|-", str_replace_na(x), "-|"))
## |-abc-|
## |-NA-|

str_c() je vektorizovana i automatski produžuje kraće vektore na dužinu najdužeg vektora karaktera:

str_c("prefix-", c("a", "b", "c"), "-suffix")
## [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"

Možemo praviti zanimljive kombinacije sa funkcijom if:

name = "Hadley"
time_of_day = "morning"
birthday = FALSE

str_c(
  "Good ", time_of_day, " ", name,
  if (birthday) " and HAPPY BIRTHDAY",
  "."
)
## [1] "Good morning Hadley."

Da sažmemo vektor stingova u jedan string, koristimo collapse:

str_c(c("x", "y", "z"), collapse = ", ")
## [1] "x, y, z"

Podnizovi stringova

Možemo izdvajati delove stringa koristeći str_sub(). str_sub() uzima start i end argumente koji daju početnu i završnu poziciju podniza (i one se uključuju u podniz).

x = c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
## [1] "App" "Ban" "Pea"
# negativni brojevi se odbijaju od kraja
str_sub(x, -3, -1)
## [1] "ple" "ana" "ear"

Primetimo da će str_sub() raditi i ako je string prekratak: samo će vratiti najviše koliko može:

str_sub("a", 1, 5)
## [1] "a"

Takođe, na ovaj način možemo i menjati stringove:

x = c("Apple", "Banana", "Pear")
str_sub(x, 1, 1) = str_to_lower(str_sub(x, 1, 1))
x
## [1] "apple"  "banana" "pear"

Locales

Koristili smo iznad str_to_lower() za promenu teksta u mala slova. Takođe, možemo koristiti i str_to_upper() i str_to_title(). Međutim, čak i promena veličine slova se može zakomplikovati, jer različiti jezici imaju različita pravila. Možemo da izaberemo skup pravila koji ćemo koristiti tako što ćemo podesiti locale:

Locale se navodi uz pomoć ISO 639 kodova jezika, što su skraćenice od dva ili tri slova. (Ako ne znamo kod za svoj jezik, možemo baciti pogled na Wikipediu - ima dobru listu.) Ako ostavimo locale praznim, on će koristiti trenutnu lokalizaciju, obezbeđenu operativnim sistemom.

Druga važna operacija koja je pogođena lokalizacijom je sortiranje. Bazni order() i sort() u R-u sortiraju stringove koristeći trenutnu lokalizaciju. Ako želimo robusnije ponašanje, tj. da radi dobro na različitim računarima, bolje je da koristimo str_sort() i str_order() koji uzimaju dodatni argument za lokalizaciju:

x = c("apple", "eggplant", "banana")

str_sort(x, locale = "en")  # Engleski
#> [1] "apple"    "banana"   "eggplant"

str_sort(x, locale = "haw") # Havajski
#> [1] "apple"    "eggplant" "banana"

Podudaranje šablona sa regularnim izrazima

Regularni izrazi čine veoma sažet jezik koji nam omogućava da opišemo šablone u stringovima. Potrebno je malo vremena da se uvežbaju, ali kada ih shvatimo, izuzetno su korisni.

Da bismo proučli regularne izraze, koristićemo str_view() i str_view_all(). Ove funkcije uzimaju vektor karaktera i regularni izraz, i pokazuju kako se oni podudaraju. Počećemo sa veoma jednostavnim regularnim izrazima i onda postepeno prelaziti na složenije. Kada ovladamo modelom podudaranja, naučićemo kako da primenimo te ideje na raznim stringr funkcijama.

Osnovna podudaranja

Najjednostavniji šabloni tačno odgovaraju delovima stringova:

x = c("apple", "banana", "pear")
str_view(x, "an")

Sledeći, malo slioženiji korak je ., koja odgovara bilo kom znaku (osim novog reda):

str_view(x, ".a.")

Ali ako . odgovara bilo kom karakteru, šta se onda podudara sa znakom .? Potrebno je da koristimo escape karakter da bismo regularnom izrazu rekli da želimo tačno poklapanje, a ne da koristimo njegovo specijalno ponašanje. Kao i stringovi, regexps koriste kosu crtu, \ , da bi izbegli posebno ponašanje. Dakle, da bismo uklopili ., potreban nam je regexp \.. Nažalost, ovo opet pravi problem. Koristimo stringove za predstavljanje regularnih izraza, a \ se koristi kao escape simbol i u stringovima. Tako da, ako želimo da kreiramo regularni izraz \., moramo da koristimo string "\\.".

# Da bismo kreirali regularni izraz, treba nam \\
dot = "\\."

# Sada sam regularni izraz samo sadrzi jednu kosu crtu
writeLines(dot)
## \.
# A ovo govori R-u da potrazi bas tacku
str_view(c("abc", "a.c", "bef"), "a\\.c")

Ako se \ koristi kao escape karakter u regularnim izrazima, šta se onda podudara sa znakom \? Pa, moramo ga izbeći, kreirajući regularni izraz \\. Da bismo kreirali ovaj regularni izraz, treba nam string, za koji nam opet treba escape karakter \. To znači da, ako želimo podudaranje sa \, moramo da pišemo \\\\ - četiri kose crte odgovaraju jednoj!

x = "a\\b"
writeLines(x)
## a\b
str_view(x, "\\\\")

Na dalje, označavaćemo regularni izraz kao \., a string koji prestavlja taj regularni izraz kao "\\.".

Početak i kraj

Podrazumevano, regularni izrazi će odgovarati bilo kom delu stringa. Često je korisno usidriti regularni izraz tako da se poklapa sa početkom ili krajem stringa. Možemo koristiti:

  • ^ ako želimo da se podudara sa početkom stringa.

  • $ da se podudara sa krajem stringa.

x = c("apple", "banana", "pear")
str_view(x, "^a")
str_view(x, "a$")

Ako želimo da se izdvaja samo string koji u potpunosti odgovara regularnom izrazu, koristimo i ^ i $:

x = c("apple pie", "apple", "apple cake")
str_view(x, "apple")
str_view(x, "^apple$")

Klase karaktera i alternative

Postoji nekoliko posebnih šablona koji odgovaraju više nego jednom karakteru. Već smo videli ., koji odgovara bilo kom karakteru osim novog reda. Postoje još četiri korisna alata:

  • \d: odgovara bilo kojoj cifri.

  • \s: odgovara praznom prostoru (npr. razmak, tab, novi red)

  • [abc]: odgovara a, b, ili c.

  • [^abc]: odgovara svemu osim a, b i c.

Setimo se, da bismo kreirali regularni izraz koji sadrži \d ili \s, moraćemo da koristimo escape \ za string, pa ćemo kucati "\\d", odnosno "\\s".

Velika zagrada koja sadrži jedan karakter je lepa alternativa za kose crte kada želimo da uključimo jedan metakarakter u regex. Mnogi ljudi smatraju ovo čitljivijim.

# Trazimo karakter koji u normalnom slucaju ima specijalno znacenje
str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")
str_view(c("abc", "a.c", "a*c", "a c"), "a[ ]")

Ovo radi za većinu (ali ne za sve) regex metakaraktere: $, |, ?, *, +, (, ), ], {.

Nažalost, nekoliko znakova ima posebno značenje čak i unutar ovih zagrada i mora biti navedeno sa escape karakterima: [, \ i ^.

Treba voditi računa o prioritetu operacija kod složenijih izraza. Na primer, abc|d..f će odgovarati i "abc" i, npr. "deaf". Primetimo da je prioritet za | nizak, tako da abc|xyz odgovara abc ili xyz, ali ne i abcyz ili abxyz. Kao i kod matematičkih izraza, ako nismo sigurni, možemo upotrebiti zagrade da bismo pojasnili šta želimo:

str_view(c("grey", "gray"), "gr(e|a)y")

Ponavljanja

Možemo kontrolisati broj podudaranja koji se posmatra:

  • ?: 0 ili 1.

  • +: 1 ili više.

  • *: 0 ili više.

x = "1888 is the longest year in Roman numerals: MDCCCLXXXVIII"
str_view(x, "CC?")
str_view(x, "CC+")
str_view(x, 'C[LX]+')

Treba imati na umu da je prioritet ovih operatora visok, pa će nam često biti potrebne zagrade, kao npr. bana(na)+.

Takođe, može se precizirati broj podudaranja:

  • {n}: tačno n

  • {n,}: n ili više

  • {, m}: ne više od m

  • {n, m} između n i m

str_view(x, "C{2}")
str_view(x, "C{2,}")
str_view(x, "C{2,3}")

Podrazumevano, ova podudaranja će pronaći najduži mogući string. Možemo ih podesiti da vraćaju najkraći string koji se podudara stavljanjem ? nakon njih. Ovo je napredna karakteristika regularnih izraza, ali je korisno znati da postoji:

str_view(x, 'C{2,3}?')
str_view(x, 'C[LX]+?')

Grupisanje

Ranije u ovom poglavlju govorili smo o upotrebi zagrada za pojašnjenje prioriteta i dodatnih znakova kada gledamo podudaranje. Zagrade takođe služe da izdvajamo delove regularnog izraza u grupe. One skladište deo stringa koji odgovara delu regularnog izraza unutar zagrada. Možemo se pozvati na isti tekst koji je prethodno uhvatila ova grupa, korišćenjem “backreferences” poput \1,\2, itd. Na primer, regularni izraz "([a-z]+)\\s*(\\d+)" predstavlja jedno ili više slova (unutar prve grupe (([a-z]+)), zatim nijedan ili više razmaka (\\s*), iza kojih se nalaze jedan ili više brojeva u drugoj grupi((\\d+)).

Sledeći regularni izraz pronalazi svo voće koje ima ponovljeni par slova.

str_view(fruit, "(..)\\1", match = TRUE)

Alati

Sada kad smo naučili osnove regularnih izraza, vreme je da naučimo kako da ih primenimo na stvarne probleme. U ovom odeljku ćemo videti široku lepezu stringr funkcija koje nam omogućavaju da:

  • Odredimo koji stringovi odgovaraju šablonu.

  • Pronađemo pozicije podudaranja.

  • Izvučemo sadržaj podudaranja.

  • Zamenimo podudaranja novim vrednostima.

  • Podelimo string na osnovu podudaranja.

Međutim, moramo biti oprezni. Pošto su regularni izrazi tako moćni, deluje da se svaki problem može rešiti jednim regularnim izrazom. Međutim, nije baš tako.

Kao primer, pogledajmo ovaj regularni izraz koji proverava da li je email adresa ispravna: (?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t] )+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?: \r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:( ?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\0 31]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\ ](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+ (?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?: (?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z |(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n) ?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\ r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n) ?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t] )*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])* )(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t] )+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*) *:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+ |\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r \n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?: \r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t ]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031 ]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\]( ?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(? :(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(? :\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(? :(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)? [ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]| \\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<> @,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|" (?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t] )*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\ ".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(? :[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[ \]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000- \031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|( ?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,; :\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([ ^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\" .\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\ ]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\ [\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\ r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\] |\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \0 00-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\ .|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@, ;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(? :[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])* (?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\". \[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[ ^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\] ]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*( ?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\ ".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:( ?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[ \["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t ])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t ])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(? :\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+| \Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?: [^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\ ]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n) ?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[" ()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n) ?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<> @,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@, ;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t] )*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\ ".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)? (?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\". \[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?: \r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[ "()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t]) *))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]) +|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\ .(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z |(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:( ?:\r\n)?[ \t])*))*)?;\s*)

Ovo je donekle pojednostavljen primer (jer su email adrese zapravo iznenađujuće složene), ali se koristi u pravom kodu.

Ne zaboravimo da smo u programskom jeziku i da imamo i druge alate. Umesto kreiranja jednog složenog regularnog izraza često je lakše kreirati nekoliko manjih regexp-ova. Ako se zaglavimo pokušavajući da napravimo jedan regexp koji rešava naš problem, bolje je da napravimo korak unazad i razmislimo da li bismo mogli da razbijemo problem na manje delove, rešavajući izazove jedan po jedan.

Pronalaženje podudaranja

Da bismo odredili da li smo pronašli podudaranje u vektoru karaktera, koristimo str_detect(). Ona vraća logički vektor koji je iste dužine kao i ulaz:

x = c("apple", "banana", "pear")
str_detect(x, "e")
## [1]  TRUE FALSE  TRUE

Znamo da kada koristimo logički vektor u numeričkom kontekstu, FALSE postaje 0, a TRUE postaje 1. To čini sum() i mean() korisnim ako želimo da odgovorimo na pitanja o broju podudaranja u većem vektoru:

# Koliko reci pocinje sa t?
sum(str_detect(words, "^t"))
## [1] 65
# Koliki je udeo reci koje se zavrsavaju samoglasnikom?
mean(str_detect(words, "[aeiou]$"))
## [1] 0.2765306

Kada imamo kompleksne logičke uslove, često je lakše da kombinujemo višestruke pozive str_detect() sa logičkim operatorima, umesto da pokušavamo da kreiramo jedan regularni izraz. Na primer, evo dva načina za pronalaženje svih reči koje ne sadrže samoglasnike:

# Pronalazimo sve reci koje sadrze bar jedan samoglasnik, pa vrsimo negaciju
no_vowels_1 = !str_detect(words, "[aeiou]")

# Pronalazimo sve reci koje se sastoje samo od suglasnika
no_vowels_2 = str_detect(words, "^[^aeiou]+$")

identical(no_vowels_1, no_vowels_2)
## [1] TRUE

Rezultati su identični, ali prvi pristup deluje lakši. Ako se regularni izraz preterano komplikuje, treba da pokušamo da ga izdelimo na manje delove, damo svakom delu ime, a zatim da kombinujemo delove sa logičkim operacijama.

Obično koristimo str_detect() da bismo izabrali elemente koji odgovaraju izrazu. Ovo možemo uraditi i uz pomoć pogodnog str_subset():

words[str_detect(words, "x$")]
## [1] "box" "sex" "six" "tax"
str_subset(words, "x$")
## [1] "box" "sex" "six" "tax"

Međutim, obično će stringovi činiti jednu kolonu naše baze podataka, pa ćemo koristiti filter:

df = tibble(
  word = words, 
  i = seq_along(word)
)

df %>% 
  filter(str_detect(word, "x$"))
## # A tibble: 4 x 2
##   word      i
##   <chr> <int>
## 1 box     108
## 2 sex     747
## 3 six     772
## 4 tax     841

Jedna varijacija str_detect() je str_count(): umesto jednostavnog da ili ne, govori nam koliko ima podudaranja u stringu:

x = c("apple", "banana", "pear")
str_count(x, "a")
## [1] 1 3 1
# Koliko ima prosecno samoglasnika u reci?
mean(str_count(words, "[aeiou]"))
## [1] 1.991837

Prirodno je koristiti str_count() sa mutate():

df %>% 
  mutate(
    vowels = str_count(word, "[aeiou]"),
    consonants = str_count(word, "[^aeiou]")
  )
## # A tibble: 980 x 4
##    word         i vowels consonants
##    <chr>    <int>  <int>      <int>
##  1 a            1      1          0
##  2 able         2      2          2
##  3 about        3      3          2
##  4 absolute     4      4          4
##  5 accept       5      2          4
##  6 account      6      3          4
##  7 achieve      7      4          3
##  8 across       8      2          4
##  9 act          9      1          2
## 10 active      10      3          3
## # ... with 970 more rows

Treba imati na umu da se podudaranja nikad ne preklapaju. Na primer, u "abababa", koliko će se puta obrazac "aba" podudarati? Regularni izrazi kažu dva, a ne tri:

str_count("abababa", "aba")
## [1] 2
str_view_all("abababa", "aba")

Primetimo da ovde koristimo str_view_all(). Kao što ćemo uskoro videti, mnoge stringr funkcije dolaze u parovima: jedna funkcija radi sa jednim podudaranjem, dok druga radi sa svim. Ta druga funkcija će imati sufiks _all.

Izdvajanje podudaranja

Da bismo izdvojili tekst koji se podudara, koristimo str_extract(). Da bismo to pokazali, trebaće nam komplikovaniji primer. Koristićemo “Harvard rečenice”, date u stringr::sentences, koje su korisne za vežbanje regexp-ova.

length(sentences)
## [1] 720
head(sentences)
## [1] "The birch canoe slid on the smooth planks." 
## [2] "Glue the sheet to the dark blue background."
## [3] "It's easy to tell the depth of a well."     
## [4] "These days a chicken leg is a rare dish."   
## [5] "Rice is often served in round bowls."       
## [6] "The juice of lemons makes fine punch."

Recimo da želimo da pronađemo sve rečenice koje sadrže boju. Prvo kreiramo vektor koji sadrži imena boja, a zatim ga pretvaramo u jedan regularni izraz:

colours = c("red", "orange", "yellow", "green", "blue", "purple")
colour_match = str_c(colours, collapse = "|")
colour_match
## [1] "red|orange|yellow|green|blue|purple"

Sada možemo da izaberemo rečenice koje sadrže boju, a zatim i da je izvučemo:

has_colour = str_subset(sentences, colour_match)

matches = str_extract(has_colour, colour_match)

head(matches)
## [1] "blue" "blue" "red"  "red"  "red"  "blue"

Primetimo da str_extract() samo izdvaja prvo podudaranje. To se najlakše može videti tako što ćemo prvo izabrati sve rečenice koje imaju više od jedne podudarnosti:

more = sentences[str_count(sentences, colour_match) > 1]

str_view_all(more, colour_match)
str_extract(more, colour_match)
## [1] "blue"   "green"  "orange"

Ovo je uobičajen obrazac za stringr funkcije, jer rad sa jednim podudaranjem omogućava da koristimo mnogo jednostavniju strukturu podataka. Da bismo dobili sva podudaranja, koristimo str_extract_all(). Ona vraća listu:

str_extract_all(more, colour_match)
## [[1]]
## [1] "blue" "red" 
## 
## [[2]]
## [1] "green" "red"  
## 
## [[3]]
## [1] "orange" "red"

Ako koristimo simplify = TRUE, str_extract_all će vratiti matricu (prazna polja će se automatski popuniti praznim stringom):

str_extract_all(more, colour_match, simplify = TRUE)
##      [,1]     [,2] 
## [1,] "blue"   "red"
## [2,] "green"  "red"
## [3,] "orange" "red"
x = c("a", "a b", "a b c")
str_extract_all(x, "[a-z]", simplify = TRUE)
##      [,1] [,2] [,3]
## [1,] "a"  ""   ""  
## [2,] "a"  "b"  ""  
## [3,] "a"  "b"  "c"

Grupisana podudaranja

Rekli smo da možemo koristiti zagrade i da izdvojimo delove složenog regularnog izraza. Na primer, recimo da želimo da izvlačimo imenice iz rečenica. Pokušaćemo sa traženjem bilo koje reči koja dolazi posle a ili the. Definisanje reči u regularnom izrazu je malo komplikovano, tako da ovde koristimo jednostavnu aproksimaciju: niz od najmanje jednog karaktera koji nije razmak.

noun = "(a|the) ([^ ]+)"

has_noun = sentences %>%
  str_subset(noun) %>%
  head(10)

has_noun %>% 
  str_extract(noun)
##  [1] "the smooth" "the sheet"  "the depth"  "a chicken"  "the parked"
##  [6] "the sun"    "the huge"   "the ball"   "the woman"  "a helps"

str_extract() daje nam potpunu podudarnost, dok str_match() daje i svaku pojedinačnu komponentu. Umesto vektora karaktera, vraća matricu, sa jednom kolonom za kompletno podudaranje i po jednom kolonom za svaku grupu:

has_noun %>% 
  str_match(noun)
##       [,1]         [,2]  [,3]     
##  [1,] "the smooth" "the" "smooth" 
##  [2,] "the sheet"  "the" "sheet"  
##  [3,] "the depth"  "the" "depth"  
##  [4,] "a chicken"  "a"   "chicken"
##  [5,] "the parked" "the" "parked" 
##  [6,] "the sun"    "the" "sun"    
##  [7,] "the huge"   "the" "huge"   
##  [8,] "the ball"   "the" "ball"   
##  [9,] "the woman"  "the" "woman"  
## [10,] "a helps"    "a"   "helps"

(Nije iznenađujuće da je naš način otkrivanje imenica loš, vidimo da prihvata i prideve kao što su “smooth” i “parked”.)

Ako su naši podaci u tiblu, često je lakše koristiti tidyr::extract(). Ovo radi slično kao str_match(), ali zahteva od nas da imenujemo kolone u koje se smeštaju podudaranja:

tibble(sentence = sentences) %>% 
  tidyr::extract(
    sentence, c("article", "noun"), "(a|the) ([^ ]+)", 
    remove = FALSE
  )
## # A tibble: 720 x 3
##    sentence                                    article noun   
##    <chr>                                       <chr>   <chr>  
##  1 The birch canoe slid on the smooth planks.  the     smooth 
##  2 Glue the sheet to the dark blue background. the     sheet  
##  3 It's easy to tell the depth of a well.      the     depth  
##  4 These days a chicken leg is a rare dish.    a       chicken
##  5 Rice is often served in round bowls.        <NA>    <NA>   
##  6 The juice of lemons makes fine punch.       <NA>    <NA>   
##  7 The box was thrown beside the parked truck. the     parked 
##  8 The hogs were fed chopped corn and garbage. <NA>    <NA>   
##  9 Four hours of steady work faced us.         <NA>    <NA>   
## 10 Large size in stockings is hard to sell.    <NA>    <NA>   
## # ... with 710 more rows

Ako želimo da nađemo podudaranja za svaki string, biće nam potreban str_match_all().

Zamena podudaranja

str_raplace() i str_replace_all() dozvoljava nam da zamenimo podudaranja novim stringovima. Najjednostavnije je da se samo zameni obrazac fisknim stringom:

x = c("apple", "pear", "banana")
str_replace(x, "[aeiou]", "-")
## [1] "-pple"  "p-ar"   "b-nana"
str_replace_all(x, "[aeiou]", "-")
## [1] "-ppl-"  "p--r"   "b-n-n-"

Sa str_replace_all() možemo izvršiti i više zamena:

x = c("1 house", "2 cars", "3 people")

str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))
## [1] "one house"    "two cars"     "three people"

Umesto da zamenimo fiksnim stringom, možemo da koristimo “backreferences” za umetanje komponenti podudaranja. U sledećem kodu, okrećemo redosled druge i treće reči:

sentences %>% 
  str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>% 
  head(5)
## [1] "The canoe birch slid on the smooth planks." 
## [2] "Glue sheet the to the dark blue background."
## [3] "It's to easy tell the depth of a well."     
## [4] "These a days chicken leg is a rare dish."   
## [5] "Rice often is served in round bowls."

Razdvajanje

Koristimo str_split() da izdelimo string na delove. Na primer, možemo izdeliti rečenice:

sentences %>%
  head(5) %>% 
  str_split(" ")
## [[1]]
## [1] "The"     "birch"   "canoe"   "slid"    "on"      "the"     "smooth" 
## [8] "planks."
## 
## [[2]]
## [1] "Glue"        "the"         "sheet"       "to"          "the"        
## [6] "dark"        "blue"        "background."
## 
## [[3]]
## [1] "It's"  "easy"  "to"    "tell"  "the"   "depth" "of"    "a"     "well."
## 
## [[4]]
## [1] "These"   "days"    "a"       "chicken" "leg"     "is"      "a"      
## [8] "rare"    "dish."  
## 
## [[5]]
## [1] "Rice"   "is"     "often"  "served" "in"     "round"  "bowls."

Pošto svaka komponenta može da sadrži različit broj delova, ovo vraća listu. Ako radimo sa vektorom dužine 1, najlakše je da samo izvučemo prvi element liste:

"a|b|c|d" %>% 
  str_split("\\|") %>% 
  .[[1]]
## [1] "a" "b" "c" "d"

Takođe, kao i kod ostalih stringr funkcija koje vraćaju listu, možemo koristiti simplify = TRUE da bismo vratili matricu:

sentences %>%
  head(5) %>% 
  str_split(" ", simplify = TRUE)
##      [,1]    [,2]    [,3]    [,4]      [,5]  [,6]    [,7]    
## [1,] "The"   "birch" "canoe" "slid"    "on"  "the"   "smooth"
## [2,] "Glue"  "the"   "sheet" "to"      "the" "dark"  "blue"  
## [3,] "It's"  "easy"  "to"    "tell"    "the" "depth" "of"    
## [4,] "These" "days"  "a"     "chicken" "leg" "is"    "a"     
## [5,] "Rice"  "is"    "often" "served"  "in"  "round" "bowls."
##      [,8]          [,9]   
## [1,] "planks."     ""     
## [2,] "background." ""     
## [3,] "a"           "well."
## [4,] "rare"        "dish."
## [5,] ""            ""

Možemo navesti maksimalni broj delova:

fields = c("Name: Hadley", "Country: NZ", "Age: 35")
fields %>% str_split(":", n = 2, simplify = TRUE)
##      [,1]      [,2]     
## [1,] "Name"    " Hadley"
## [2,] "Country" " NZ"    
## [3,] "Age"     " 35"

Stringove moemo podeliti i na karaktere, reči, ili rečenice uz pomoć boundary():

x = "This is a sentence.  This is another sentence."
str_view_all(x, boundary("word"))
str_split(x, " ")[[1]]
## [1] "This"      "is"        "a"         "sentence." ""          "This"     
## [7] "is"        "another"   "sentence."
str_split(x, boundary("word"))[[1]]
## [1] "This"     "is"       "a"        "sentence" "This"     "is"      
## [7] "another"  "sentence"

Pronalaženje podudaranja

str_locate() i str_locate_all() daju početne i završne pozicije svakog podudaranja. Ovo je posebno korisno kada nijedna funkcija ne radi baš ono što želimo. Možemo koristiti str_locate() da nađemo odgovarajuća podudaranja, pa str_sub() da ih izvučemo i/ili modifikujemo.

Drugi tipovi

Do sada smo regularne izraze navodili samo kao stringove. To možemo raditi, jer, kada koristimo šablon koji je string, on se automatski prebacuje u poziv za regex():

# Regularni poziv
str_view(fruit, "nana", match = TRUE)
# Je skracenica za
str_view(fruit, regex("nana"), match = TRUE)

Možemo koristiti i druge argumente u regex() da podesimo detalje podudaranja:

  • ignore_case = TRUE dozvoljava da se karakteri podudaraju bez obzira na veličinu slova. Ovo uvek koristi trenutni “locale”.
bananas = c("banana", "Banana", "BANANA")
str_view(bananas, "banana")
str_view(bananas, regex("banana", ignore_case = TRUE))
  • multiline = TRUE dozvoljava ^ i $ da traže podudaranje na početku i kraju svakog reda, a ne na početku i kraju kompletnog stringa.
x = "Line 1\nLine 2\nLine 3"
writeLines(x)
## Line 1
## Line 2
## Line 3
str_extract_all(x, "^Line")[[1]]
## [1] "Line"
str_extract_all(x, regex("^Line", multiline = TRUE))[[1]]
## [1] "Line" "Line" "Line"
  • comments = TRUE omogućava da koristimo komentare i prazan prostor da bi složeni regularni izrazi bili razumljiviji. Razmaci se ignorišu, kao i sve posle #. Da bi se vršila podudaranja sa praznim prostorom, trebaće nam: "\\ ".
phone = regex("
  \\(?     # opciono otvorena zagrada
  (\\d{3}) # tri broja
  [) -]?   # opcino zatvorena zagrada, razmak ili crtica 
  (\\d{3}) # jos tri broja
  [ -]?    # opciono razmak ili crtica
  (\\d{3}) # jos tri broja
  ", comments = TRUE)

str_match("514-791-8141", phone)
##      [,1]          [,2]  [,3]  [,4] 
## [1,] "514-791-814" "514" "791" "814"
  • dotall = TRUE dozvoljava . da odgovara svemu, uključujući i \n.

Postoje još tri funkcije koje možemo koristiti umesto regex():

  • fixed(): Odgovara tačno navedenom nizu bajtova. Ona radi na veoma niskom nivou i omogućava nam da izbegnemo kompleksno dodavanje escape karaktera i može biti mnogo brža od regularnih izraza, Sledeći kod pokazuje da je 3 puta brža za jednostavan primer.
library(microbenchmark)
microbenchmark(
  fixed = str_detect(sentences, fixed("the")),
  regex = str_detect(sentences, "the"),
  times = 20
)
## Unit: microseconds
##   expr     min      lq    mean  median      uq     max neval
##  fixed 174.472 179.033 203.237 187.871 192.147 507.451    20
##  regex 626.616 632.888 644.747 634.313 637.449 772.578    20

Treba biti oprezan sa korišćenjem fixed() sa ne-engleskim podacima. To je problematično jer često postoji više načina za predstavljanje istog karaktera. Na primer, postoje dva načina definisanja “á”: ili kao jedan znak ili kao “a” plus akcenat:

a1 = "\u00e1"
a2 = "a\u0301"
c(a1, a2)
#> [1] "á" "á"
a1 == a2
## [1] FALSE

Oni se prikazuju identično, ali zato što su definisani drugačije, fixed() ne nalazi podudarnost. Umesto toga, možemo da koristimo coll(), definisan sledeći, da bismo poštovali ljudska pravila za poređenje karaktera:

str_detect(a1, fixed(a2))
## [1] FALSE
str_detect(a1, coll(a2))
## [1] TRUE
  • coll(): Poredi stringove koristeći standardna pravila upoređivanja. Ovo je korisno za podudaranja neosetljiva na veličinu slova. Treba imati na umu da coll() uzima parametar locale koji kontroliše koja pravila se koriste za upoređivanje karaktera. U različitim delovima sveta postoje različita pravila!

I fixed() i regex() imaju argumente ignore_case(), ali ne dozvoljavaju da izaberemo lokalizaciju: oni uvek koriste podrazumevanu lokalizaciju. Možemo videti to sledećim kodom, (više o stringi kasnije).

stringi::stri_locale_info()
## $Language
## [1] "sr"
## 
## $Country
## [1] "RS"
## 
## $Variant
## [1] ""
## 
## $Name
## [1] "sr_Cyrl_RS"

Loša strana coll() je brzina: zato što su pravila za prepoznavanje toga koji su karakteri isti komplikovana, coll() je relativno spor u odnosu na regex() i fixed().

Kod str_split() smo videli kako možemo koristiti boundary(). Možemo ga koristiti i sa drugim funkcijama:

x = "This is a sentence."
str_view_all(x, boundary("word"))
str_extract_all(x, boundary("word"))
## [[1]]
## [1] "This"     "is"       "a"        "sentence"

Druge upotrebe regularnih izraza

Postoje dve korisne funkcije u bazi R-a koje takođe koriste regularne izraze:

  • apropos() pretražuje sve objekte dostupne iz globalnog okruženja. Ovo je korisno ako se sećamo dela imena funkcije, ali ne i celog imena.
apropos("replace")
## [1] "replace"          "setReplaceMethod" "str_replace"     
## [4] "str_replace_all"  "str_replace_na"
  • dir() ispisuje sve datoteke u direktorijumu. Argument pattern uzima regularni izraz i vraća samo imena datoteka koja sadrže taj obrazac. Na primer, možmo pronaći sve R Markdown dokumente u trenutnom direktorijumu sa:
head(dir(pattern = "\\.Rmd$"))
## [1] "FaktoriDate.Rmd" "Stringovi.Rmd"   "Wrangle(1).Rmd"  "Wrangle(2).Rmd" 
## [5] "Wrangle(2)1.Rmd" "Wrangle(3).Rmd"

Stringi

stringr je izgrađen na osnovu stringi paketa. stringr je koristan kada učimo jer izlaže minimalan skup funkcija, koje su pažljivo odabrane da bi se obradile najčešće funkcije za rad sa stringovima. Sa druge strane, stringi je dizajniran da bude sveobuhvatan. Sadrži skoro sve funkcije koje su nam potrebne: stringi ima 234 funkcije, a stringr 46.

Ako se zaglavimo u pokušajima da nešto uradimo uz pomoć stringr, vredi pogledati stringi. Paketi rade veoma slično, tako da bi trebalo da možemo da prevedemo naše stringr znanje na prirodan način. Glavna razlika je prefiks: str_ vs. stri_.