Regex #
Regular expression (regex) adalah bahasa mini untuk mendeskripsikan pola dalam teks. Dengan regex kamu bisa menemukan, mengekstrak, memvalidasi, dan mengganti teks berdasarkan pola yang sangat fleksibel — jauh lebih kuat dari metode string biasa seperti split() atau startswith(). Python menyediakan modul re di stdlib untuk semua operasi regex. Yang membuat regex kuat sekaligus tricky adalah bahwa pola yang sama bisa berarti banyak hal tergantung konteks — greedy vs non-greedy, anchored vs floating, capturing vs non-capturing. Artikel ini membahas semua konsep penting regex di Python dengan contoh nyata.
Raw String untuk Pola Regex #
Sebelum mulai menulis pola, penting dipahami mengapa regex selalu menggunakan raw string r"...":
import re
# ANTI-PATTERN: string biasa — backslash diinterpretasi Python lebih dulu
pola = "\d+" # Python menginterpretasi \d sebagai d (escape tidak dikenal)
# pola sebenarnya hanya "d+"
# BENAR: raw string — backslash tidak diinterpretasi Python
pola = r"\d+" # \d diteruskan ke regex engine sebagai-adalah
pola = r"\b\w+\b" # word boundary + word chars + word boundary
# Tanpa raw string kamu harus double-escape semua backslash
pola_tanpa_raw = "\\d+" # sama dengan r"\d+" tapi lebih sulit dibaca
Karakter dan Kelas Karakter #
# Karakter literal — cocokkan persis
re.findall(r"cat", "the cat sat on the mat") # → ['cat']
# . (titik) — cocokkan SEMBARANG karakter kecuali newline
re.findall(r"c.t", "cat cut c4t c\nt") # → ['cat', 'cut', 'c4t']
# Kelas karakter [...] — cocokkan SALAH SATU karakter dalam set
re.findall(r"[aeiou]", "Python") # → ['o']
re.findall(r"[a-z]", "Hello World") # → ['e', 'l', 'l', 'o', 'o', 'r', 'l', 'd']
re.findall(r"[A-Z]", "Hello World") # → ['H', 'W']
re.findall(r"[0-9]", "abc123def") # → ['1', '2', '3']
re.findall(r"[a-zA-Z0-9]", "Hi! 42") # → ['H', 'i', '4', '2']
# [^...] — cocokkan karakter yang TIDAK ada dalam set
re.findall(r"[^aeiou]", "Python") # → ['P', 'y', 't', 'h', 'n']
re.findall(r"[^0-9]", "abc123") # → ['a', 'b', 'c']
Shorthand Character Classes #
Shorthand │ Setara dengan │ Arti
──────────┼─────────────────────┼──────────────────────────────
\d │ [0-9] │ digit
\D │ [^0-9] │ bukan digit
\w │ [a-zA-Z0-9_] │ word character (alphanumeric + _)
\W │ [^a-zA-Z0-9_] │ bukan word character
\s │ [ \t\n\r\f\v] │ whitespace (spasi, tab, newline)
\S │ [^ \t\n\r\f\v] │ bukan whitespace
teks = "Budi123 beli 5 apel"
print(re.findall(r"\d+", teks)) # → ['123', '5']
print(re.findall(r"\w+", teks)) # → ['Budi123', 'beli', '5', 'apel']
print(re.findall(r"\s+", teks)) # → [' ', ' ', ' ']
Quantifier — Mengatur Jumlah Kemunculan #
teks = "aababaabbb"
# * — 0 atau lebih
re.findall(r"ab*", teks) # → ['a', 'ab', 'a', 'ab', 'a', 'ab', 'abbb'] — hmm
# + — 1 atau lebih
re.findall(r"\d+", "a1b22c333") # → ['1', '22', '333']
# ? — 0 atau 1 (opsional)
re.findall(r"colou?r", "color colour") # → ['color', 'colour']
# {n} — tepat n kali
re.findall(r"\d{3}", "12 123 1234") # → ['123', '123'] (dari 1234)
# {n,m} — antara n dan m kali
re.findall(r"\d{2,4}", "1 12 123 1234 12345") # → ['12', '123', '1234', '1234']
# {n,} — n atau lebih
re.findall(r"\d{3,}", "12 123 1234 12345") # → ['123', '1234', '12345']
Greedy vs Non-Greedy #
Secara default, quantifier bersifat greedy — mencocokkan sebanyak mungkin karakter. Tambahkan ? setelah quantifier untuk membuatnya non-greedy (mencocokkan sesedikit mungkin):
html = "<b>tebal</b> dan <i>miring</i>"
# Greedy — mencocokkan sebanyak mungkin
print(re.findall(r"<.+>", html))
# → ['<b>tebal</b> dan <i>miring</i>'] ← satu kecocokan besar
# Non-greedy — mencocokkan sesedikit mungkin
print(re.findall(r"<.+?>", html))
# → ['<b>', '</b>', '<i>', '</i>'] ← setiap tag terpisah
# Contoh lain
teks = '"apel", "jeruk", "mangga"'
# Greedy — dari kutip pertama sampai kutip TERAKHIR
re.findall(r'".*"', teks) # → ['"apel", "jeruk", "mangga"']
# Non-greedy — setiap kata dalam kutip
re.findall(r'".*?"', teks) # → ['"apel"', '"jeruk"', '"mangga"']
Anchor — Posisi dalam String #
# ^ — awal string (atau awal baris jika re.MULTILINE)
re.match(r"^Hello", "Hello World") # → match
re.match(r"^World", "Hello World") # → None
# $ — akhir string (atau akhir baris jika re.MULTILINE)
re.search(r"World$", "Hello World") # → match
re.search(r"Hello$", "Hello World") # → None
# \b — word boundary (batas antara \w dan \W)
print(re.findall(r"\bcat\b", "the cat scattered concatenate"))
# → ['cat'] ← hanya kata "cat" terpisah, bukan "cat" dalam kata lain
print(re.findall(r"cat", "the cat scattered concatenate"))
# → ['cat', 'cat', 'cat'] ← mencocokkan di dalam kata juga
# \A — awal string (lebih ketat dari ^, tidak terpengaruh MULTILINE)
# \Z — akhir string (lebih ketat dari $)
re.match(r"\APython", "Python is great") # → match
Fungsi-Fungsi re
#
re.match() vs re.search()
#
import re
teks = "Budi Santoso berumur 28 tahun"
# match() — hanya mencocokkan di AWAL string
print(re.match(r"\d+", teks)) # → None (string tidak dimulai angka)
print(re.match(r"Budi", teks)) # → match
# search() — mencari di MANA SAJA dalam string
print(re.search(r"\d+", teks)) # → match (28 ditemukan)
print(re.search(r"\d+", teks).group()) # → '28'
# ANTI-PATTERN: menggunakan match() ketika harusnya search()
# match() hanya cocok untuk validasi pola di awal string
re.findall() dan re.finditer()
#
teks = "Email: [email protected], [email protected], [email protected]"
# findall() — kembalikan semua kecocokan sebagai list string
email_list = re.findall(r"\b[\w.-]+@[\w.-]+\.\w+\b", teks)
print(email_list)
# → ['[email protected]', '[email protected]', '[email protected]']
# finditer() — kembalikan iterator Match object (lebih efisien untuk data besar)
for m in re.finditer(r"\b[\w.-]+@[\w.-]+\.\w+\b", teks):
print(f" Email: {m.group()}, posisi: {m.start()}–{m.end()}")
# findall dengan grup () — kembalikan list of tuples jika ada grup
teks = "Nama: Budi (28), Ani (32), Citra (25)"
hasil = re.findall(r"(\w+) \((\d+)\)", teks)
print(hasil) # → [('Budi', '28'), ('Ani', '32'), ('Citra', '25')]
re.sub() — Ganti Pola
#
teks = "Harga: Rp 25.000 dan Rp 150.000"
# Ganti angka dengan placeholder
print(re.sub(r"\d+", "X", teks))
# → Harga: Rp X.X dan Rp X.X
# Batas penggantian dengan count
print(re.sub(r"\d+", "X", teks, count=2))
# → Harga: Rp X.X dan Rp 150.000
# Gunakan grup dalam replacement — \1, \2 untuk merujuk grup
nama = "Santoso, Budi"
balik = re.sub(r"(\w+), (\w+)", r"\2 \1", nama)
print(balik) # → Budi Santoso
# Gunakan fungsi sebagai replacement
def ke_upper(m):
return m.group().upper()
print(re.sub(r"\b\w{5,}\b", ke_upper, "Python adalah bahasa yang hebat"))
# → PYTHON adalah BAHASA yang HEBAT (hanya kata ≥5 huruf)
re.split() — Pecah String
#
# Split berdasarkan whitespace (satu atau lebih)
print(re.split(r"\s+", " satu dua tiga "))
# → ['', 'satu', 'dua', 'tiga', '']
# Split berdasarkan beberapa delimiter sekaligus
teks = "apel,jeruk;mangga|pisang"
print(re.split(r"[,;|]", teks))
# → ['apel', 'jeruk', 'mangga', 'pisang']
# Pertahankan delimiter dalam hasil (gunakan grup)
print(re.split(r"([,;|])", teks))
# → ['apel', ',', 'jeruk', ';', 'mangga', '|', 'pisang']
Grup Penangkap (Capturing Groups) #
Grup memungkinkan mengekstrak bagian tertentu dari kecocokan:
# Grup dasar dengan ()
pola = r"(\d{4})-(\d{2})-(\d{2})"
teks = "Tanggal: 2024-08-17"
m = re.search(pola, teks)
if m:
print(m.group()) # → 2024-08-17 (keseluruhan)
print(m.group(1)) # → 2024 (grup 1)
print(m.group(2)) # → 08 (grup 2)
print(m.group(3)) # → 17 (grup 3)
print(m.groups()) # → ('2024', '08', '17') (semua grup)
Named Groups — Grup dengan Nama #
Named groups membuat pola lebih mudah dibaca dan digunakan:
# (?P<nama>...) — grup dengan nama
pola = r"(?P<tahun>\d{4})-(?P<bulan>\d{2})-(?P<hari>\d{2})"
teks = "2024-08-17"
m = re.match(pola, teks)
if m:
print(m.group("tahun")) # → 2024
print(m.group("bulan")) # → 08
print(m.group("hari")) # → 17
print(m.groupdict()) # → {'tahun': '2024', 'bulan': '08', 'hari': '17'}
# Named group dalam sub() — merujuk dengan \g<nama>
log = "2024-08-17 09:30:45 - INFO - Server started"
pola = r"(?P<tanggal>\d{4}-\d{2}-\d{2}) (?P<waktu>\d{2}:\d{2}:\d{2})"
hasil = re.sub(pola, r"[\g<tanggal> \g<waktu>]", log)
print(hasil) # → [2024-08-17 09:30:45] - INFO - Server started
Non-Capturing Groups #
# (?:...) — grup untuk pengelompokan tapi TIDAK ditangkap
pola = r"(?:https?|ftp)://(\w+\.\w+)"
teks = "Kunjungi https://python.org atau http://docs.python.org"
# findall dengan non-capturing group hanya kembalikan grup yang ditangkap
hasil = re.findall(pola, teks)
print(hasil) # → ['python.org', 'docs.python.org']
# Protokol (https/http) tidak tertangkap karena (?:...)
Lookahead dan Lookbehind #
Lookahead dan lookbehind mencocokkan posisi berdasarkan apa yang ada di sekitarnya — tanpa menyertakan teks tersebut dalam kecocokan:
# Lookahead positif (?=...) — cocokkan jika DIIKUTI oleh pola
harga = "Rp 25000 USD 50 EUR 75"
# Angka yang diikuti " USD"
print(re.findall(r"\d+(?= USD)", harga)) # → ['50']
# Lookahead negatif (?!...) — cocokkan jika TIDAK diikuti oleh pola
print(re.findall(r"\d+(?! USD)", harga)) # → ['25000', '75'] (bukan angka USD)
# Lookbehind positif (?<=...) — cocokkan jika DIDAHULUI oleh pola
# Angka yang didahului "Rp "
print(re.findall(r"(?<=Rp )\d+", harga)) # → ['25000']
# Lookbehind negatif (?<!...) — cocokkan jika TIDAK didahului oleh pola
print(re.findall(r"(?<!Rp )\d+", harga)) # → ['50', '75']
# Contoh praktis: extract nilai dari password requirements
# Temukan kata yang mengandung huruf besar DAN angka
kata_list = ["Password1", "password", "PASSWORD", "Pass123", "123456"]
def punya_huruf_besar_dan_angka(kata):
return bool(re.search(r"(?=.*[A-Z])(?=.*\d)", kata))
for kata in kata_list:
print(f"{kata}: {punya_huruf_besar_dan_angka(kata)}")
# → Password1: True
# → password: False
# → PASSWORD: False
# → Pass123: True
# → 123456: False
re.compile() — Kompilasi Pola untuk Performa
#
Jika kamu menggunakan pola yang sama berulang kali, kompilasi dulu dengan re.compile():
import re
# ANTI-PATTERN: compile ulang setiap iterasi — lambat untuk data besar
teks_list = [...]
for teks in teks_list:
hasil = re.findall(r"\b[\w.-]+@[\w.-]+\.\w+\b", teks)
# BENAR: compile sekali, gunakan berkali-kali
POLA_EMAIL = re.compile(r"\b[\w.-]+@[\w.-]+\.\w+\b")
for teks in teks_list:
hasil = POLA_EMAIL.findall(teks) # menggunakan compiled pattern
# Semua fungsi re tersedia sebagai metode pada compiled object
m = POLA_EMAIL.search("Kirim ke [email protected] ya")
if m:
print(m.group())
# Manfaat lain compile: bisa tambahkan komentar dengan re.VERBOSE
POLA_TELEPON = re.compile(r"""
(?:\+62|0) # kode negara atau 0
[\s-]? # opsional spasi atau dash
(?:8\d{2}|21) # kode area: 8xx (HP) atau 21 (Jakarta)
[\s-]? # opsional separator
\d{3,4} # nomor bagian 1
[\s-]? # opsional separator
\d{3,4} # nomor bagian 2
""", re.VERBOSE)
Flags #
# re.IGNORECASE (re.I) — case insensitive
print(re.findall(r"python", "Python PYTHON python", re.I))
# → ['Python', 'PYTHON', 'python']
# re.MULTILINE (re.M) — ^ dan $ cocokkan awal/akhir setiap baris
teks_multi = "baris pertama\nbaris kedua\nbaris ketiga"
print(re.findall(r"^baris", teks_multi, re.M))
# → ['baris', 'baris', 'baris'] (tanpa M hanya 1)
# re.DOTALL (re.S) — . juga mencocokkan newline
teks = "awal\ntengah\nakhir"
print(re.search(r"awal.+akhir", teks)) # → None (. tidak cocok \n)
print(re.search(r"awal.+akhir", teks, re.S)) # → match
# Kombinasi flags
print(re.findall(r"^python", teks_multi, re.I | re.M))
# re.VERBOSE (re.X) — izinkan komentar dan whitespace dalam pola
POLA_EMAIL = re.compile(r"""
[\w.+-]+ # local part — huruf, angka, titik, plus, dash
@ # karakter @ literal
[\w-]+ # domain name
(?:\.[\w-]+)* # sub-domain (opsional, bisa lebih dari satu)
\.\w{2,} # TLD — minimal 2 karakter
""", re.VERBOSE)
Pola Validasi Umum #
import re
# Email sederhana
POLA_EMAIL = re.compile(r"^[\w.+-]+@[\w-]+\.\w{2,}$")
# Nomor telepon Indonesia
POLA_TELEPON_ID = re.compile(r"^(?:\+62|0)[2-9]\d{7,11}$")
# URL
POLA_URL = re.compile(
r"https?://" # protokol
r"(?:[\w-]+\.)*" # subdomain opsional
r"[\w-]+\.\w{2,}" # domain + TLD
r"(?:/[^\s]*)?" # path opsional
)
# NIK Indonesia (16 digit)
POLA_NIK = re.compile(r"^\d{16}$")
# Kode pos Indonesia (5 digit)
POLA_KODEPOS = re.compile(r"^\d{5}$")
# Tanggal format YYYY-MM-DD (validasi format saja, bukan kalender)
POLA_TANGGAL = re.compile(
r"^\d{4}" # tahun
r"-(0[1-9]|1[0-2])" # bulan 01–12
r"-(0[1-9]|[12]\d|3[01])$" # hari 01–31
)
# Penggunaan
def validasi_email(email: str) -> bool:
return bool(POLA_EMAIL.match(email))
def validasi_telepon(telepon: str) -> bool:
# Normalisasi dulu — hapus spasi dan dash
telepon_bersih = re.sub(r"[\s-]", "", telepon)
return bool(POLA_TELEPON_ID.match(telepon_bersih))
print(validasi_email("[email protected]")) # → True
print(validasi_email("bukan-email")) # → False
print(validasi_telepon("+62 812-3456-7890")) # → True
print(validasi_telepon("1234")) # → False
Kapan Tidak Menggunakan Regex #
Regex sangat powerful tapi bukan selalu solusi terbaik:
# Gunakan str methods jika pola sederhana dan literal
teks = "Hello, World!"
# ANTI-PATTERN: regex untuk operasi sederhana
if re.match(r"^Hello", teks): # berlebihan
pass
if re.search(r"World", teks): # berlebihan
pass
# BENAR: str methods lebih cepat dan lebih mudah dibaca
if teks.startswith("Hello"):
pass
if "World" in teks:
pass
# Jangan parse HTML/XML dengan regex — gunakan parser yang tepat
# ANTI-PATTERN:
judul = re.findall(r"<title>(.*?)</title>", html) # rapuh dan terbatas
# BENAR: gunakan BeautifulSoup atau lxml
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
judul = soup.find("title").text
Gunakan regex jika:
✓ Pola tidak bisa diungkapkan dengan operasi string biasa
✓ Butuh ekstrak beberapa bagian sekaligus dari teks
✓ Pola melibatkan variasi (opsional, pengulangan, alternatif)
✓ Validasi format kompleks (email, URL, NIK)
Gunakan str methods jika:
✓ Cek prefix/suffix: startswith(), endswith()
✓ Cek keberadaan: 'x' in teks
✓ Ganti substring tetap: teks.replace("lama", "baru")
✓ Pecah berdasarkan delimiter tetap: teks.split(",")
✓ Menghapus whitespace: teks.strip()
Ringkasan #
- Selalu gunakan raw string
r"..."untuk pola regex — mencegah Python menginterpretasi backslash sebelum sampai ke regex engine.re.search()bukanre.match()untuk pencarian di dalam string —match()hanya mencocokkan di awal string.- Greedy vs non-greedy — tambahkan
?setelah quantifier (+?,*?) untuk mencocokkan sesedikit mungkin karakter.- Named groups
(?P<nama>...)membuat pola lebih mudah dibaca dan hasilnya bisa diakses via nama, bukan indeks.- Non-capturing group
(?:...)untuk pengelompokan tanpa menangkap — tidak mempengaruhi hasilfindall()dangroups().re.compile()untuk pola yang digunakan berulang kali — menghindari kompilasi ulang di setiap pemanggilan.re.VERBOSEuntuk pola panjang — izinkan whitespace dan komentar di dalam pola untuk keterbacaan.- Lookahead/lookbehind untuk mencocokkan berdasarkan konteks sekitar tanpa menyertakan konteks tersebut dalam kecocokan.
- Jangan gunakan regex untuk operasi string sederhana —
startswith(),in,split(), danreplace()lebih cepat dan lebih mudah dibaca.- Jangan parse HTML/XML dengan regex — gunakan BeautifulSoup atau lxml yang tahu struktur dokumen.