Catatan mengenai siapa yang menulis ini dan mengapa hal itu tidak terlalu relevan di sini: kami adalah Geonode dan kami menjual proxy kepada orang-orang yang melakukan pekerjaan ekstraksi data. Pilihan selektor tidak memengaruhi apa pun yang kami jual, dan satu-satunya kesamaan yang layak disebutkan adalah untuk tujuan diagnostik — selektor yang tidak cocok dengan apa pun dan permintaan yang diblokir terlihat identik dari luar, namun keduanya merupakan masalah yang sama sekali tidak terkait. Jika halaman berhasil dimuat dan selektor Anda tidak menemukan apa pun, referensi ini adalah tempat yang tepat dan tidak ada yang perlu diubah mengenai cara halaman tersebut diambil.
Selektor Dasar
| Selektor | Cocok dengan |
|---|---|
* | Setiap elemen |
div | Setiap elemen div |
.card | Elemen dengan kelas card |
#main | Elemen dengan id main |
.card.featured | Elemen dengan kedua kelas |
div.card | Elemen div dengan kelas card |
div, p | Elemen div dan elemen p |
Dua di antaranya patut diperhatikan.
.card.featured tanpa spasi berarti kedua kelas tersebut ada pada satu elemen. .card .featured dengan spasi berarti elemen dengan kelas featured di dalam elemen dengan kelas card. Satu karakter saja dapat mengubah makna secara total, dan ini adalah kesalahan ketik yang paling umum di bidang ini.
div, p adalah daftar selektor, bukan hubungan. Daftar ini mencocokkan semua elemen dalam salah satu grup, dan setiap bagian dievaluasi secara independen — sehingga div, p.note mencocokkan semua elemen div dan paragraf dengan kelas note, bukan elemen div dengan kelas note.
Kombinator
Elemen-elemen ini menunjukkan hubungan antar elemen.
| Selektor | Cocok dengan |
|---|---|
div p | p di mana saja di dalam div (keturunan) |
div > p | p yang merupakan anak langsung dari div |
h2 + p | p yang berada tepat setelah h2 (saudara sebaris) |
h2 ~ p | Setiap p setelah h2 dengan induk yang sama (saudara umum) |
Kombinator keturunan berupa spasi, sehingga mudah dimasukkan secara tidak sengaja dan mudah terlewatkan saat membaca.
> lebih tepat dan biasanya lebih andal. div > p tidak akan cocok dengan paragraf yang dibalut dalam <section> bulan depan — yang terdengar seperti kelemahan, namun seringkali justru sebaliknya, karena selektor yang diam-diam mulai cocok dengan lebih banyak elemen daripada yang dimaksudkan lebih buruk daripada selektor yang berhenti cocok dan memberi tahu Anda.
+ dan ~ adalah kombinator saudara dan keduanya hanya melihat ke depan. Tidak ada kombinator "saudara sebelumnya" dalam CSS; :has() memberikan efek tersebut secara tidak langsung, yang akan dibahas di bawah ini.
Pemilih Atribut
Seringkali merupakan opsi paling stabil yang tersedia, karena atribut biasanya dipilih secara sengaja.
| Pemilih | Cocok dengan |
|---|---|
[href] | Elemen dengan atribut href |
[href="/about"] | Nilai persis |
[href^="/docs"] | Nilai dimulai dengan |
[href$=".pdf"] | Nilai diakhiri dengan |
[href*="download"] | Nilai mengandung |
[class~="card"] | Nilai berupa daftar yang dipisahkan spasi yang mengandung kata card |
[lang|="en"] | Nilai berupa en atau dimulai dengan en- |
[data-state="open" i] | Pencocokan tanpa membedakan huruf besar/kecil |
Dua di antaranya jarang digunakan.
[class~="card"] sama persis dengan .card — operator ~= melakukan pencocokan kata yang dipisahkan oleh spasi, yang sesuai dengan kebutuhan atribut class. Mengetahui keberadaannya berguna terutama untuk memahami apa yang sebenarnya dilakukan oleh .card, serta untuk membuat selektor secara programatik ketika Anda memiliki nama atribut dalam variabel.
Bendera i membuat pencocokan tidak peka huruf besar-kecil, yang sangat berguna untuk nilai atribut yang huruf besar-kecilnya bervariasi di seluruh situs.
Khusus untuk ekstraksi, atribut data-* lebih diutamakan daripada kelas. Sebuah [data-testid="product-card"] dipilih oleh pengembang sebagai titik kait yang stabil; sedangkan nama kelas dapat dihasilkan ulang oleh alat build pada setiap proses deploy.
Pseudo-Kelas: Struktural
| Selektor | Cocok dengan |
|---|---|
:first-child | Elemen yang merupakan anak pertama dari induknya |
:last-child | Anak terakhir |
:only-child | Elemen yang tidak memiliki saudara sebaris |
:nth-child(3) | Anak ketiga |
:nth-child(2n) | Anak dengan nomor genap |
:nth-child(2n+1) | Anak dengan nomor ganjil |
:nth-child(-n+3) | Tiga anak pertama |
:nth-last-child(2) | Anak kedua dari belakang |
:first-of-type | Elemen pertama dari jenisnya di antara saudara sejenis |
:nth-of-type(2) | Elemen kedua dari jenisnya |
:empty | Elemen yang tidak memiliki anak, termasuk teks |
:root | Akar dokumen, biasanya html |
Varian "-of-type" lebih penting daripada yang disadari orang. p:first-child hanya cocok dengan paragraf jika paragraf tersebut merupakan anak pertama dari induknya — jadi jika judul muncul terlebih dahulu, tidak ada yang cocok. p:first-of-type cocok dengan paragraf pertama terlepas dari apa yang mendahuluinya. Untuk ekstraksi, yang kedua hampir selalu yang Anda maksud.
:nth-child(-n+3) untuk "tiga yang pertama" adalah ungkapan yang patut diingat dari sintaks An+B, bersama dengan :nth-child(n+4) untuk "yang keempat dan seterusnya". Menggabungkan keduanya menghasilkan rentang: :nth-child(n+2):nth-child(-n+5) memilih anak-anak dari yang kedua hingga kelima.
Pseudo-Kelas: Logis
Kelompok yang mengalami perubahan paling signifikan dalam beberapa tahun terakhir.
| Selektor | Cocok dengan |
|---|---|
:not(.hidden) | Elemen tanpa kelas hidden |
:is(h1, h2, h3) | Salah satu dari selektor yang tercantum |
:where(h1, h2, h3) | Sama dengan :is() tetapi dengan spesifisitas nol |
:has(> img) | Elemen yang mengandung keturunan atau saudara yang cocok |
:is() dan :where() mempersingkat daftar yang berulang. :is(article, section) > h2 menggantikan article > h2, section > h2, dan penghematan ini bertambah dengan cepat pada daftar yang lebih panjang. Satu-satunya perbedaan di antara keduanya adalah spesifisitas: :is() mengambil spesifisitas dari argumennya yang paling spesifik, sedangkan :where() selalu memberikan nilai nol.
:not() menerima daftar selektor di peramban modern, sehingga :not(.a, .b) berfungsi dan berarti "tidak ada". Perilaku argumen tunggal pada versi lama mengharuskan penggunaan rantai :not(.a):not(.b).
:has() adalah yang paling penting. Dokumentasi MDN menjelaskannya sebagai representasi "sebuah elemen jika salah satu selektor relatif yang diberikan sebagai argumen cocok dengan setidaknya satu elemen saat diikat terhadap elemen ini", yang menyediakan "cara untuk memilih elemen induk atau elemen saudara sebelumnya relatif terhadap elemen referensi". Statusnya adalah "Baseline widely available", didukung di berbagai peramban sejak Desember 2023.
div.card:has(span.sold-out) /* a card containing a sold-out marker */
h1:has(+ p) /* an h1 immediately followed by a p */
li:has(~ li.active) /* an li with a later active sibling */
label:has(input:checked) /* a label wrapping a checked input */
Tiga batasan yang didokumentasikan: ia "tidak dapat disarangkan" di dalam :has() lain, pseudo-elemen tidak valid di dalamnya atau sebagai titik acuan untuknya, dan spesifisitasnya mengikuti aturan yang sama seperti :is() dan :not() — argumen yang paling spesifik.
Pseudo-Kelas: Status dan Bentuk
| Selektor | Cocok dengan |
|---|---|
:hover, :focus, :active | Status interaksi |
:focus-visible | Fokus yang seharusnya menampilkan lingkaran terlihat |
:focus-within | Elemen yang berisi keturunan yang difokuskan |
:checked | Kotak centang yang dicentang, tombol radio, atau opsi yang dipilih |
:disabled, :enabled | Status kontrol formulir |
:required, :optional | Atribut validasi formulir |
:valid, :invalid | Status validasi batasan |
:placeholder-shown | Bidang masukan yang menampilkan teks placeholder-nya |
:target | Elemen yang sesuai dengan fragmen URL |
:visited, :link | Status tautan |
Sebagian besar hal ini berkaitan dengan gaya tampilan daripada ekstraksi data, dengan dua pengecualian. :checked benar-benar berguna untuk membaca status formulir dalam otomatisasi pengujian, dan :disabled memberi tahu Anda apakah suatu kontrol interaktif — yang sering kali menjelaskan mengapa klik tidak menghasilkan apa-apa.
Perhatikan bahwa :hover dan :focus tidak dapat diakses secara efektif menggunakan querySelectorAll dalam konteks pengikisan data, karena kedua status tersebut tidak ada dalam dokumen yang baru saja Anda parsing.
Spesifisitas, Secara Singkat
Hal ini relevan ketika selektor bersaing dalam menentukan gaya, dan tidak relevan untuk melakukan kueri — penting untuk diketahui karena kebingungan antara kedua konteks tersebut sering terjadi.
Spesifisitas dihitung berdasarkan tiga angka:
| Komponen | Disumbangkan oleh |
|---|---|
| ID | #main |
| Kelas, atribut, pseudo-kelas | .card, [href], :hover |
| Elemen, pseudo-elemen | div, ::before |
Jika dibandingkan dari kiri ke kanan: berapa pun jumlah kelas tidak akan pernah melebihi satu ID. Gaya inline mengungguli semuanya, dan !important mengungguli gaya inline, itulah sebabnya keduanya lebih merupakan pilihan terakhir daripada alat bantu.
Dua catatan modern. :where() tidak memberikan spesifisitas sama sekali, sehingga menjadikannya pilihan yang tepat untuk nilai default perpustakaan yang seharusnya dapat diubah oleh pengguna tanpa kesulitan. Dan :is(), :not(), serta :has() semuanya mengambil spesifisitas dari argumen yang paling spesifik, sehingga :is(#main, div) sama spesifiknya dengan #main.
Untuk querySelectorAll dan scraping, spesifisitas sama sekali tidak berlaku. Selektor tersebut cocok dengan suatu elemen atau tidak; tidak ada yang bersaing.
Pola untuk Ekstraksi
Selektor yang benar-benar melakukan pekerjaan dalam sebuah scraper.
Kartu produk dalam tata letak kisi:
div.product-grid > article.product-card
Tautan ke PDF:
a[href$=".pdf"]
Tautan eksternal:
a[href^="http"]:not([href*="example.com"])
Hook pengujian yang stabil, bukan sebuah kelas:
[data-testid="price"]
Kontainer yang berisi sesuatu:
tr:has(td.error)
article:has(img)
form:has(input:invalid)
Paragraf pertama sebuah artikel, terlepas dari apa yang ada di depannya:
article p:first-of-type
Lewati baris header:
tbody tr:not(:first-child)
Ambil baris kedua hingga kesebelas:
tbody tr:nth-child(n+2):nth-child(-n+11)
Elemen dengan token kelas tertentu, yang dibuat secara programatik:
[class~="card"]
Sel kosong yang ingin Anda kecualikan:
td:not(:empty)
Menulis Selektor yang Tidak Mudah Rusak
Referensi memberi tahu Anda apa saja yang tersedia. Ketahanan berasal dari pemilihan di antara berbagai opsi tersebut, dan perbedaan antara selektor yang tetap berfungsi setelah perancangan ulang dan selektor yang akan rusak minggu depan sepenuhnya bergantung pada apa yang menjadi dasarnya.
Urutkan anchor Anda berdasarkan seberapa sengaja mereka dipilih. Sebuah data-testid ada karena pengembang menempatkannya di sana untuk otomatisasi, sehingga ini adalah hal yang paling stabil di halaman. id berada di urutan berikutnya — biasanya disengaja, kadang-kadang dihasilkan secara otomatis. Elemen semantik atau peran ARIA stabil karena mengandung makna. Nama kelas yang ditulis secara manual cukup stabil. Nama kelas yang dihasilkan secara otomatis, seperti css-1x9dj2k, berubah pada setiap proses build dan tidak memiliki nilai apa pun. Selektor posisional adalah yang paling tidak stabil di antara semuanya, karena selektor ini mengkodekan bentuk halaman yang tepat pada satu momen tertentu.
Jadilah sespesifik yang dibutuhkan oleh maknanya, dan tidak lebih. body > div > div > div.content > p bersifat presisi namun rapuh: penambahan wrapper di mana pun dalam rantai tersebut akan merusaknya. .content p mengekspresikan kebutuhan yang sebenarnya dan tetap berfungsi meski terjadi restrukturisasi. Insting untuk menambah spesifisitas demi keamanan justru menghasilkan kebalikan dari keamanan.
Pilihlah selektor yang gagal secara jelas. Selektor yang cocok dengan nol elemen menimbulkan masalah yang jelas. Selektor yang cocok dengan terlalu banyak elemen secara diam-diam mengembalikan elemen pertama, yang mungkin masuk akal namun bisa saja salah. Jika diberi pilihan, pilihlah versi yang akan gagal daripada versi yang akan bergeser — div > p daripada div p ketika hubungan tersebut benar-benar langsung.
Simpan selektor di satu tempat. Empat puluh string selektor yang tersebar di seluruh basis kode adalah empat puluh risiko independen. Jika dikumpulkan dalam satu modul dengan nama yang deskriptif, mereka menjadi peta dari apa yang Anda andalkan, dan memperbarui setelah perubahan situs hanya membutuhkan waktu satu jam, bukan satu hari untuk menelusuri kode.
Pastikan bentuk dari apa yang Anda ekstrak. Ini adalah kebiasaan yang mendeteksi pergeseran diam-diam. Jika harga harus sesuai dengan pola mata uang, periksalah. Jika daftar tidak pernah memiliki kurang dari dua puluh item, anggap kurang dari dua puluh sebagai kesalahan. Tanpa ini, selektor yang mulai mencocokkan elemen yang salah akan menghasilkan data yang salah namun terstruktur dengan baik tanpa henti, dan tidak ada yang memicu pengecualian pada titik mana pun.
Simpan HTML yang telah Anda parsing. Ketika sebuah selektor rusak, membandingkan markup lama dengan yang baru akan menunjukkan kepada Anda dengan tepat apa yang berubah dalam hitungan detik. Mengunduh ulang untuk menyelidiki lebih lambat, menghabiskan bandwidth, dan mungkin memberikan Anda halaman yang berbeda dari yang sebenarnya gagal.
Hal-hal yang Masih Belum Dapat Dilakukan oleh CSS
Daftar singkat ini berisi alasan-alasan yang sah untuk menggunakan XPath.
Mencocokkan berdasarkan isi teks. Tidak ada standar :contains(). Standar ini pernah diusulkan namun ditinggalkan, dan tidak ada browser yang mengimplementasikannya. Inilah celah terbesar dan alasan mengapa pola "mencari nilai berdasarkan labelnya" menjadi ranah XPath — //dt[normalize-space()='Price']/following-sibling::dd[1] tidak memiliki padanan dalam CSS.
Menavigasi ke leluhur sembarang. :has() menyediakan pemilihan orang tua bersyarat — "div yang mengandung span ini" — tetapi tidak ada sumbu leluhur umum. Menelusuri beberapa tingkat ke atas ke wadah tertentu memerlukan XPath.
Fungsi string. Tidak ada pemotongan, tidak ada substring, tidak ada konversi huruf besar-kecil, tidak ada penggabungan. Apa pun yang Anda cocokkan, Anda memprosesnya setelahnya dalam bahasa pemrograman Anda.
Mengeksekusi kueri XML dengan namespace. CSS tidak memiliki dukungan namespace yang berarti. Untuk peta situs, RSS, atau SOAP, gunakan XPath.
Memilih saudara sebelumnya secara langsung. :has() memberikan efek tersebut — h2:has(+ p) memilih heading — tetapi tidak ada kombinator - yang setara dengan +.
Itulah daftar lengkap untuk keperluan praktis. Segala hal lain yang biasanya orang lakukan dengan XPath, kini dapat dilakukan oleh CSS, dan biasanya dengan kode yang lebih mudah dibaca.
Perbedaan Lingkungan yang Perlu Diperiksa
Hal yang paling sering mengejutkan terkait selektor adalah adanya fitur yang tersedia di tempat lain, bukan di tempat Anda menjalankannya.
Browser mendukung semua yang tercantum di sini, termasuk :has(), :is(), :where(), dan :not() dengan argumen ganda.
Parser sisi server sangat bervariasi. cssselect dari Python, yang digunakan oleh lxml dan Scrapy, menerjemahkan CSS menjadi XPath — yang berfungsi dengan baik untuk selektor klasik dan kurang baik untuk selektor yang lebih baru. Dukungan untuk :has() khususnya tidak universal, dan selektor yang berfungsi di konsol browser mungkin menimbulkan kesalahan atau diam-diam tidak mengembalikan apa pun di parser.
BeautifulSoup menggunakan soupsieve untuk metode select()-nya, yang memiliki cakupan yang baik namun tidak identik, dan sama sekali tidak mendukung XPath.
Playwright dan Selenium menggunakan mesin browser, sehingga dukungannya sesuai dengan browser — dengan Playwright juga menawarkan penentu teks dan peran yang mencakup banyak hal yang tidak dapat diekspresikan oleh CSS.
Kebiasaan praktis: uji selektor di lingkungan yang akan menjalankannya. Konsol browser memang nyaman, tetapi menguji implementasi yang berbeda terhadap DOM pasca-JavaScript, yang merupakan dua hal yang dapat menyesatkan Anda.
Pertanyaan Lainnya
Apa perbedaan antara .card.featured dan .card .featured?
Yang pertama, tanpa spasi, akan mencocokkan satu elemen yang memiliki kedua kelas tersebut. Yang kedua, dengan spasi, akan mencocokkan elemen dengan kelas featured yang berada di dalam elemen dengan kelas card. Satu spasi saja sudah cukup untuk mengubah arti secara total, dan ini merupakan kesalahan selektor CSS yang paling umum.
Bagaimana cara memilih elemen induk dalam CSS?
Dengan :has() — div:has(> span.price) memilih div alih-alih span. Fitur ini telah tersedia secara luas di berbagai peramban sejak Desember 2023. Tidak ada selektor leluhur umum, sehingga untuk mencapai wadah tertentu beberapa tingkat di atasnya masih memerlukan XPath.
Apakah selektor CSS dapat mencocokkan teks?
Tidak. Tidak ada pseudo-class standar :contains(); pseudo-class ini pernah diusulkan namun tidak pernah diadopsi. Pencocokan berdasarkan konten teks memerlukan XPath, atau lokator khusus perpustakaan seperti getByText() dari Playwright.
Apa perbedaan antara :nth-child dan :nth-of-type? `
`:nth-childmenghitung semua anak dari elemen induk;:nth-of-typehanya menghitung elemen dengan tipe yang sama. Jadi,p:first-childtidak akan mencocokkan apa pun jika ada judul di depan, sedangkanp:first-of-type`` akan mencocokkan paragraf pertama terlepas dari apa pun.
Apa fungsi :is() dan apa perbedaannya dengan :where()?
Keduanya menerima daftar selektor dan mencocokkan salah satunya, sehingga mempersingkat selektor yang berulang. Satu-satunya perbedaan adalah spesifisitas: :is() mengadopsi spesifisitas dari argumennya yang paling spesifik, sedangkan :where() selalu memberikan nilai nol — yang menjadikan :where() ideal untuk nilai default yang seharusnya mudah diganti.
Bagaimana cara kerja spesifisitas CSS?
ID memiliki prioritas lebih tinggi daripada kelas, atribut, dan pseudo-kelas, yang pada gilirannya memiliki prioritas lebih tinggi daripada selektor elemen dan pseudo-elemen, jika dibandingkan komponen per komponen. Gaya inline lebih diutamakan daripada selektor, dan !important lebih diutamakan daripada keduanya. Semua ini tidak berlaku saat Anda menggunakan selektor untuk melakukan kueri, bukan untuk memberi gaya.
Apakah selektor CSS lebih baik daripada XPath untuk pengambilan data?
Untuk pemilihan berdasarkan class, id, atribut, dan struktur, ya — lebih singkat dan jelas. Untuk mencocokkan konten teks atau menelusuri ke atas hingga ke leluhur sembarang, XPath diperlukan. Sebagian besar pustaka pengikisan mendukung keduanya, dan menggunakan masing-masing sesuai konteksnya adalah hal yang wajar.
Mengapa selektor saya berfungsi di browser tetapi tidak di pengikis saya?
Ada dua penyebab umum. DOM browser sudah diproses oleh JavaScript, sedangkan parser Anda melihat HTML mentah, sehingga elemen tersebut mungkin tidak ada dalam data yang Anda ambil. Selain itu, implementasi CSS di sisi server tidak mencakup semua selektor modern — khususnya :has() yang tidak didukung secara konsisten di luar browser.
Kesimpulan
Inti kerja selektor CSS sebenarnya cukup sederhana: tipe, kelas, dan id untuk identitas; empat kombinator untuk hubungan; selektor atribut untuk elemen yang dipilih secara sengaja; serta beberapa pseudo-class untuk posisi dan status.
Tiga kebiasaan berikut ini lebih efektif dalam meningkatkan kualitas selektor daripada sekadar mengetahui sintaks tambahan. Pilihlah > daripada spasi kosong jika hubungannya benar-benar langsung, karena selektor yang berhenti mencocokkan lebih berguna daripada yang mulai mencocokkan terlalu banyak. Pilihlah atribut data-* daripada nama kelas, karena kelas dihasilkan ulang oleh alat build sedangkan atribut dipilih oleh manusia. Dan pilihlah :nth-of-type daripada :nth-child ketika yang dimaksud adalah "paragraf kedua" daripada "anak kedua".
:has() Apakah penambahan ini layak diadopsi secara sengaja? Pemilihan induk dan kondisi saudara dalam CSS biasa menghilangkan sebagian besar hal yang sebelumnya memaksa orang menggunakan XPath, dan fitur ini telah tersedia secara luas cukup lama untuk diandalkan di browser. Di luar browser, periksa parser Anda terlebih dahulu — di situlah kejutan-kejutan yang tersisa berada.
