Perilaku bawaan regex kadang terlalu kaku untuk kebutuhan nyata: pola python menolak Python, anchor ^ hanya mengenal satu awal string, dan pola panjang tidak bisa diberi komentar. Flags mengubah aturan main tersebut, sedangkan lookaround menambahkan pemeriksaan konteks — “cocok hanya bila sebelum posisi ini ada …” — tanpa mengonsumsi karakter. Chapter ini menutup kosakata regex dengan kedua alat tersebut.

Flags yang Mengubah Perilaku Pola #

FlagSingkatanEfek
re.IGNORECASEre.Ipola tidak peduli huruf besar atau kecil
re.MULTILINEre.M^ dan $ mengenali awal dan akhir setiap baris
re.DOTALLre.S. juga mencocokkan karakter baris baru
re.VERBOSEre.Xspasi dan komentar # di dalam pola diabaikan

Flag dilewatkan sebagai argument tambahan pada re.search() dan kawan-kawannya, atau sebagai argument kedua re.compile(). Flag re.IGNORECASE paling sederhana:

python
import re

teks = "Python python PYTHON"
print(re.findall(r"python", teks))
print(re.findall(r"python", teks, re.IGNORECASE))

Output yang Dihasilkan:

text
['python']
['Python', 'python', 'PYTHON']

Flag re.MULTILINE mengubah makna anchor ^ dan $ dari Chapter *Pola Dasar Regex`: keduanya kini mengenali tepi setiap baris, bukan hanya tepi string utuh:

python
import re

catatan = """judul laporan
isi laporan
penutup laporan"""

print(re.findall(r"^isi", catatan))
print(re.findall(r"^isi", catatan, re.MULTILINE))
print(re.findall(r"laporan$", catatan))
print(re.findall(r"laporan$", catatan, re.MULTILINE))

Output yang Dihasilkan:

text
[]
['isi']
['laporan']
['laporan', 'laporan', 'laporan']

Tanpa flag, pola ^isi gagal karena baris isi laporan tidak berada di awal string; kata laporan hanya dihitung sekali karena hanya baris terakhir yang menyentuh akhir string. Flag re.DOTALL melonggarkan satu larangan yang sudah kita saksikan di Chapter Pola Dasar Regex — . yang menolak baris baru:

python
import re

blok = "baris pertama\nbaris kedua"
print(re.findall(r"pertama.baris", blok))
print(re.findall(r"pertama.baris", blok, re.DOTALL))

Output yang Dihasilkan:

text
[]
['pertama\nbaris']

Pola yang Bisa Dibaca dengan re.VERBOSE #

Pola yang tumbuh panjang cepat menjadi tak terbaca. Flag re.VERBOSE menyuruh mesin mengabaikan spasi dan baris baru di dalam pola serta memperlakukan # sebagai awal komentar — pola bisa ditulis seperti kode yang terformat:

python
import re

pola_seri = re.compile(r"""
    ^S       # diawali huruf S
    \d{4}    # tepat empat digit
    $        # pola berakhir di sini
""", re.VERBOSE)

for kode in ["S1234", "S12345", "X1234"]:
    print(kode, "->", bool(pola_seri.fullmatch(kode)))

Output yang Dihasilkan:

text
S1234 -> True
S12345 -> False
X1234 -> False

Pada baris 3-7, setiap bagian pola menempati satu baris dengan komentarnya sendiri, dan flag re.VERBOSE diberikan sebagai argument kedua re.compile(). Tanpa flag tersebut, seluruh spasi di dalam pola akan dicocokkan secara literal dan karakter # akan dianggap karakter biasa. Loop pada baris 9-10 memperlihatkan hasilnya: S1234 lolos, S12345 ditolak karena \d{4} menuntut tepat empat digit, dan X1234 ditolak huruf pembukanya.

Lookaround: Memeriksa Tanpa Mengonsumsi #

Lookaround adalah pemeriksaan posisi yang tidak memakan karakter — sifat ini disebut zero-width. Bandingkan dua pola yang sama-sama mengambil angka setelah Rp :

python
import re

struk = "Total Rp 7500, diskon Rp 500"

m_biasa = re.search(r"Rp (\d+)", struk)
m_lookbehind = re.search(r"(?<=Rp )\d+", struk)
print("match utuh:", m_biasa.group(0), "| group 1:", m_biasa.group(1))
print("dengan lookbehind:", m_lookbehind.group(0))

Output yang Dihasilkan:

text
match utuh: Rp 7500 | group 1: 7500
dengan lookbehind: 7500

Pola pertama mengonsumsi Rp sebagai bagian dari kecocokan, sehingga angka harus diambil lewat capture group. Pola kedua memakai positive lookbehind (?<=Rp ): mesin hanya memeriksa bahwa tiga karakter Rp (huruf R, huruf p, satu spasi) berada tepat sebelum posisi saat ini, lalu mulai mencocokkan \d+ dari posisi itu — kecocokan berisi angka saja. Empat bentuk lookaround yang tersedia:

BentukNamaArti
(?=...)positive lookaheadcocok bila teks setelah posisi ini cocok dengan ...
(?!...)negative lookaheadcocok bila teks setelah posisi ini tidak cocok
(?<=...)positive lookbehindcocok bila teks sebelum posisi ini cocok
(?<!...)negative lookbehindcocok bila teks sebelum posisi ini tidak cocok

Karena tidak mengonsumsi karakter, lookahead bisa memfilter kecocokan tanpa ikut memakan konteksnya:

python
import re

belanja = "beras 5 kg, gula 2 kg, susu 1 liter"
print(re.findall(r"\d+(?= kg)", belanja))
print(re.findall(r"\d+(?! kg)", belanja))

Output yang Dihasilkan:

text
['5', '2']
['1']

Positive lookahead (?= kg) hanya menerima digit yang diikuti satuan kg — dan karena kg tidak ikut dikonsumsi, hasil kecocokannya murni angka. Negative lookahead (?! kg) membalik syarat tersebut dan justru menangkap digit 1 pada 1 liter. Pola serupa dengan arah sebaliknya memakai lookbehind:

python
import re

teks = "meja #3 dan kursi 7"
print(re.findall(r"(?<!#)\d+", teks))

Output yang Dihasilkan:

text
['7']

Digit 3 ditolak karena posisinya didahului karakter #; hanya 7 yang lolos.

Merangkai Lookahead: Validasi Password #

Kekuatan sesungguhnya dari lookaround: beberapa lookahead bisa ditempatkan berurutan di satu titik yang sama, sehingga satu pola memeriksa beberapa syarat sekaligus tanpa saling mengganggu. Contoh klasiknya adalah validasi password — wajib ada huruf kecil, huruf besar, digit, dan panjang minimal delapan karakter:

python
import re

pola_password = re.compile(r"""
    (?=.*[a-z])      # minimal satu huruf kecil
    (?=.*[A-Z])      # minimal satu huruf besar
    (?=.*\d)         # minimal satu digit
    .{8,}            # badan password: minimal 8 karakter
""", re.VERBOSE)

for kandidat in ["rahasia1A", "pe1A", "TANPA-ANGKA1", "tanpa-besar123"]:
    print(kandidat, "->", bool(pola_password.fullmatch(kandidat)))

Output yang Dihasilkan:

text
rahasia1A -> True
pe1A -> False
TANPA-ANGKA1 -> False
tanpa-besar123 -> False

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 3-6 (tiga lookahead): ketiganya dievaluasi di titik yang sama — awal string; masing-masing memindai ke depan lewat .* untuk mencari satu kategori karakter tanpa mengonsumsi satu karakter pun.
  2. Baris 7 (.{8,}): baru setelah ketiga syarat lulus, bagian pola ini mengonsumsi badan password dengan panjang minimal delapan karakter.
  3. Baris 10-11 (for kandidat in): rahasia1A memenuhi semua syarat; pe1A terlalu pendek; TANPA-ANGKA1 tanpa huruf kecil; tanpa-besar123 tanpa huruf besar.

Tiga jenis kegagalan berbeda tertangkap oleh satu pola. Tanpa lookahead, kita membutuhkan tiga pemanggilan re.search() terpisah — tiga pola yang harus dirawat satu per satu.