Chapter-chapter sebelumnya terus memperlengkapi kita dengan pola yang makin tajam; chapter ini justru mengajarkan menyarungkannya. Keterampilan yang membedakan pemrogram berpengalaman bukan kemampuan menulis regex paling rumit, melainkan kepekaan mengenali kapan regex adalah alat yang salah: saat sudah ada parser khusus untuk formatnya, saat method string lebih sederhana, atau saat pola berisiko membuat program berhenti merespons.
Mengurai HTML dengan Regex adalah Jebakan #
HTML memiliki struktur bersarang yang tidak bisa digambarkan oleh regex — secara formal, regex tidak mampu menghitung pasangan tag. Pola seperti <p class="harga">(.*)</p> tampak bekerja pada contoh pertama yang kita uji, lalu diam-diam gagal pada markup yang bermakna sama:
import re
html1 = '<p class="harga">Rp 150000</p>'
html2 = '<p id="utama" class="harga">Rp 150000</p>'
pola = r'<p class="harga">(.*)</p>'
print(re.search(pola, html1).group(1))
print(re.search(pola, html2))Output yang Dihasilkan:
Rp 150000
NonePenambahan satu attribute id — perubahan yang sah dan lazim — meruntuhkan pola. Melonggarkan pola menjadi <p.*>(.*)</p> justru menimbulkan masalah baru: .* yang greedy dari Chapter Character Classes dan Quantifiers akan melahap tag pembuka lain di tengahnya. Untuk HTML, gunakan parser. Standard library Python sudah menyertakan html.parser:
from html.parser import HTMLParser
class AmbilHarga(HTMLParser):
def __init__(self):
super().__init__()
self.dalam_harga = False
self.hasil = []
def handle_starttag(self, tag, attrs):
if tag == "p" and dict(attrs).get("class") == "harga":
self.dalam_harga = True
def handle_endtag(self, tag):
if tag == "p":
self.dalam_harga = False
def handle_data(self, data):
if self.dalam_harga:
self.hasil.append(data.strip())
parser = AmbilHarga()
parser.feed('<p id="utama" class="harga">Rp 150000</p>')
parser.feed('<p class="penjelasan">Diskon lebaran</p>')
print(parser.hasil)Output yang Dihasilkan:
['Rp 150000']Mari kita telaah bagian-bagian pentingnya:
- Baris 3 (
class AmbilHarga(HTMLParser)): class mewarisiHTMLParser; setiap kejadian markup memicu method yang sesuai secara otomatis. - Baris 9-11 (
handle_starttag): method dipanggil untuk setiap tag pembuka; parameterattrsberupa list pasangan nama-nilai attribute, dandict(attrs)mengubahnya menjadi dictionary agar mudah diperiksa. - Baris 17-19 (
handle_data): teks di antara tag dilaporkan lewat method ini; teks hanya dikumpulkan bila parser sedang berada di dalam<p>yang dimaksud. - Baris 21-24: object parser dibuat, dua dokumen kecil di-feed, dan hasilnya dicetak — attribute yang urutannya berbeda tidak lagi menjadi masalah.
Untuk proyek nyata dengan HTML dunia liar, pustaka pihak ketiga seperti BeautifulSoup dan lxml adalah pilihan standar industri — keduanya dibangun di atas parser sungguhan, bukan regex.
String Method Sering Lebih Cukup #
Banyak pekerjaan teks sehari-hari tidak butuh pola sama sekali. Method string dari Chapter String Methods lebih pendek, lebih cepat, dan langsung terbaca:
| Kebutuhan | Solusi tanpa regex | Solusi regex |
|---|---|---|
| Memeriksa awalan | teks.startswith("INV") | re.search(r"^INV", teks) |
| Memeriksa kehadiran teks tetap | "budi" in teks | re.search(r"budi", teks) |
| Memotong pemisah | teks.split(",") dan strip() | re.split(r"\s*,\s*", teks) |
| Mengganti teks tetap | teks.replace("lama", "baru") | re.sub(r"lama", "baru", teks) |
Perbandingan konkret pada satu baris data berpemisah koma:
import re
baris = "budi, 85, jakarta"
print([kolom.strip() for kolom in baris.split(",")])
print(re.split(r"\s*,\s*", baris))
print(baris.startswith("budi"))Output yang Dihasilkan:
['budi', '85', 'jakarta']
['budi', '85', 'jakarta']
TrueKedua pendekatan menghasilkan keluaran yang sama. Versi split() plus list comprehension dari Chapter Comprehensions dan Iterators tidak lebih panjang, tidak memuat pola yang harus didekodekan pembaca, dan tidak menanggung biaya mesin regex.
Format Terstruktur Butuh Parser #
Nomor HP dan baris log adalah teks semi-terstruktur — wilayah kerja regex. Begitu data memiliki format baku dengan aturan sintaksnya sendiri, gunakan parser resminya. Module json mengubah string JSON menjadi dictionary dan list sungguhan:
import json
profil = '{"nama": "Budi", "nilai": [85, 92, 78]}'
data = json.loads(profil)
print(data["nama"], "nilai terbaik:", max(data["nilai"]))Output yang Dihasilkan:
Budi nilai terbaik: 92Menulis pola regex untuk JSON yang benar nyaris mustahil: string JSON boleh berisi tanda kutip ter-escape, koma, dan kurung kurawal di dalam nilainya. Hal yang sama berlaku untuk format lain:
import tomllib
config_toml = """
[database]
host = "localhost"
port = 5432
"""
config = tomllib.loads(config_toml)
print(config["database"]["host"], "port", config["database"]["port"])Output yang Dihasilkan:
localhost port 5432Catastrophic Backtracking #
Ancaman paling serius dari regex yang salah rancang bukan hasil yang salah, melainkan waktu yang tidak pernah selesai. Pola (a+)+$ mengandung quantifier bersarang atas kelompok karakter yang sama: saat kecocokan gagal, mesin regex mencoba semua cara membagi deretan a menjadi grup-grup sebelum menyerah. Banyaknya cara pembagian berlipat dua setiap tambahan satu karakter:
def jumlah_cara_membagi(n):
return 2 ** (n - 1)
for n in (10, 20, 26):
print(f"n={n}: {jumlah_cara_membagi(n):,} kemungkinan")Output yang Dihasilkan:
n=10: 512 kemungkinan
n=20: 524,288 kemungkinan
n=26: 33,554,432 kemungkinanPerkiraan tersebut bukan teori. Mari kita ukur sungguhan — perhatikan karakter x di akhir input yang memaksa kegagalan total:
import re
import time
pola_berbahaya = re.compile(r"(a+)+$")
for n in (16, 20, 24):
teks = "a" * n + "x"
mulai = time.perf_counter()
pola_berbahaya.search(teks)
durasi = time.perf_counter() - mulai
print(f"n={n}: {durasi:.3f} detik")Output yang Dihasilkan (durasi berubah mengikuti kecepatan mesin Anda):
n=16: 0.008 detik
n=20: 0.131 detik
n=24: 2.093 detikDua puluh empat huruf membuat pencarian tertahan dua detik; beberapa huruf tambahan lagi cukup untuk menggantung server selama berjam-jam. Fenomena ini disebut catastrophic backtracking. Penawarnya: hindari quantifier bersarang atas kelompok karakter yang sama, dan bila memungkinkan sederhanakan (a+)+$ menjadi a+$ — keduanya mencocokkan string a yang sama persis tanpa percobaan pembagian yang meledak.
Tabel Keputusan #
Cara tercepat menahan diri: sebelum menulis pola, periksa dulu kecocokan situasi Anda pada tabel berikut.
| Situasi | Alat yang tepat |
|---|---|
| Awalan, akhiran, atau kehadiran teks tetap | method string: startswith, endswith, in, replace |
| Memotong pemisah sederhana | split dan strip |
| Pola berubah-ubah: digit berulang, pemisah opsional | regex |
| Tanggal dan angka berformat baku | regex untuk menangkap, int atau datetime untuk mengonversi |
| JSON, TOML, CSV, INI, URL | parser standard library |
| HTML dan XML | html.parser atau pustaka parser HTML |
Ada pepatah lama di kalangan pemrogram: orang yang menyelesaikan setiap masalah dengan regex akhirnya memiliki dua masalah. Regex tetap alat yang hebat di wilayahnya — teks semi-terstruktur — dan section ini telah menunjukkan betapa tajamnya. Keahlian terakhir adalah tahu kapan mengembalikannya ke sarung.