Regular Expression di Rust dengan regex
Regular expression (disingkat regex) adalah bahasa pola mini untuk teks. Alih-alih bilang “cari kata kucing”, kamu bisa bilang “cari empat digit, tanda hubung, dua digit, tanda hubung, dua digit”, lalu menemukan semua tanggal seperti 2024-05-17 sekaligus.
Standard library Rust punya method string yang praktis seperti contains, split, dan find (lihat String). Kalau potongan teks yang tetap tidak cukup, dan kamu butuh sebuah bentuk teks, pakai crate (library Rust) regex. Crate ini dikelola oleh proyek Rust dan sangat cepat.
Menambahkan regex dan pencocokan pertamamu
Section titled “Menambahkan regex dan pencocokan pertamamu”Di folder proyekmu, jalankan:
cargo add regexPerintah itu menambahkan regex = "1" di bawah [dependencies] dalam Cargo.toml. (Baru kenal crate? Lihat Cargo dan crate.) Tombol “Coba sendiri” membuka Rust Playground, yang sudah menyediakan regex.
use regex::Regex;
fn main() { let re = Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap();
println!("{}", re.is_match("Dirilis pada 2024-05-17.")); println!("{}", re.is_match("Dirilis tahun lalu."));}Output:
truefalse\d artinya “digit apa saja” dan {4} artinya “tepat empat kali dari hal sebelumnya”. is_match menjawab satu pertanyaan: adakah yang cocok di mana saja dalam teks?
Perhatikan huruf r sebelum pola: r"\d{4}". Itu adalah raw string. Di string Rust biasa, \ memulai escape seperti \n (baris baru). Pola regex penuh dengan backslash, jadi raw string menyelamatkanmu dari menulis "\\d{4}". Selalu pakai raw string untuk pola.
Kenapa Regex::new mengembalikan Result
Section titled “Kenapa Regex::new mengembalikan Result”Sebuah pola itu seperti program kecil, dan bisa saja ada kesalahan di dalamnya, misalnya kurung yang lupa ditutup. Regex::new memeriksa pola dan mengembalikan Result: Ok(Regex) kalau valid, atau Err dengan pesan yang membantu kalau tidak:
use regex::Regex;
fn main() { match Regex::new(r"(\d+") { Ok(_) => println!("pola valid"), Err(e) => println!("pola salah:\n{e}"), }}Output:
pola salah:regex parse error: (\d+ ^error: unclosed groupPesan error dari crate regex tetap berbahasa Inggris: unclosed group artinya ada grup (kurung buka) yang belum ditutup, dan tanda ^ menunjuk posisinya.
Kalau polanya ditulis langsung di kodemu dan kamu yakin benar, .unwrap() tidak masalah: kalau ternyata salah, kamu akan tahu saat program pertama kali dijalankan. Kalau polanya berasal dari pengguna (misalnya dari kotak pencarian), tangani Err-nya, jangan biarkan program crash.
Mencari kecocokan: find dan find_iter
Section titled “Mencari kecocokan: find dan find_iter”find mengembalikan kecocokan pertama sebagai Option. find_iter memberimu iterator atas semua kecocokan (lihat Iterator):
use regex::Regex;
fn main() { let re = Regex::new(r"\d+").unwrap(); let text = "3 apel, 12 pir dan 7 plum";
if let Some(m) = re.find(text) { println!("angka pertama: {} (di byte {}..{})", m.as_str(), m.start(), m.end()); }
let all: Vec<&str> = re.find_iter(text).map(|m| m.as_str()).collect(); println!("semua angka: {:?}", all);
let total: u32 = re.find_iter(text).map(|m| m.as_str().parse::<u32>().unwrap()).sum(); println!("total buah: {total}");}Output:
angka pertama: 3 (di byte 0..1)semua angka: ["3", "12", "7"]total buah: 22\d+ artinya “satu digit atau lebih”, jadi 12 ditemukan sebagai satu kecocokan, bukan dua. Setiap kecocokan tahu teksnya (as_str()) dan posisinya (start() dan end(), dalam byte).
Capture group dan named group
Section titled “Capture group dan named group”Sering kali kamu tidak hanya butuh kecocokannya, tapi juga potongan-potongan di dalamnya. Bungkus sebagian pola dengan tanda kurung untuk menangkap (capture) bagian itu. Beri nama dengan (?<nama>...) supaya kodemu lebih mudah dibaca:
use regex::Regex;
fn main() { let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})").unwrap(); let text = "Backup berjalan pada 2024-05-17 dan 2024-06-02.";
if let Some(caps) = re.captures(text) { println!("seluruh kecocokan: {}", &caps[0]); println!("tahun: {}", &caps["year"]); println!("bulan: {}", &caps[2]); println!("hari: {}", caps.name("day").unwrap().as_str()); }
for caps in re.captures_iter(text) { println!("{}/{}/{}", &caps["day"], &caps["month"], &caps["year"]); }}Output:
seluruh kecocokan: 2024-05-17tahun: 2024bulan: 05hari: 1717/05/202402/06/2024Ada tiga cara membaca sebuah grup:
caps[0]selalu berisi seluruh kecocokan. Grup diberi nomor mulai dari 1, dari kiri ke kanan:caps[2]adalah bulannya.caps["year"]membaca grup berdasarkan namanya. Mengakses dengan[...]akan panic kalau grup itu tidak ikut dalam kecocokan.caps.name("day")mengembalikanOption, pilihan yang aman kalau grupnya opsional.
captures mencari kecocokan pertama. captures_iter menelusuri setiap kecocokan, sama seperti find_iter.
Mengganti teks
Section titled “Mengganti teks”replace mengganti kecocokan pertama, dan replace_all mengganti setiap kecocokan. Di teks pengganti, $nama (atau $1) menyisipkan apa yang ditangkap oleh sebuah grup:
use regex::Regex;
fn main() { let spaces = Regex::new(r"\s+").unwrap(); println!("{}", spaces.replace_all("terlalu banyak spasi", " "));
let date = Regex::new(r"(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})").unwrap(); let text = "Dari 2024-05-17 sampai 2024-06-02"; println!("{}", date.replace(text, "$d/$m/$y")); println!("{}", date.replace_all(text, "$d/$m/$y")); println!("{}", date.replace_all(text, "$m_$y")); println!("{}", date.replace_all(text, "${m}_${y}"));}Output:
terlalu banyak spasiDari 17/05/2024 sampai 2024-06-02Dari 17/05/2024 sampai 02/06/2024Dari 2024 sampai 2024Dari 05_2024 sampai 06_2024Lihat baris keempat. Hasilnya Dari 2024 sampai 2024, bukan 05_2024. Kenapa? Nama grup boleh mengandung _, jadi $m_ dibaca sebagai grup bernama m_. Grup itu tidak ada, jadi hasilnya teks kosong. Taruh namanya di dalam kurung kurawal, ${m}_, untuk menandai dengan jelas di mana nama itu berakhir. Kalau ragu, pakai kurung kurawal.
\s+ artinya “satu karakter whitespace (spasi, tab, baris baru) atau lebih”, jadi menggantinya dengan satu spasi adalah cara klasik untuk merapikan input yang berantakan.
Memecah teks
Section titled “Memecah teks”split memotong string di setiap tempat yang cocok dengan pola. Mirip str::split, tapi pemisahnya bisa berupa pola:
use regex::Regex;
fn main() { let sep = Regex::new(r"\s*[,;]\s*").unwrap(); let parts: Vec<&str> = sep.split("apel, pisang;ceri , kurma").collect(); println!("{:?}", parts);
let two: Vec<&str> = sep.splitn("a, b, c, d", 2).collect(); println!("{:?}", two);}Output:
["apel", "pisang", "ceri", "kurma"]["a", "b, c, d"][,;] artinya “koma atau titik koma”, dan \s* di kedua sisinya menelan spasi apa pun di sekitarnya. splitn(text, 2) berhenti setelah dua potongan.
Compile sekali, jangan di dalam loop
Section titled “Compile sekali, jangan di dalam loop”Membuat Regex adalah bagian yang lambat: crate ini mengubah polamu menjadi mesin pencocok yang efisien. Sedangkan memakainya cepat. Jadi jangan pernah memanggil Regex::new di dalam perulangan (loop) atau fungsi yang sering dijalankan. Buat sekali, lalu pakai berulang kali.
Untuk pola yang dipakai di seluruh programmu, simpan dalam static dengan std::sync::LazyLock. Pola itu dibuat saat pertama kali dipakai, lalu dibagikan:
use regex::Regex;use std::sync::LazyLock;
static EMAIL: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"(?i)^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$").unwrap()});
fn is_email(s: &str) -> bool { EMAIL.is_match(s)}
fn main() { println!("{input:22} -> {}", is_email(input)); }}Output:
[email protected] -> true[email protected] -> truenot-an-email -> falsex@y -> false(Input contohnya sengaja dibiarkan apa adanya, karena {input:22} meratakan kolom berdasarkan panjang teks.)
Bagian baru dalam pola ini:
(?i)menyalakan pencocokan yang tidak membedakan huruf besar dan kecil, jadi[a-z]juga cocok denganBdanM.^dan$artinya “awal teks” dan “akhir teks”. Tanpa keduanya,is_matchakan menjawab ya untuk"halo [email protected]!!", karena alamat email itu muncul di suatu tempat di dalamnya.\.adalah titik sungguhan. Titik biasa.artinya “karakter apa saja”.
Ini pengecekan “kelihatannya seperti email”, cocok untuk menangkap salah ketik. Memvalidasi semua alamat email yang sah dengan regex terkenal sangat sulit, jadi satu-satunya tes yang benar-benar pasti adalah mengirim email ke alamat itu.
Contoh: mengambil data dari baris log
Section titled “Contoh: mengambil data dari baris log”Ayo gabungkan semuanya. Kita membaca sebuah log, melewati baris yang formatnya tidak sesuai, dan untuk setiap baris ERROR kita cetak tanggal, jam, dan semua alamat email yang ada di pesannya:
use regex::Regex;use std::sync::LazyLock;
static LOG_LINE: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>[A-Z]+) +(?<msg>.*)$").unwrap()});
static EMAIL: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}").unwrap()});
fn main() { let log = "\2024-05-17 09:12:03 INFO user [email protected] berhasil login2024-05-17 09:15:44 ERROR pembayaran gagal untuk [email protected]baris ini rusak2024-05-18 11:02:10 WARN disk 91% penuh
let mut errors = 0; for line in log.lines() { let Some(caps) = LOG_LINE.captures(line) else { println!("dilewati: {line:?}"); continue; }; if &caps["level"] == "ERROR" { errors += 1; let emails: Vec<&str> = EMAIL.find_iter(&caps["msg"]).map(|m| m.as_str()).collect(); println!("{} {} -> {:?}", &caps["date"], &caps["time"], emails); } } println!("{errors} error ditemukan");}Output:
2024-05-17 09:15:44 -> ["[email protected]"]dilewati: "baris ini rusak"2024-05-18 11:05:00 -> ["[email protected]", "[email protected]"]2 error ditemukanBeberapa catatan:
"\di awallogadalah trik Rust: backslash di akhir baris di dalam string akan melewati karakter baris baru, jadi baris log pertama langsung dimulai.let Some(caps) = ... else { ... }adalah let-else: kalau barisnya tidak cocok, kita cetak lalucontinueke baris berikutnya.(?<level>[A-Z]+) +mencocokkan level lalu satu spasi atau lebih, karenaINFOdanWARNdiberi satu spasi tambahan supaya sejajar denganERROR.- Di program sungguhan, kamu akan membaca log dari file, baris demi baris. Lihat File I/O.
Yang sengaja tidak dilakukan regex
Section titled “Yang sengaja tidak dilakukan regex”Kalau kamu pernah memakai regex di JavaScript, Python, atau Perl, mungkin kamu ingin memakai look-around ((?=...), (?<=...)) atau backreference (\1, “teks yang sama dengan grup 1”). Crate regex tidak mendukung keduanya, dan memberitahumu begini:
regex parse error: foo(?=bar) ^^^error: look-around, including look-ahead and look-behind, is not supportedArtinya: look-around, termasuk look-ahead dan look-behind, tidak didukung. Ini pertukaran yang disengaja. Fitur-fitur itu bisa membuat sebagian pola butuh waktu eksponensial pada input tertentu, jadi pola pendek ditambah string yang cukup panjang bisa membuat program macet (ini disebut catastrophic backtracking, dan pernah membuat situs web sungguhan down). Tanpa fitur itu, crate regex bisa menjamin waktu pencocokan hanya tumbuh sebanding dengan panjang teks (dikali ukuran pola). Teks dua kali lebih panjang berarti kira-kira dua kali kerja, tidak pernah meledak. Karena itu crate ini aman, bahkan untuk pola atau teks yang berasal dari pengguna.
Biasanya kamu bisa mendapat hasil yang sama dengan cara lain: pakai capture group dan baca hanya bagian yang kamu mau, atau lakukan pengecekan sederhana kedua dengan kode Rust biasa. Kalau kamu benar-benar butuh fitur itu, crate fancy-regex menambahkannya, tapi tanpa jaminan waktu tadi.
Contekan
Section titled “Contekan”| Pola | Cocok dengan |
|---|---|
. |
karakter apa saja kecuali baris baru |
\d \w \s |
sebuah digit, karakter “kata” (huruf, digit, _), whitespace |
\D \W \S |
kebalikannya: bukan digit, bukan karakter kata, bukan whitespace |
[abc] [a-z] [^0-9] |
salah satu dari a, b, c; huruf kecil apa saja; apa saja selain digit |
* + ? |
nol atau lebih, satu atau lebih, nol atau satu dari item sebelumnya |
{3} {2,5} {2,} |
tepat 3, dari 2 sampai 5, minimal 2 |
^ $ |
awal dan akhir teks |
\b |
batas kata (antara karakter kata dan karakter bukan kata) |
(...) (?<nama>...) |
grup bernomor, grup bernama |
(?i) |
tidak membedakan huruf besar/kecil mulai dari sini |
Satu lagi: garis tegak berarti “atau”. Contohnya, kucing|anjing cocok dengan salah satu kata itu, dan gr(a|e)y cocok dengan gray dan grey. Untuk mencocokkan karakter khusus apa adanya, taruh backslash di depannya: \., \(, \$.
Kesalahan umum
Section titled “Kesalahan umum”Lupa memakai raw string. Di string biasa, \d bukan escape yang valid, jadi Rust sudah berhenti sebelum crate regex sempat melihat polamu:
use regex::Regex;
fn main() { let re = Regex::new("\d+").unwrap(); println!("{}", re.is_match("id=22"));}error: unknown character escape: `d` --> src/main.rs:4:27 |4 | let re = Regex::new("\d+").unwrap(); | ^ unknown character escape |help: if you meant to write a literal backslash (perhaps escaping in a regular expression), consider a raw string literal |4 | let re = Regex::new(r"\d+").unwrap(); | +Compiler bilang \d adalah escape karakter yang tidak dikenal, dan bahkan menyarankan solusinya: tambahkan r di depan.
Membuat regex di dalam loop. Kode ini bisa di-compile dan berjalan, tapi lambat, karena polanya di-compile ulang untuk setiap baris:
for line in lines { let re = Regex::new(r"\d+").unwrap(); // di-compile lagi dan lagi if re.is_match(line) { /* ... */ }}Pindahkan Regex::new ke atas loop, atau ke dalam static LazyLock seperti yang ditunjukkan sebelumnya.
Lupa ^ dan $ saat memvalidasi. is_match mencari pola di mana saja. Untuk memastikan seluruh input punya bentuk tertentu, kunci polanya di kedua ujung.
Ringkasan
Section titled “Ringkasan”- Tambahkan crate-nya dengan
cargo add regex, dan tulis pola sebagai raw string:r"\d+". Regex::newmengembalikanResult, karena sebuah pola bisa saja tidak valid.is_matchmenjawab ya atau tidak;find/find_itermemberi teks yang cocok;captures/captures_itermemberi grup berdasarkan nomor atau(?<nama>...).replacedanreplace_allbisa menyisipkan grup dengan$nama; pakai${nama}kalau setelahnya ada huruf atau_.- Compile pola sekali lalu pakai berulang kali, misalnya dalam
staticLazyLock. - Tidak ada look-around atau backreference, dan itu disengaja: sebagai gantinya, pencocokan selalu berjalan dalam waktu linear.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa pola regex di Rust biasanya diawali r, seperti r"\d+"?
2. Dengan pola (?<y>\d{4})-(?<m>\d{2}), apa hasil caps["m"] untuk teks 2024-05?
3. Kenapa crate regex tidak mendukung look-around dan backreference?
4. Di mana tempat terbaik memanggil Regex::new untuk pola yang kamu pakai di setiap baris file besar?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Dokumentasi crate
regex— docs.rs (bahasa Inggris): panduan resmi, dengan banyak contoh dan referensi sintaks lengkap. - API
Regex— docs.rs (bahasa Inggris): setiap method, termasukcaptures_iter,replacen, dansplit. LazyLock— dokumentasi standard library (bahasa Inggris): cara kerja static yang diinisialisasi secara lazy.- Regular Expressions — Rust Cookbook (bahasa Inggris): resep singkat untuk mengambil hashtag, nomor telepon, email, dan lainnya.