Idenya
Setiap scraper pada intinya terdiri dari dua langkah: temukan elemen, lalu baca elemen itu. Menemukan adalah bagian yang rapuh: kalau selector-mu rusak saat situsnya mengganti sebuah class CSS, scraper-nya ikut rusak. Playwright memberimu dua jenis locator:
- CSS selector, yang mendeskripsikan struktur halaman (
.product-card .price); - locator berorientasi pengguna, yang mendeskripsikan apa yang dilihat manusia (
get_by_role("button", name="Search")).
Locator itu resep, bukan hasil
Untuk mengambil apa pun dari sebuah halaman, kamu harus menunjuknya lebih dulu: "kartu-kartu produk", "harga di dalam kartu ini". Di Playwright, kamu melakukannya dengan locator. Ini satu locator untuk setiap kartu produk di halaman daftar:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://nusantara-gadget.test/products")
cards = page.locator(".product-card")
print(cards)
print(cards.count())
browser.close()
<Locator locator(".product-card")>
12
page.locator(...) belum mencari apa pun; ia adalah resep untuk menemukan elemen. Pencarian terjadi saat kamu bertindak terhadapnya (count(), inner_text(), click()), dan Playwright menjalankannya ulang setiap kali, jadi locator tidak pernah basi saat halamannya berubah.
CSS selector yang benar-benar akan kamu pakai
Markup kartu di halaman produk terlihat seperti ini:
<article class="product-card" data-id="1" data-category="phones">
<h3 class="title"><a href="/product/1">Galaxy A55 5G</a></h3>
<p class="brand">Samsung</p>
<p class="price" data-price="5999000">Rp 5.999.000</p>
...
<span class="stock in-stock">In stock</span>
</article>
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://nusantara-gadget.test/products")
print(page.locator(".product-card .title").first.inner_text()) # descendant
print(page.locator('[data-category="laptops"]').count()) # attribute
print(page.locator(".product-card:has(.sold-out)").count()) # has a child
print(page.locator("#product-list > article").count()) # id + direct child
print(page.locator('.product-card[data-id="3"] .price').inner_text())
browser.close()
Galaxy A55 5G
4
2
12
Rp 13.999.000
| Selector | Mencocokkan |
|---|---|
.price | elemen dengan class price |
#product-list | elemen dengan id product-list |
.card .price | .price di mana saja di dalam .card |
ul > li | li yang merupakan anak langsung dari ul |
[data-id="3"] | atribut yang sama dengan sebuah nilai |
.card:has(.sold-out) | kartu yang berisi elemen .sold-out |
Utamakan pengait yang stabil: id, atribut data-*, dan nama class yang bermakna (.price) lebih tahan terhadap redesain daripada path panjang seperti div > div:nth-child(3) > p.
Locator berorientasi pengguna
Locator ini menemukan elemen dengan cara manusia (atau screen reader) mendeskripsikannya, sehingga tetap berfungsi saat HTML-nya disusun ulang:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://nusantara-gadget.test/products")
print(page.get_by_role("heading", level=1).inner_text())
print(page.get_by_role("link", name="Next").get_attribute("href"))
print(page.get_by_placeholder("Search products").count())
print(page.get_by_text("Sold out").count())
print(page.get_by_role("link", name="iPhone 15").get_attribute("href"))
browser.close()
All products
/products?page=2
1
2
/product/3
get_by_role(role, name=...): link, tombol, heading, textbox, checkbox, baris...nameadalah teks yang terlihat (atau label). Inilah pilihan pertama yang direkomendasikan Playwright.get_by_text(text): elemen apa pun yang teksnya mengandungtext(exact=Trueuntuk mencocokkan seluruh teks).get_by_label(text),get_by_placeholder(text): field formulir.get_by_test_id(id): elemen dengandata-testid="...", saat developer menambahkan pengait untuk test.
Mempersempit: filter, nth, chaining
Sering kali sebuah selector mencocokkan terlalu banyak. Kamu hanya ingin produk Apple, atau harga di dalam satu kartu tertentu, atau hanya hasil pertama. Locator bisa dipersempit langkah demi langkah:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://nusantara-gadget.test/products")
cards = page.locator(".product-card")
apple = cards.filter(has_text="Apple")
print(apple.count())
print(apple.locator(".title").all_inner_texts())
print(cards.filter(has=page.locator(".sold-out")).locator(".title").all_inner_texts())
print(cards.nth(2).locator(".title").inner_text(), "|", cards.last.locator(".title").inner_text())
browser.close()
2
['iPhone 15', 'MacBook Air M3']
['Redmi Note 13 Pro', 'Vivo V30']
iPhone 15 | Swift Go 14
.filter(has_text=...)mempertahankan elemen yang berisi teks itu;.filter(has=locator)mempertahankan elemen yang berisi anak yang cocok..locator(...)pada sebuah locator mencari di dalam setiap kecocokan..first,.last,.nth(i)memilih satu kecocokan (mulai dari 0).
Strict mode
Aksi yang butuh satu elemen (inner_text, click, fill) menolak menebak saat sebuah locator cocok dengan beberapa elemen:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://nusantara-gadget.test/products")
page.locator(".price").inner_text()
Error: strict mode violation: locator(".price") resolved to 12 elements. Make the locator more specific, or use .first, .nth(i) or .all().
Error itu adalah fitur: ia mencegah scraper-mu diam-diam membaca harga yang salah. Buat locator-nya lebih spesifik, atau gunakan .first, .nth(i), atau method yang bekerja untuk banyak elemen (count(), all_inner_texts(), all()).
Utamakan locator berorientasi pengguna (get_by_role, get_by_text, get_by_label) dan pengait CSS yang stabil (id, data-*, class yang bermakna). Persempit dengan filter, chaining, dan nth. Strict mode melindungimu dari kecocokan yang ambigu.
Mengapa ini penting untuk kode yang bersih
Selector adalah kontrak antara scraper-mu dan situs yang tidak kamu kendalikan. Simpan di satu tempat (dict berisi selector bernama di bagian atas file), buat mereka mendeskripsikan makna alih-alih layout, dan redesain pun cukup diperbaiki dengan dua baris alih-alih perburuan di seluruh kode.
Rangkuman
page.locator(css)adalah resep malas, dijalankan ulang di setiap aksi.- CSS:
.class,#id,a b,a > b,[attr="v"],:has(...). get_by_role,get_by_text,get_by_label,get_by_placeholder,get_by_test_id.- Persempit dengan
.filter(has_text=/has=),.locator()berantai,.first/.last/.nth(i). - Aksi untuk satu elemen memunculkan error strict mode saat beberapa elemen cocok.