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)
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:
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"
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"
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"
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.
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 "\\."
.
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$")
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")
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)
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.
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
.
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"
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()
.
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."
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"
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.
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"
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"
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_
.