Regex

Regex #

Regex di Rust tidak ada di standard library — kamu membutuhkan crate regex. Tapi berbeda dari implementasi regex di banyak bahasa, crate regex Rust didesain dengan jaminan keamanan yang kuat: ia hanya mendukung regular expressions yang dijamin berjalan dalam waktu linear (O(n)) terhadap panjang input. Ini berarti tidak ada backtracking eksponensial — celah keamanan yang terkenal di implementasi regex di bahasa lain. Tradeoff-nya: beberapa fitur regex seperti lookahead, lookbehind, dan backreference tidak tersedia. Artikel ini membahas semua yang kamu butuhkan: dari sintaks dasar hingga named capture groups, penggantian dengan closure, kompilasi efisien dengan lazy_static, dan pola validasi yang siap pakai.

Instalasi #

# Cargo.toml
[dependencies]
regex = "1"

# Untuk kompilasi sekali lintas pemanggilan (direkomendasikan):
lazy_static = "1"
# atau gunakan std::sync::OnceLock (Rust 1.70+, tanpa dependensi tambahan)

Sintaks Pola Regex #

Sebelum masuk ke kode, penting memahami pola dasar yang tersedia. Crate regex menggunakan sintaks yang kompatibel dengan RE2:

PolaMaknaContoh cocok
.Karakter apapun kecuali newlinea.c cocok abc, axc
\dDigit (0–9)\d+ cocok 123
\DBukan digit\D+ cocok abc
\wWord char (a-z, A-Z, 0-9, _)\w+ cocok hello_42
\WBukan word char\W+ cocok !@
\sWhitespace (spasi, tab, newline)\s+ cocok
\SBukan whitespace\S+ cocok kata
^Awal string (atau baris dengan (?m))^Hello
$Akhir string (atau baris dengan (?m))world$
*0 atau lebihab*c cocok ac, abc, abbc
+1 atau lebihab+c cocok abc, abbc
?0 atau 1ab?c cocok ac, abc
{n}Tepat n kali\d{4} cocok 2024
{n,m}n hingga m kali\d{2,4} cocok 12, 1234
[abc]Salah satu dari a, b, c[aeiou] cocok vokal
[^abc]Bukan a, b, atau c[^0-9] bukan digit
(abc)Capture group(\d+) tangkap angka
(?P<nama>...)Named capture group(?P<tahun>\d{4})
a|ba atau bkucing|anjing
(?i)Case-insensitive(?i)hello cocok HELLO
(?m)Multiline (^ dan $ per baris)(?m)^kata
(?s)Single-line (. cocok newline)(?s).+

Operasi Dasar #

is_match — Cek Kecocokan #

use regex::Regex;

fn main() {
    let pola_angka = Regex::new(r"\d+").unwrap();
    let pola_email = Regex::new(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$").unwrap();

    // is_match — hanya cek ada/tidak
    println!("{}", pola_angka.is_match("ada 42 di sini"));  // true
    println!("{}", pola_angka.is_match("tidak ada angka")); // false

    // Case-insensitive
    let pola_ci = Regex::new(r"(?i)hello").unwrap();
    println!("{}", pola_ci.is_match("HELLO WORLD"));  // true
    println!("{}", pola_ci.is_match("Hello Rust"));   // true

    // Validasi email sederhana
    let alamat_valid = "[email protected]";
    let alamat_tidak_valid = "bukan-email";
    println!("'{}' valid: {}", alamat_valid, pola_email.is_match(alamat_valid));
    println!("'{}' valid: {}", alamat_tidak_valid, pola_email.is_match(alamat_tidak_valid));
}

find — Temukan Kecocokan Pertama #

use regex::Regex;

fn main() {
    let re = Regex::new(r"\d+").unwrap();
    let teks = "Harga: 25000 rupiah, diskon 10 persen";

    // find — temukan kecocokan pertama
    match re.find(teks) {
        Some(m) => {
            println!("Pertama ditemukan: '{}'", m.as_str());  // "25000"
            println!("Posisi: {}..{}", m.start(), m.end());   // 7..12
        }
        None => println!("Tidak ditemukan"),
    }

    // find dengan posisi awal tertentu
    // (gunakan find pada slice)
    if let Some(m) = re.find(&teks[15..]) {
        println!("Setelah posisi 15: '{}'", m.as_str());  // "10"
    }
}

captures — Ekstrak Bagian Spesifik #

Capture groups memungkinkan kamu mengekstrak bagian tertentu dari teks yang cocok:

use regex::Regex;

fn main() {
    // Numbered capture groups
    let re_tanggal = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let teks = "Tanggal lahir: 1995-08-24";

    if let Some(caps) = re_tanggal.captures(teks) {
        // caps[0] = keseluruhan match, caps[1..] = group pertama dst.
        println!("Keseluruhan: {}", &caps[0]);  // "1995-08-24"
        println!("Tahun: {}", &caps[1]);         // "1995"
        println!("Bulan: {}", &caps[2]);         // "08"
        println!("Hari: {}", &caps[3]);           // "24"
    }

    // Named capture groups — lebih mudah dibaca
    let re_url = Regex::new(
        r"(?P<protokol>https?)://(?P<domain>[\w.-]+)(?P<path>/[\w./]*)?",
    ).unwrap();

    let url = "https://www.contoh.com/artikel/rust";

    if let Some(caps) = re_url.captures(url) {
        println!("Protokol: {}", &caps["protokol"]);  // "https"
        println!("Domain: {}", &caps["domain"]);       // "www.contoh.com"

        // Named group yang opsional — gunakan get() bukan indeks langsung
        match caps.name("path") {
            Some(p) => println!("Path: {}", p.as_str()),  // "/artikel/rust"
            None => println!("Tidak ada path"),
        }
    }
}

Iterasi Semua Kecocokan #

find_iter — Iterasi Semua Match #

use regex::Regex;

fn main() {
    let re = Regex::new(r"\d+").unwrap();
    let teks = "Ada 3 kucing, 12 anjing, dan 7 burung di kebun itu";

    // Kumpulkan semua angka
    let angka: Vec<&str> = re.find_iter(teks)
        .map(|m| m.as_str())
        .collect();
    println!("Semua angka: {:?}", angka);  // ["3", "12", "7"]

    // Jumlahkan semua angka
    let total: u32 = re.find_iter(teks)
        .filter_map(|m| m.as_str().parse().ok())
        .sum();
    println!("Total: {}", total);  // 22

    // Dengan posisi
    for m in re.find_iter(teks) {
        println!("'{}' di posisi {}", m.as_str(), m.start());
    }
}

captures_iter — Iterasi Semua Capture Groups #

use regex::Regex;

fn main() {
    // Ekstrak semua pasangan kunci=nilai dari konfigurasi
    let re = Regex::new(r"(?P<kunci>\w+)\s*=\s*(?P<nilai>[^\n]+)").unwrap();
    let config = "
        host = localhost
        port = 8080
        debug = true
        nama_app = Rust App
    ";

    let mut konfigurasi = std::collections::HashMap::new();
    for caps in re.captures_iter(config) {
        let kunci = caps["kunci"].trim().to_string();
        let nilai = caps["nilai"].trim().to_string();
        konfigurasi.insert(kunci, nilai);
    }

    println!("{:#?}", konfigurasi);

    // Ekstrak semua tanggal dari teks
    let re_tgl = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let log = "Event 2024-01-15: login. Event 2024-03-22: logout. Event 2024-07-10: update.";

    for caps in re_tgl.captures_iter(log) {
        println!("Tanggal: {}/{}/{}", &caps[3], &caps[2], &caps[1]);
    }
}

Penggantian Teks #

replace dan replace_all #

use regex::Regex;

fn main() {
    // replace — ganti kecocokan pertama
    let re = Regex::new(r"\d+").unwrap();
    let hasil = re.replace("ada 42 apel dan 7 jeruk", "N");
    println!("{}", hasil);  // "ada N apel dan 7 jeruk"

    // replace_all — ganti semua kecocokan
    let hasil_semua = re.replace_all("ada 42 apel dan 7 jeruk", "N");
    println!("{}", hasil_semua);  // "ada N apel dan 7 jeruk"

    // Penggantian dengan capture group ($1, $2, ...)
    let re_tgl = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let teks = "Lahir: 1995-08-24, Menikah: 2020-06-15";

    // Ubah dari YYYY-MM-DD ke DD/MM/YYYY
    let hasil_tgl = re_tgl.replace_all(teks, "$3/$2/$1");
    println!("{}", hasil_tgl);  // "Lahir: 24/08/1995, Menikah: 15/06/2020"

    // Named groups dalam penggantian
    let re_nama = Regex::new(r"(?P<depan>\w+)\s+(?P<belakang>\w+)").unwrap();
    let nama = "Budi Santoso";
    let dibalik = re_nama.replace(nama, "$belakang, $depan");
    println!("{}", dibalik);  // "Santoso, Budi"
}

replace_all dengan Closure — Penggantian Dinamis #

Closure memungkinkan penggantian yang dihitung berdasarkan konten match:

use regex::Regex;

fn main() {
    // Mask semua nomor kartu kredit (tunjukkan hanya 4 digit terakhir)
    let re_cc = Regex::new(r"\b(\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?)(\d{4})\b").unwrap();
    let teks = "Kartu: 1234 5678 9012 3456 dan 9876-5432-1098-7654";

    let masked = re_cc.replace_all(teks, |caps: &regex::Captures| {
        format!("****-****-****-{}", &caps[2])
    });
    println!("{}", masked);

    // Ubah setiap kata menjadi title case
    let re_kata = Regex::new(r"\b\w+\b").unwrap();
    let kalimat = "halo dunia dari rust";

    let title_case = re_kata.replace_all(kalimat, |caps: &regex::Captures| {
        let kata = &caps[0];
        let mut chars = kata.chars();
        match chars.next() {
            None => String::new(),
            Some(c) => c.to_uppercase().to_string() + chars.as_str(),
        }
    });
    println!("{}", title_case);  // "Halo Dunia Dari Rust"

    // Naikkan semua angka sebesar 10
    let re_angka = Regex::new(r"\d+").unwrap();
    let data = "Item A: 5, Item B: 12, Item C: 3";

    let naik = re_angka.replace_all(data, |caps: &regex::Captures| {
        let n: u32 = caps[0].parse().unwrap();
        (n + 10).to_string()
    });
    println!("{}", naik);  // "Item A: 15, Item B: 22, Item C: 13"
}

Kompilasi Efisien — Jangan Kompilasi dalam Loop #

Kompilasi Regex adalah operasi yang relatif mahal. Mengkompilasi pola yang sama berulang kali di dalam loop atau fungsi yang dipanggil berkali-kali adalah anti-pattern yang sering terlewat:

use regex::Regex;

// ANTI-PATTERN: kompilasi setiap kali fungsi dipanggil
fn validasi_email_buruk(email: &str) -> bool {
    let re = Regex::new(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$").unwrap();
    re.is_match(email)
}

// BENAR dengan lazy_static — kompilasi sekali, gunakan selamanya
use lazy_static::lazy_static;

lazy_static! {
    static ref RE_EMAIL: Regex = Regex::new(
        r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
    ).unwrap();

    static ref RE_TELEPON: Regex = Regex::new(
        r"^(\+62|62|0)8[1-9]\d{6,10}$"
    ).unwrap();
}

fn validasi_email(email: &str) -> bool {
    RE_EMAIL.is_match(email)
}

fn validasi_telepon(nomor: &str) -> bool {
    RE_TELEPON.is_match(nomor)
}

// Alternatif dengan std::sync::OnceLock (Rust 1.70+, tanpa lazy_static)
use std::sync::OnceLock;

fn re_tanggal() -> &'static Regex {
    static RE: OnceLock<Regex> = OnceLock::new();
    RE.get_or_init(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap())
}

fn main() {
    // Validasi banyak email — RE_EMAIL hanya dikompilasi sekali
    let emails = [
        "[email protected]",
        "tidak-valid",
        "[email protected]",
        "joko@",
    ];

    for email in &emails {
        println!("{}: {}", email, validasi_email(email));
    }

    // Validasi nomor telepon Indonesia
    let nomor = ["081234567890", "62812345678", "+6281234567890", "12345"];
    for n in &nomor {
        println!("{}: {}", n, validasi_telepon(n));
    }

    // Validasi tanggal dengan OnceLock
    println!("2024-08-24 valid: {}", re_tanggal().is_match("2024-08-24"));
    println!("24-08-2024 valid: {}", re_tanggal().is_match("24-08-2024"));
}

RegexSet — Cocokkan Banyak Pola Sekaligus #

RegexSet mencocokkan satu string terhadap banyak pola dalam satu operasi — jauh lebih efisien dari mengecek satu per satu:

use regex::RegexSet;

fn main() {
    // Klasifikasi tipe konten berdasarkan pola URL/ekstensi
    let set = RegexSet::new(&[
        r"\.jpg$|\.jpeg$|\.png$|\.gif$|\.webp$",  // 0: gambar
        r"\.mp4$|\.avi$|\.mov$|\.mkv$",            // 1: video
        r"\.mp3$|\.wav$|\.ogg$|\.flac$",           // 2: audio
        r"\.pdf$|\.doc$|\.docx$|\.xlsx$",          // 3: dokumen
        r"\.rs$|\.py$|\.js$|\.go$",                // 4: kode
    ]).unwrap();

    let nama_label = ["Gambar", "Video", "Audio", "Dokumen", "Kode"];

    let file_list = [
        "foto.jpg",
        "video.mp4",
        "lagu.mp3",
        "laporan.pdf",
        "main.rs",
        "tidak-dikenal.xyz",
    ];

    for file in &file_list {
        let cocok: Vec<&str> = set.matches(file)
            .iter()
            .map(|i| nama_label[i])
            .collect();

        if cocok.is_empty() {
            println!("{}: tidak dikenal", file);
        } else {
            println!("{}: {}", file, cocok.join(", "));
        }
    }
}

RegexBuilder — Konfigurasi Lanjutan #

RegexBuilder memungkinkan mengkonfigurasi berbagai opsi sebelum mengkompilasi regex:

use regex::RegexBuilder;

fn main() {
    // Case-insensitive
    let re_ci = RegexBuilder::new(r"hello world")
        .case_insensitive(true)
        .build()
        .unwrap();

    println!("{}", re_ci.is_match("HELLO WORLD"));  // true
    println!("{}", re_ci.is_match("Hello World"));  // true

    // Multiline — ^ dan $ berlaku per baris
    let re_ml = RegexBuilder::new(r"^\w+")
        .multi_line(true)
        .build()
        .unwrap();

    let teks_ml = "baris pertama\nbaris kedua\nbaris ketiga";
    for m in re_ml.find_iter(teks_ml) {
        println!("Match: '{}'", m.as_str());
        // "baris", "baris", "baris" — awal setiap baris
    }

    // Dot cocok newline
    let re_dot = RegexBuilder::new(r"Mulai.*Selesai")
        .dot_matches_new_line(true)
        .build()
        .unwrap();

    let teks_ml2 = "Mulai\nbaris tengah\nSelesai";
    println!("Match multiline: {}", re_dot.is_match(teks_ml2));  // true

    // Batasi ukuran untuk melindungi dari ReDoS
    let re_terbatas = RegexBuilder::new(r"\w+")
        .size_limit(1024 * 1024)  // maks 1MB untuk bytecode regex
        .build()
        .unwrap();
}

Pola Validasi Siap Pakai #

Kumpulan pola validasi umum yang bisa langsung digunakan:

use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
    // Email (sederhana — untuk validasi ketat gunakan library khusus)
    static ref RE_EMAIL: Regex = Regex::new(
        r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$"
    ).unwrap();

    // Nomor telepon Indonesia
    static ref RE_TELEPON_ID: Regex = Regex::new(
        r"^(\+62|62|0)(21|22|24|31|274|361|411|751|778|811|812|813|821|822|823|851|852|853|855|856|857|858|859|877|878|896|897|898|899)\d{5,10}$"
    ).unwrap();

    // Kode pos Indonesia (5 digit)
    static ref RE_KODEPOS: Regex = Regex::new(
        r"^\d{5}$"
    ).unwrap();

    // NIK (16 digit)
    static ref RE_NIK: Regex = Regex::new(
        r"^\d{16}$"
    ).unwrap();

    // URL dengan protokol
    static ref RE_URL: Regex = Regex::new(
        r"^https?://[\w\-]+(\.[\w\-]+)+([\w\-\._~:/?#\[\]@!\$&'\(\)\*\+,;=%]+)?$"
    ).unwrap();

    // Format tanggal YYYY-MM-DD
    static ref RE_TANGGAL_ISO: Regex = Regex::new(
        r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$"
    ).unwrap();

    // Hanya huruf dan spasi (nama orang)
    static ref RE_NAMA: Regex = Regex::new(
        r"^[a-zA-Z\s'-]{2,100}$"
    ).unwrap();
}

fn main() {
    let data_uji = [
        ("Email valid",     RE_EMAIL.is_match("[email protected]")),
        ("Email tidak valid", RE_EMAIL.is_match("bukan@email")),
        ("Kode pos valid",  RE_KODEPOS.is_match("40115")),
        ("Kode pos tidak",  RE_KODEPOS.is_match("4011")),
        ("URL valid",       RE_URL.is_match("https://www.rust-lang.org")),
        ("Tanggal valid",   RE_TANGGAL_ISO.is_match("2024-08-24")),
        ("Tanggal tidak",   RE_TANGGAL_ISO.is_match("2024-13-45")),
    ];

    for (deskripsi, hasil) in &data_uji {
        println!("{}: {}", deskripsi, hasil);
    }
}

Ringkasan #

  • Raw string r"..." untuk pola regex — menghindari double-escape (\\d jadi \d). Selalu gunakan raw string untuk pola regex.
  • Kompilasi sekali, gunakan berkali-kaliRegex::new relatif mahal. Gunakan lazy_static! atau OnceLock untuk menyimpan Regex sebagai statis global, bukan mengkompilasi di dalam fungsi atau loop.
  • is_match untuk validasi, find untuk posisi, captures untuk ekstraksi — pilih method sesuai apa yang dibutuhkan; jangan gunakan captures jika hanya butuh is_match.
  • Named capture groups (?P<nama>...) lebih mudah dibaca — akses dengan &caps["nama"] daripada &caps[1] yang rentan error saat pola berubah.
  • replace_all dengan closure untuk penggantian dinamis — ketika teks pengganti perlu dihitung berdasarkan konten match, gunakan closure sebagai argumen replace_all.
  • RegexSet untuk klasifikasi — lebih efisien dari mengecek banyak pola satu per satu; cocok untuk routing, klasifikasi file, dan deteksi format.
  • Crate regex tidak mendukung lookahead/lookbehind — ini trade-off untuk jaminan O(n). Jika butuh fitur ini, pertimbangkan crate fancy-regex (dengan peringatan: bisa eksponensial).
  • Selalu tangani error kompilasiRegex::new mengembalikan Result; gunakan unwrap() hanya untuk pola yang sudah diverifikasi benar (seperti di lazy_static), atau tangani dengan match untuk pola yang datang dari input pengguna.

← Sebelumnya: Date & Time   Berikutnya: Crates →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact